Verze: 0.10 (jádro produktu hotové v0.9 + předběžný
market research cílovky kurzu jako podklad pro úpravy dotazníku — fáze
A) Datum: 2026-06-30 Autor: Honza
Gronka + Claude (Opus 4.8) Status: Návrh ke schválení —
připraveno jako zadání pro ultraplan Přílohy:
schema.sql (CREATE TABLE + RLS + indexy),
api-contracts.md (rozhraní mezi subsystémy),
HANDOFF-2026-06-30.md (shrnutí stavu)
Review: v0.1 prošla adversariálním panelem (architekt,
datový model, security/GDPR, produkt/sales) — 51 nálezů (12 critical, 15
high), v0.2 je zapracovala. v0.3 vyřešila 3 sporné body (cenové pásmo,
Gemini tier, metriky) + ukotvila strategický kontext (warmup ke kurzu).
v0.4 doplnila §10b: identita přes účet, limity volání. v0.5
aktualizovala §0b (reálný stav: OpenRouter embeddingy, RAG hotový) +
přidala SQL a API přílohy. v0.7 (audit): sjednocen chat model napříč
dokumentem na gemini-3.1-flash-lite přes OpenRouter;
přerámován §0b na „co z legacy přebíráme vs. stavíme od nuly”.
v0.8–0.9 (2026-06-30): dotaženo jádro produktu —
matching rubrika (§6.5: matchProducts(),
buyer_role B2B/B2C, dvě vrstvy), maturity rubrika (§6.4:
maturityEval(), signály, práh, keyword override),
gamifikovaný dotazník (§5.5: gap-analýza reálného kódu marketéra + plán
úprav + market research vrstva), životní cyklus konverzace (§6.9:
re-entry, cold lead, anon merge), rozhodnutí o dvou
klonech (warmup zdarma + budoucí prémiový v kurzu).
Druhý nezávislý audit (council — 4 kritici:
skeptik/pre-mortem/budget-hawk/skills-engineer) → 4 must-fix opraveny
(trigger klasifikátoru + keyword override, cold lead jen se souhlasem,
sjednocení identity na magic-link + events tabulka, raw_answers
vs. výmaz). GDPR model souhlasů ověřen u ÚOOÚ
(§5.5/§9.4: titul podle účelu — AI zpracování = nutnost služby,
kontakt/marketing = aktivní souhlas; „pokračováním souhlasíte” je
neplatné). v0.10 (2026-06-30): předběžný market research cílovky
zářijového kurzu (fáze A, jako podklad pro úpravy dotazníku
§5.5) — deep research přes 3 modely
(research-cilovka-kurzu-2026-06.md) + keyword/SEO/audience
data z Marketing Mineru, scrapingu SEBA a FB Ads
(research-keywords-marketing-miner-2026-06.md). Klíčový
princip pro dotazník: dvě roviny jazyka (emoční pain
fráze do dotazníku, vyhledávané termíny do SEO). Nemění logiku produktu,
jen podkládá §5.5 daty.
Jak číst tento spec (důležité pro ultraplan): Toto
je zadání pro přestavbu od základu, ne popis
stávajícího kódu. Existující appka v AI-clon/ byla
throwaway test napojení na OpenRouter (autor: brigádník přes
Antigravity) — ověřila, že chat model, embeddingy a RAG fungují.
Na téhle appce se NESTAVÍ. Z legacy se přenese jen
několik izolovaných, ověřených kusů (viz §0b „CO PŘEBÍRÁME”); zbytek je
greenfield. Když spec píše „funguje” nebo „implementováno”, myslí tím
princip ověřený v testu, ne hotový modul nové platformy.
0. Co se změnilo proti
v0.1 (shrnutí reworku)
Pozn. k „stávajícímu”: legacy appka (brigádník,
Antigravity) byla throwaway test napojení — viz §0b. Tam, kde tato sekce
mluví o „kódu” / „rozšíření stávajícího”, jde o stav toho testu;
platforma se staví od základu, ne jako jeho rozšíření.
Review odhalilo, že v0.1 popisovala jako „rozšíření stávajícího” tři
vrstvy, které v legacy testu neexistují a postaví se od
nuly: 1. Perzistence stavu — legacy chat nic neukládá
(bezstavový route.ts, stav v localStorage).
Doplněn jako samostatný subsystém (E). 2. Strukturované
signály — model produkuje jen prózu; matching/lead/banner
potřebují strojově čitelný výstup. Doplněna architektura klasifikátoru
(§6.4). 3. Bezpečnostní vrstva — v DB žádná RLS;
plánovaný anon-zápis z WP = únik CRM. Přepracováno (§3.6, §9). Dále
opraveno: identita (handoff token místo user_id v URL),
datový model (cenové varianty + měna), GDPR (výmaz, souhlas, přenos do
Gemini), sales logika (rozpočet jako signál ne strop, warm leady),
metriky, fázování.
0b.
VZTAH K LEGACY APPCE — co přebíráme vs. co stavíme od nuly (pro
ultraplan klíčové)
Stav k 2026-06-28. Existující appka v AI-clon/
je throwaway test (brigádník, Antigravity) — sloužila k ověření
napojení na OpenRouter a funkčnosti modelů/RAG. Na ní se
NESTAVÍ. Platforma se staví od základu; z legacy se přenese jen
několik izolovaných ověřených kusů. Tahle sekce říká, co přesně
recyklovat a co psát greenfield.
Posouzení znovupoužitelnosti provedeno čtením reálného kódu
(2026-06-28). Závěr: legacy není slepá ulička ani základ — je to
svazek 2 kvalitních modulů + infrastruktura, kolem nichž je
throwaway frontend a bezstavový endpoint k zahození.
♻️
CO PŘEBÍRÁME Z LEGACY (ověřené, znovupoužitelné — kopírovat, ne psát
znovu)
- OpenRouter chat modul —
src/lib/chat.ts. Completions přes OpenRouter
(OpenAI-kompatibilní /chat/completions), implicit caching
(cache_control: ephemeral), reasoning
(effort), usage tracking (cached/reasoning tokens, cost).
Čistý izolovaný modul, kvalitní error handling. Přenést jak
je, jen vyměnit system prompt (nový pro sales/orchestraci).
Model: gemini-3.1-flash-lite přes OpenRouter (ověřeno
funkční).
- OpenRouter embedding modul —
src/lib/embeddings.ts (embedText).
google/gemini-embedding-2, 768-dim, validace dimenzí,
timeout. Přenést jak je. KRITICKÉ: ingest i chat MUSÍ
stejný embedding model, jinak vektory nesedí.
- Infrastruktura — Dockerfile (multi-stage,
node:24-alpine, standalone), docker-compose, next.config, tsconfig.
Přenést jak je, rozšířit env o nové klíče (auth,
CRM).
- RAG základ — Supabase pgvector, tabulka
documents (768-dim), funkce match_documents.
Migrace 00000000000001_schema_documents.sql +
00000000000002_grants_documents.sql. Přenést +
rozšířit (metadata/filtr §13, CRM tabulky §3). 208 chunků z
knowledge/ je naindexováno — RAG princip ověřen
funkční.
- Chunking logika —
scripts/ingest.mjs
(deterministický sliding-window, ~5 chunků/soubor). Přenést
princip; file→DB pipeline se upraví podle nového zdroje
obsahu.
- Markdown + mermaid renderer + styling — z
src/app/page.tsx (MarkdownMessage) a
globals.css (glass panel, gradients, Tailwind). Vykopírovat
komponentu a styly do nového frontendu (chat fáze je bude
potřebovat).
🔨
CO STAVÍME OD NULY (greenfield — throwaway legacy se zahazuje)
- (C) Chat endpoint + orchestrace — legacy
route.ts je bezstavový throwaway
(request→reply, žádný DB zápis kromě čtení). Nový /api/chat
(§6, api-contracts §3): perzistence zpráv, čtení profilu ze session,
klasifikátor signálů, matching. Z legacy se recykluje jen volání
chat.ts/embeddings.ts.
- System prompt — legacy
src/lib/prompt.ts je hyperspecifický pro „AI Petr mentor” →
přepsat pro sales/lead-gen tone + orchestrační logiku
(maturity, produktové doporučení). Princip „strukturovaný cachovatelný
prompt s kostrou” zůstává (§6.7).
- (A) Gamifikovaný dotazník — neexistuje (jen
prototyp marketéra protocol-dotaznik.pages.dev). Custom React embed do
WP/Elementor.
- (B) CRM / datová vrstva — neexistuje. Tabulky
user_profiles, conversations, messages, leads, products,
product_variants, product_terms, consents, usage_limits (viz
schema.sql).
- (D) Admin — neexistuje (legacy má jen tlačítko
„Přihlásit se” = alert placeholder, k zahození).
- (E) Orchestrace stavu + klasifikátor — neexistuje.
Perzistence konverzací serverově, strukturované signály (JSON).
- Frontend (dotazník→chat→lead flow) — legacy
page.tsx je chat-only s localStorage sidebar
session managementem. Zahodit (kromě markdown rendereru
+ stylů výše); nový UI flow se píše od nuly.
- Identita/účet — neexistuje (Supabase Auth je v
legacy vypnutý). Perzistence konverzace v legacy je jen
localStorage (brain_sessions) → nahradí
DB.
- Limity volání, matching, sales briefing —
neexistuje.
⚙️ KLÍČOVÉ TECHNICKÉ
FAKTY pro implementaci
- Vše přes OpenRouter (chat i embeddingy, jeden klíč
OPENROUTER_API_KEY). NE přímý Google
(@google/genai) — free tier nemá caching (limit=0). Chat
model gemini-3.1-flash-lite, embedding
google/gemini-embedding-2, oba s prefixem
google/ v plném názvu. Sjednoceno napříč specem (dřív
§0b/§6.1 uváděly zastaralý gemini-3.5-flash — opraveno v
v0.7).
- Embedding + chat model v
.env (jeden zdroj pravdy).
Ingest i chat MUSÍ stejný embedding model.
- Chat běží v kontejneru, na Supabase sahá přes
host.docker.internal:54321.
- Nasazování (od 2026-07-01): „nic ven / dev jen
lokálně” ZRUŠENO — jedeme normální pipeline (Docker lokální test → push
→ Supabase cloud + server). Hlídat jen osobní data (Gemini přes
OpenRouter = prod vypnutý logging/trénink) a nikdy neodeslat reálný
mail/webhook subjektům při testu. Veškerá AI komunikace jde na
OpenRouter.
- Mandát: pro tento projekt NEPLATÍ „nezasahuj do
cizího kódu” — cíl je prémiová appka stavěná od základu, legacy je
throwaway.
1. Účel a vize
1.0 Strategický
kontext (čti první — určuje priority)
AI klon NENÍ konečný cíl — je to warmup nástroj na vrcholu
trychtýře k plánovanému online kurzu Petra Vojnara (budoucí
vlajková loď SEBA Academy). Důsledky pro celou platformu: -
Teď: klon zahřívá zájemce, dává hodnotu a doporučuje
stávající produkty (mentoring/workshopy/masterclass). -
Až bude kurz: stane se prioritním
produktem; matching ho upřednostní, klon povede primárně k
němu. - Architektura na to musí být připravená (kurz
jako budoucí prioritní produkt v katalogu + matchingu), ale sám kurz se
nedefinuje, dokud neexistuje. - Cenové pásmo v
dotazníku slouží i jako price research pro
kurz (kolik je publikum ochotné investovat) — viz §5.3.
1.1 Co stavíme
Z existujícího AI chatbota „AI Petr” (digitální dvojče Petra Vojnara)
děláme platformu pro kvalifikaci zájemců a prodej vzdělávacích
produktů SEBA Academy — lead-generation a sales nástroj, který:
1. Pozná zájemce už při vstupu (gamifikovaný dotazník → profil). 2. Vede
hodnotnou konverzaci v Petrově stylu (čerpá z knowledge kurzu,
perzistentně si pamatuje stav). 3. V pravou chvíli (kontextová zralost)
doporučí vhodný produkt z portfolia podle profilu a
vývoje konverzace. 4. Posbírá/doplní lead (včetně „warm” leadů) a uloží
do vlastního CRM. 5. Petrovi dá v zabezpečeném admin
rozhraní sales briefing ke každému leadu.
1.2 Mimo rozsah verze 1 (YAGNI)
- Online platba v chatu (zájemce se objedná přes web/kontakt; chat
kvalifikuje a předá lead).
- Automatické plánování do kalendáře (Petr kontaktuje sám; termín =
preference).
- Online vzdělávací program (zatím neexistuje; doplní se do
katalogu).
- Plnohodnotný e-mailing (CRM nachystá strukturu pro notifikaci;
mailing nástroj se nestaví).
- (Pozn.: identita v1 = magic-link účet, ne handoff
token — viz §5.2/§10b, sjednoceno council fixem. Cross-device návrat
magic linkem je tedy součást v1, ne odložený plán.)
1.3 Úspěšná kritéria
- Zájemce projde dotazníkem a vpadne do chatu s předvyplněným profilem
(Petr se neptá na to, co zná).
- Vazba profil↔︎konverzace je bezpečná (žádná krádež identity, žádný
IDOR).
- Chat při zralé konverzaci doporučí 1–2 relevantní produkty podle
segmentu a situace (rozpočet jako signál, ne strop).
- Vznikne strukturovaný lead v CRM (i „warm” bez explicitního „chci”),
Petr ho vidí jako sales briefing.
- Dev: vše lokálně, jediná externí komunikace Gemini,
jen syntetická data. Prod: osobní data jen na placeném
Gemini tieru (viz §9.5).
2. Architektura platformy
2.1 Subsystémy (5)
┌─────────────────────────┐ ┌─────────────────────────┐
│ (A) Gamifikovaný │ │ (C) AI chat „AI Petr" │
│ dotazník │ │ Next.js / Docker │
│ custom React embed │ │ - čte profil (přes │
│ na WP stránce (Elementor)│ │ session, ne URL) │
│ sebacademy.cz │ │ - doporučuje produkty │
│ │ │ - sbírá/doplňuje lead │
└───────────┬─────────────┘ └───────────┬─────────────┘
│ POST profil │ čte/píše přes
│ (server endpoint, │ server endpoint
│ ne anon z browseru) │
▼ ▼
┌───────────────────────────────────────────────────────────────┐
│ (B) Datová vrstva / CRM (Supabase, RLS default-deny) │
│ profily · konverzace+zprávy · leady · katalog · variants · │
│ termíny · consent log · knowledge (RAG pgvector) │
└───────────────────────────┬───────────────────────────────────┘
┌─────────────┴──────────────┐
▼ ▼
┌─────────────────────────┐ ┌─────────────────────────┐
│ (E) Orchestrace stavu │ │ (D) Admin pro Petra │
│ + klasifikátor │ │ (auth, server-side) │
│ - perzistence konverzace │ │ - leady + sales briefing │
│ - maturity/segment/lead │ │ - správa katalogu/termínů│
│ jako strukturovaný JSON│ │ - audit přístupů │
└─────────────────────────┘ └─────────────────────────┘
(E) je nový subsystém odhalený review — bez
perzistence stavu a strukturovaných signálů nemůže fungovat matching,
lead ani admin.
2.2 Princip: DB je jediná
autorita
Supabase je centrální mozek i zdroj pravdy o stavu.
Frontend (chat UI) přestává být zdrojem pravdy — konverzace se ukládá
serverově po každém tahu. Subsystémy komunikují přes Supabase, ne mezi
sebou. Identita subjektu se řeší přes server-side session, ne přes
URL.
2.3 Technický
stack (základ z legacy testu, viz §0b)
- Chat: Next.js 16.1.6, React 19.2.3. AI přes
OpenRouter (OpenAI-kompatibilní
/chat/completions
i /embeddings, jeden klíč) — NE @google/genai
přímo (free tier bez caching). @supabase/supabase-js.
(Legacy má @google/genai v deps jako pozůstatek testu — v
přestavbě se nepoužívá, lze odstranit.)
- DB: Supabase, pgvector. Dev = lokální (CLI). Prod =
Supabase Cloud EU + DPA (viz §9.5).
- Dotazník: custom React appka embedovaná do
WP/Elementor (iframe/script — viz §11/1), sdílí design s chatem.
- Admin + server endpointy: součást Next.js appky,
service_role pouze v server-only modulech.
Hosting architektura (ROZHODNUTO 2026-06-30) — tři vrstvy,
každá kde má:
| Vrstva |
Kde běží |
Pozn. |
| WordPress (web + promo kurzu) |
NextHosting PRO (hlavní doména sebacademy.cz) |
PHP/MySQL, stávající. Odkazuje/embeduje appku. |
| Next.js appka (dotazník + chat + admin) |
NextHosting PRO, subdoména
ai.sebacademy.cz (SSL funguje), prod root
/home3/uqwbayti/ai.sebacademy.cz |
Node 20/22 přes Passenger (CloudLinux Node App
Manager). Bez Dockeru — NextHosting Docker nemá →
next start (standalone build), za LiteSpeed reverse
proxy. |
| Supabase (PostgreSQL + pgvector + Auth) |
Supabase Cloud, EU region (externě), Free
tarif |
Nutně externí — NextHosting má jen MariaDB/MySQL,
ne PostgreSQL, ani pgvector (potřebný pro RAG). DPA podepsat. Tarif viz
níže. |
Supabase tarif (ROZHODNUTO 2026-06-30): Free + vlastní
zálohování. Odhad provozu (peak ~4 000 lidí/měsíc, pár dotazů,
jen text) je hluboko pod Free limity (50k MAU, 500 MB DB, 5 GB egress) —
kapacitně Free stačí ~12×. Projekt je zatím nejistý → neplatit Pro ($25)
předem. Dvě slabiny Free (uspání po týdnu nečinnosti + žádné zálohy)
řeší denní cron na NextHostingu: - pg_dump
proti prod Supabase → záloha-<datum>.sql na disk
exonu (120 GB; data jsou KB–MB, vejdou se tisíce záloh). Rotace 30 dní.
- Tentýž běh drží projekt naživu (dotaz na DB = reset
7denní inaktivity) → appka nespadne kvůli uspání. - Robustnost
(povinné): cron po dumpu ověří, že soubor existuje a NENÍ
prázdný; při selhání pošle alert (mail). Jinak tichý výpadek = ani
záloha, ani keep-alive. - Restore otestovat jednou
ručně (psql < záloha.sql do čistého projektu) —
neověřená záloha je jen pocit. - Upgrade na Pro ($25)
zvážit, až (a) poroste provoz nad pohodlí Free, nebo (b) bude vadit
závislost na vlastním cronu místo managed záloh. Není to teď nutné.
- „Vše na exonu kromě DB”: appka i web na
NextHostingu (jednotná značka, subdoména), Supabase externě (technická
nutnost, ne volba).
- Sdílení dat s WordPressem (pokud bude potřeba —
produkty/objednávky): přes WP REST API, NE sdílená DB
(WP=MySQL, klon=Supabase=PostgreSQL, různé databáze).
- Pozn. pro ultraplan / vrstvu 1: ověřit Next.js 16
přes Passenger bez Dockeru (standalone výstup, port binding, env
proměnné na CloudLinux). NextHosting nepodporuje dlouhoběžící daemony
nad rámec app procesu — klasifikátor/async úlohy řešit v rámci request
lifecycle nebo přes cron (min. interval 1 min), ne jako samostatný
worker.
3. Datový model (Supabase)
documents (RAG, pgvector 768) už existuje. Níže
nové/upravené. Všechny tabulky s osobními údaji: RLS ON,
default-deny (§3.6). Všude created_at +
updated_at (+ trigger), FK indexy, soft delete
deleted_at kde jsou osobní data.
3.1 user_profiles —
profil zájemce
| sloupec |
typ |
popis |
| id |
uuid PK |
|
| created_at / updated_at |
timestamptz |
|
| deleted_at |
timestamptz NULL |
soft delete (GDPR) |
| name / email / phone |
text / text / text NULL |
kontakt (phone volitelný) |
| profession / comm_goal / situation / desired_outcome |
text |
z dotazníku |
| previous_attempts / prior_experience |
text NULL |
|
| perceived_value |
text NULL |
cenové pásmo — jen signál, ne strop (§6.5) |
| has_company |
boolean NULL |
firma/tým (sekundární/legacy signál — matching řídí
buyer_role) |
| buyer_role |
text NULL CHECK in (self/team_lead/org_decision) |
kdo kupuje — řídí B2B vs. B2C větev matchingu
(§6.5.2, A4). Z dotazníku C2.1 nebo doplněno v chatu. |
| raw_answers |
jsonb |
immutable archiv odpovědí (+
questionnaire_version) |
| questionnaire_version |
text |
verze dotazníku (interpretace raw) |
- Partial UNIQUE na
email WHERE deleted_at IS NULL + upsert podle e-mailu →
jeden subjekt místo N duplicit (mobil/desktop/návrat). Alternativa:
povolit duplicity + merge v adminu (rozhodnout — §11/8).
- Souhlas NENÍ boolean zde — řešen samostatně v
consents
(§3.7).
raw_answers (immutable) vs. strukturovaná pole
(řeší D7 contact_delta): raw_answers
= nedotknutelný archiv odpovědí dotazníku (pro research, nikdy se
nepřepisuje). Strukturovaná sloupce (profession,
situation, buyer_role,
has_company…) jsou odvozená/obohatitelná —
klasifikátorový contact_delta (co se nově zjistilo v chatu,
§6.4) aktualizuje tyto sloupce, ne
raw_answers. Tím se profil obohacuje konverzací, ale
původní odpovědi zůstávají auditovatelné.
- Vazba na účet (§10b):
auth_user_id
uuid NULL FK → Supabase Auth (když si zájemce nastaví účet/heslo).
Identita pro limity + návrat. Dokud nemá účet, profil žije jen přes
magic-link session.
3.1b
usage_limits — počítadlo zpráv (§10b.3)
| sloupec |
typ |
popis |
| id |
uuid PK |
|
| subject_key |
text |
identita: account:<id> /
session:<id> / ip:<hash> (vrstvený
limit) |
| window_start |
timestamptz |
začátek počítaného okna (denní/klouzavé) |
| message_count |
int |
počet zpráv v okně |
| updated_at |
timestamptz |
|
Index na (subject_key, window_start). Server
inkrementuje atomicky při každém tahu, kontroluje měkký/tvrdý práh. IP
ukládat jako hash (GDPR minimalizace).
3.2
conversations — konverzace (hlavička)
| sloupec |
typ |
popis |
| id |
uuid PK |
|
| user_id |
uuid FK → user_profiles NULL |
NULL = anonymní (přímý vstup) |
| created_at / updated_at / last_message_at |
timestamptz |
last_message_at pro řazení |
| deleted_at |
timestamptz NULL |
|
| state |
jsonb |
orchestrační stav (§6.4): maturity, segment_guess,
offered_products[], known_fields, last_classified_at |
3.3 messages — zprávy
(append-only)
| sloupec |
typ |
popis |
| id |
uuid PK |
|
| conversation_id |
uuid FK → conversations |
|
| created_at |
timestamptz |
|
| role |
text CHECK (user/model/system) |
|
| content |
text |
|
Samostatná tabulka (ne messages jsonb v conversations) —
append-only, stránkovatelné, bez race při read-modify-write blobu. Stav
„jak daleko jsme” žije v conversations.state, NE v ořezané
historii.
3.4 leads —
kvalifikovaný lead (i warm)
| sloupec |
typ |
popis |
| id |
uuid PK |
|
| user_id |
uuid FK → user_profiles NULL |
NULL povoleno (lead z anon konverzace) NEBO se nejdřív založí lazy
profil |
| conversation_id |
uuid FK → conversations |
|
| created_at / updated_at / contacted_at / status_changed_at |
timestamptz |
|
| status |
text CHECK |
new / warm / contacted /
won / lost |
| temperature |
text CHECK |
hot / warm / cold (pro řazení Petrem) |
| source |
text CHECK |
questionnaire / direct (atribuce) |
| recommended_product_ids |
jsonb |
co systém nabídl |
| selected_variant_id |
uuid FK → product_variants NULL |
co zájemce vybral |
| preferred_term_id |
uuid FK → product_terms NULL |
preferovaný termín (ne volný text) |
| contact_name / contact_email / contact_phone |
text |
immutable snapshot v okamžiku vzniku
(dokumentováno) |
| sales_briefing |
text |
viz §6.6 — ne profil, ale předávací briefing |
3.5
products + product_variants +
product_terms
products (evergreen identita produktu):
| id | slug (UNIQUE) | name | category (ENUM/CHECK:
mentoring/firemni/workshop_open/masterclass) | segment (CHECK:
individual_low/individual_high/company) | description | url | active
|
product_variants (řeší cenové varianty
+ měnu — klíčová oprava): | id | product_id FK | term_id FK NULL |
variant_name (NULL/‘Standard’/‘Premium’) | currency (CHECK ‘CZK’/‘EUR’)
| price_minor int (haléře/centy) | vat_included bool | price_from int
NULL | is_custom_quote bool | active | > Masterclass = 2 varianty na
termín (Standard/Premium). Otevřený workshop = CZK i EUR varianta.
Firemní = is_custom_quote=true + price_from.
DPH přes vat_included, ne volný text.
product_terms (jen kapacita/místo/datum
— BEZ ceny, ta je ve variants): | id | product_id FK | date | location |
capacity | spots_left CHECK(0..capacity) | active |
3.6 RLS (Row Level
Security) — kritická vrstva
- Default-deny na všech tabulkách s osobními daty
(
user_profiles, conversations,
messages, leads, consents).
- Anon klíč (client-side): POUZE čtení katalogu
(
products/product_variants/product_terms
WHERE active) — nic víc, žádný přístup k osobním datům.
- Veškerý zápis osobních dat jen přes server endpointy se
service_role (validace, rate-limit, consent check).
- Pravidlo do specu: každá nová tabulka s osobními údaji = RLS
ON + default deny, policy testovat adversariálně (pokus o čtení
cizího řádku). RLS i lokálně (dev/prod parita —
§9.6).
3.7
consents — granulární souhlas (GDPR čl. 7)
id | user_id FK | created_at | purpose (CHECK:
qualification_contact / ai_processing_google / marketing) | granted bool
| consent_version text | consent_text_snapshot text | source text
|
Jeden boolean nestačí: souhlas musí být per účel + doložitelný (kdy,
jaké znění, verze). Viz §9.4.
3.8 GDPR výmaz (mechanismus)
- Výmaz = anonymizace (name/email/phone/situation →
NULL/‘[smazáno]’,
raw_answers → NULL —
není trvale immutable, GDPR > archiv), zachová strukturální záznam
pro účetnictví.
- FK
leads.user_id / conversations.user_id:
ON DELETE SET NULL (ne CASCADE — netiše nemazat
historii).
messages.conversation_id: ON DELETE
RESTRICT (council fix) — konverzaci s historií NELZE tvrdě
smazat, chrání audit stopu. Konverzace se ruší jen soft
(deleted_at); messages se při výmazu anonymizují UPDATE, ne
DELETE. Doporučeno revoke DELETE i pro service_role.
gdpr_deletion_log (kdo/kdy/jaký subjekt) pro
doložitelnost.
- Retence per status (např.
lost lead anonymizace po X
měsících).
- Pozor: náš výmaz nesmaže, co už prošlo přes
OpenRouter/Gemini → proto na prod vypnutý trénink/logging na OpenRouteru
+ jen relevantní data (§9.5).
4. Produktový katalog — obsah
Ceny ověřené z webu 2026-06-27. Před nasazením znovu
ověřit. Detail v memory seba-produktove-portfolio.
Cenová pásma dotazníku synchronizovat s tímto ceníkem (§5.3).
| Produkt |
Segment |
Varianty / ceny |
| Mentoring — konzultace 90 min |
individual_high |
7 990 Kč |
| Mentoring — 1 měsíc (5×75 min) |
individual_high |
44 990 Kč |
| Mentoring — 2 měsíce (8×75 min) |
individual_high |
67 990 Kč |
| Firemní workshop |
company |
34 990 / 49 990 / 59 990 / 79 990 Kč (custom quote,
price_from) |
| Zbraně vlivu |
company |
od 34 990 Kč (custom quote) |
| Otevřený workshop |
individual_low |
5 990 Kč / 269 € (2 měnové varianty), termíny
rotují |
| Masterclass říjen (1.10.2026) |
individual_low |
Standard 9 990 / Premium 15 990 Kč |
| Masterclass listopad (12.11.2026) |
individual_low |
Standard 9 990 / Premium 15 990 Kč |
5. Subsystém A — Gamifikovaný
dotazník
5.1 Účel a princip
Před vstupem do chatu posbírat profil zábavnou, motivující formou.
Marketér dodal funkční kód
(AI-clon/dotaznik-web/index.html, self-contained vanilla
JS) — rozvíjíme ho, ne stavíme od nuly. Gap-analýza + plán úprav: §5.5.
Brutální gamifikace — u každého pole proč to chceme
vědět + jak pomůže, prokládat tipy/infografikou, ukazovat
co získají. Telefon volitelný; povinné jen jméno + e-mail.
Dvojí účel dotazníku (klíčové): (1)
háček „nastav si AI klona na míru” — zero bariér,
gamifikace, příjemné vyplňování; (2) hlavní zdroj dat
pro matching/chat (bloky A+B) ZÁROVEŇ market research pro zářijový
online kurz. Těžiště dat je v dotazníku (ne v chatu) — většina lidí chat
nezapne nebo skončí po 1 zprávě, takže vyplněný dotazník = jediná jistá
data. Každá otázka má dvojí výnos (matching signál +
research datapoint). Vše rámováno jako „aby tě klon pochopil” →
pravdivost dat plyne z motivace mít lepšího klona, ne z donucení.
5.2 Výstup a identita
(přepracováno — bezpečnost)
- Dotazník NEzapisuje z browseru přes anon klíč.
POSTuje na serverový endpoint platformy (service_role),
který: validuje, rate-limituje (anti-spam + Turnstile/hCaptcha), ověří
souhlas, zapíše
user_profiles + consents.
- Identita přes účet (§10b — nahrazuje samotný handoff
token): dotazník vytvoří lehký účet (Supabase Auth) a pošle
magic link na e-mail. Vstup do chatu rovnou (bez
tření); magic link je záloha pro návrat. V chatu měkká výzva nastavit
heslo. Session = httpOnly cookie z magic-link/hesla.
user_id NIKDY není v URL (IDOR/krádež
identity).
- Magic link / token jednorázový, TTL omezený, single-use,
neuhodnutelný.
5.3 Otázky a cenová pásma
Otázky (V1 radio / V2 text, plná parita — viz §5.5): jméno, role,
buyer_role (NOVÁ), zájem, situace, spouštěč
„proč teď” (NOVÁ), cíl, zkusil, historie, cena, e-mail+GDPR,
telefon. Detailní mapování otázka→matching→research v §5.5.
buyer_role (NOVÁ, P0): „Řešíš to sám
za sebe, nebo i za tým?” → radio
self/team_lead/org_decision. Řídí
B2B/B2C větev matchingu (§6.5.2). Mapuje na
user_profiles.buyer_role.
- Spouštěč „proč teď” (NOVÁ): „Co tě k tomu přivádí
zrovna teď?” → naléhavost pro warm/hot (§6.4.5) + nejcennější marketing
datapoint (trigger events pro načasování kampaní kurzu).
- Cenové pásmo — ROZHODNUTO (zůstává, dvojí role):
- Price research pro budoucí online kurz (§1.0) —
cenová citlivost publika pro pricing kurzu. Ukládá se vždy (i do
raw_answers), nezávisle na matchingu.
- Vedlejší signál pro matching (slabý tie-breaker,
§6.5.3) — rozhoduje jen mezi cenově odlišnými rovnocennými
kandidáty.
- NENÍ source of truth. Pásmo jen
přispívá, nikdy nevylučuje dražší
produkt (§6.5 — signál, ne strop).
- Přemapovaná pásma (rozhodnuto 2026-06-30, korekce hranice
2026-07-01 auditem) — kopírují portfolio + jemnější rozlišení
dole kvůli pricingu kurzu:
do_3k „Do 3 000 Kč” (očekávaná hladina online kurzu —
klíčové pro pricing)
3_10k „3 000 – 10 000 Kč”
(workshop/konzultace/masterclass)
10_40k „10 000 – 40 000 Kč” (prémiové/hybridní pásmo,
mezikrok k mentoringu)
40k_plus „40 000 Kč a víc / firemní rozpočet”
(mentoring 44–68k padne CELÝ sem)
nevim „Nevím / nechci teď řešit” (úniková volba —
snižuje drop-off na citlivé otázce; audit 2026-07)
- Horní hranice 40k, NE 45k (audit-korekce):
mentoring začíná na 44 990 → hranice musí být POD ním, jinak se produkt
rozpůlí přes dvě pásma. Zachovat rámec „hodnota řešení ≠ rozpočet”
(framing „kdyby se ti to fakt posunulo”, bez „jednou provždy” =
overpromise). Až bude backend, pásma odvozovat z
products
(jeden zdroj pravdy). Nahrazuje beta-pásma do5k/5-15k/15-30k/30k+.
5.4 Napojení z WP
(embed překonán — rozhodnuto §11/1)
Dotazník NENÍ iframe/script embed do WP (překonáno
§11/1, 2026-06-30). Běží jako součást samostatné Next.js appky na
ai.sebacademy.cz; z WordPressu
(sebacademy.cz) vede jen odkaz/tlačítko na
appku. Odpadá cross-origin embed → žádný iframe/CSRF problém z
Elementoru. Marketérův dotaznik-web/index.html (vanilla JS,
self-contained) se portuje do appky (vrstva 4
ultraplanu), ne embeduje do WP.
CORS /api/profile — K OVĚŘENÍ ve vrstvě 1 (změna
proti dřívějšímu „allow-list jen sebacademy.cz”): když dotazník
i endpoint běží na stejné subdoméně
ai.sebacademy.cz (jedna appka), je to
same-origin → cross-origin CORS allow-list není
potřeba. Allow-list sebacademy.cz by měl smysl jen kdyby
dotazník volal API z jiné domény (WP embed). Model potvrdit při stavbě
endpointu (§9.3, api-contracts §Pravidla). Anti-bot (Turnstile/hCaptcha)
+ rate-limit platí nezávisle na CORS.
5.5
Marketérův kód — gap-analýza, plán úprav, research vrstva (NOVÉ
2026-06-30)
Marketér dodal funkční AI-clon/dotaznik-web/index.html
(1492 ř.): dva <form> (V1 #quiz radio,
V2 #quizV2 text), přepínač verzí, otázky řízené
data-* atributy (rozšiřitelné), boční „Profil tvého AI
klona” s živým %, personalizace + 5. pád. Handoff =
buildPrompt() (1. osoba) → sendData() na
webhook → launchKlon() redirect s promptem v URL.
Silné NEMĚNIT: brand UI, gamifikace, % ukazatel,
mikrotexty „proč to chceme vědět”, personalizace, hybrid radio+reveal
text, dvě verze (rozšiřujeme).
Mezery vs. potřeby (bloky A+B + spec):
| # |
Mezera |
Důsledek |
Zdroj |
| C-1 |
Chybí buyer_role |
Matching B2B/B2C větev nefunguje |
§6.5.2 |
| C-2 |
Chybí spouštěč „proč teď” |
Žádná naléhavost (warm/hot) + chybí marketing trigger |
§6.4.5 |
| C-3 |
Handoff = prompt v URL |
Křehké, profil se neukládá server-side, žádná session |
§5.2 |
| C-4 |
Sběr až na konci |
Kdo odpadne v půlce = data ztracena |
§12 drop-off |
| C-5 |
Žádný GDPR souhlas |
Blokuje ostré spuštění |
§9.7 |
| C-6 |
Cenová pásma nesedí |
Beta-pásma neodpovídají portfoliu/kurzu |
§5.3 |
| C-7 |
V2 zahodila historii+cenu |
Slabší kvalifikace ve V2 |
§5.3 |
Plán úprav (pořadí pro ultraplan): 1.
Frontend (hned, bez backendu): přidat
buyer_role + spouštěč do V1 i V2; dofázovat historii+cenu
do V2 (parita); přemapovat cenová pásma (§5.3); GDPR souhlasy
podle účelu (viz níže — NE „pokračováním souhlasíš”); přidat
nové otázky do PROFILE_FIELDS (živé skládání klona).
✅ FÁZE B HOTOVÁ (2026-07-01) — frontend úpravy hotové po
3-modelovém auditu. Před editací proběhl deep research audit
dotazníku (Claude+Codex+Gemini) → research-dotaznik-audit-2026-07.md.
Audit potvrdil kostru (nepřepisovat) a našel věcný problém
navíc: otázka zajem míchala pain a outcome + chyběl moment
porady a tréma (přemapováno na čisté pain pointy). Hotovo v
dotaznik-web/index.html: (A) přemapovaný
zajem, nová spoustec „proč teď”, přemapovaná
cenová pásma (do_3k/3_10k/10_40k/40k+/nevím — horní
hranice 40k, ne 45k, ať mentoring padne celý do nejvyššího pásma),
zostřený cil, GDPR (info o AI bez
checkboxu + oddělený NEpředzaškrtnutý marketingový checkbox →
answers.consent_contact), microcopy diskrétnosti nad
e-mailem, jméno předvyplněné z intra, změkčený telefon; (B) nová
buyer_role, nová format/diskrétnost, doladěný
role, historie + „jen sleduju obsah”.
C2.4 parita vyřešena třetí cestou: signálové otázky
(role/buyer_role/spoustec/historie/cena/format) = radio v OBOU verzích,
lišit jen otevřené (situace/cil/zkusil/zajem) → parita I izolace jedné
proměnné. Přidán A/B split (náhodný 50/50 + ruční
override) + tracking (localStorage/console, připraveno
na FORM_ENDPOINT). Engine: PROFILE_FIELDS,
generateLead, buildPrompt,
captureAnswer napojeny. Ověřeno v prohlížeči (obě verze
projdou, GDPR default NEzaškrtnutý). Backend
(FORM_ENDPOINT/KLON_URL) = vrstva 1
ultraplanu, zatím prázdné.
📋 Finální znění všech otázek (V1/V2, GDPR texty,
split/tracking stav, co čeká na backend) = dotaznik-verze-B-2026-07.md
— zdroj pravdy vytažený z kódu, podklad pro ultraplan. A/B split
+ tracking: frontend hotový (náhodný 50/50 + ruční override +
lokální tracking), ale admin vynucená verze/poměr + síťový sběr
statistik + vyhodnocení = ultraplan (backend) —
pickVersion() a track() jsou na to připravené,
aktivují se s FORM_ENDPOINT.
GDPR souhlasy — model podle účelu (council fix, ověřeno
ÚOOÚ/EDPB 2026-06-30): „pokračováním souhlasíte” je pro GDPR
souhlas neplatné — souhlas musí být oddělený,
aktivní, jednoznačný projev vůle (ÚOOÚ: implicitní souhlas skrytý v
textu ani nečinnost neplatí; předzaškrtnuté pole zakázáno — Planet49).
Řešení rozlišuje právní titul podle účelu:
| Účel |
Právní titul |
UI na contact screen |
ai_processing_google (klon zpracuje text přes
Gemini/OpenRouter) |
Plnění služby (čl. 6/1/b) — bez toho klon
nefunguje, NENÍ souhlas |
Jen informace + odkaz na zásady: „Tvé zprávy
zpracovává AI (Google Gemini přes OpenRouter).” Žádný checkbox →
klon funguje bez tření. |
qualification_contact (Petr osloví s nabídkou) |
Souhlas (čl. 6/1/a) — oslovení není nutné pro
funkci |
Aktivní NEpředzaškrtnutý checkbox, oddělený,
volitelný: „Souhlasím, aby mě SEBA kontaktovala s nabídkou na
míru.” |
marketing (newsletter) |
Souhlas |
Aktivní checkbox, volitelný, oddělený. |
- Checkbox je jen u dvou volitelných účelů (kontakt,
marketing) — ne podmínka vstupu do chatu. Klon i sběr research dat běží
bez nich (
ai_processing = nutnost služby; research data
agregovaně z raw_answers).
- Cold lead vzniká JEN při
qualification_contact.granted=true (§6.9.2). Bez
něj profil + research, žádný kontaktovatelný lead.
- Povinný checkbox pro
ai_processing by byl CHYBA (ÚOOÚ:
povinný checkbox pro zpracování nutné ke službě nesplňuje svobodnost).
Proto je to informace, ne souhlas.
- Zapsat do
consents (§3.7) granulárně per účel +
consent_text_snapshot. Sladí C1.3 (TODO) i §9.4.
- Backend (vrstva 1 ultraplanu): průběžné ukládání po
každém kroku (POST patch →
/api/profile, draft podle
session id, upsert podle e-mailu) — řeší C-4; server-side handoff
(FORM_ENDPOINT=/api/profile, KLON_URL=/chat
BEZ promptu v URL) — řeší C-3; session cookie / magic link (§5.2).
buildPrompt() zůstává jako copy-paste fallback.
Market research vrstva (marketing kurzu): dotazník =
první systematický zdroj dat o cílovce (žádná persona/ICP zatím
neexistuje). Každý datapoint slouží i analytice:
| Datapoint |
Výstup pro kurz |
role distribuce |
kdo je cílovka → cílení reklam |
buyer_role poměr |
B2B vs B2C trh → produktová strategie |
situace+zajem (text) |
pain points vlastními slovy → headliny, copy, jazyk
LP |
spoustec distribuce |
trigger events → načasování + messaging
kampaní |
zkusil (text) |
konkurence → vymezení + kde inzerovat |
cena distribuce |
cenová citlivost → pricing + segmentace |
Research data → raw_answers (immutable, §3.1), nezávisle
na matchingu. A/B test V1 (radio, strukturované) vs V2 (text,
pravdivější jazyk) přímo měří, co dá kvalitnější research. 4
dimenze (pain/spouštěč/konkurence/cena) jsou rozšiřitelné.
⚠️ PŘEDBĚŽNÝ market research HOTOVÝ (2026-06-30, fáze A před
úpravou dotazníku). Než dotazník nasbírá vlastní data, máme
externí research cílovky zářijového kurzu — slouží jako podklad pro
formulace otázek jazykem cílovky a pro spouštěčovou otázku na reálných
triggerech: -
research-cilovka-kurzu-2026-06.md — deep
research přes 3 modely (Claude+Codex+Gemini) + syntéza. Persony, pain
points, spouštěče, konkurence, cena, kanály. Má sekci „PRO
DOTAZNÍK” s autentickými českými formulacemi pain
points/spouštěčů použitelnými 1:1 jako znění otázek/odpovědí. -
research-keywords-marketing-miner-2026-06.md
— keyword/SEO/audience data (Marketing Miner oficiální MCP + scraping
SEBA + FB Ads). Klíčové pro dotazník: DVĚ ROVINY JAZYKA
— pain fráze („jak se prosadit” 10/měs) se NEgooglí, ale cílovka jimi
mluví → do dotazníku patří emoční jazyk (rezonuje), do SEO vyhledávané
termíny (asertivita 6200). introvert (4500) má
vztahový/definiční záměr → pro dotazník jako self-identifikace OK, pro
SEO ne. - Surové výstupy 3 modelů: research-raw-2026-06/.
Brand jazyk SEBA: memory seba-brand-jazyk-web.
6. Subsystém C+E — AI
chat + orchestrace stavu
6.1 Co je z
legacy ověřené (a co se z toho přebírá)
Legacy test ověřil funkční jádro chatu: model
gemini-3.1-flash-lite přes OpenRouter (s
caching + reasoning), RAG na documents (208 chunků),
kontextová logika nabízení (zralost), banner. Přebíráme z
toho moduly chat.ts + embeddings.ts +
RAG základ (viz §0b „CO PŘEBÍRÁME”). Samotný chat endpoint a prompt se
přepisují kvůli perzistenci a orchestraci (§6.2–6.6).
System prompt v legacy je hyperspecifický pro mentor case → nahradí se
sales/orchestrační verzí.
6.2 Perzistence
konverzace (NOVÉ — subsystém E)
- Server endpoint po každém tahu (user i model)
zapíše
messages + aktualizuje
conversations.last_message_at. DB =
autorita (frontend přestane být zdroj pravdy).
- Admin pak čte konverzaci z DB, ne z prohlížeče zájemce.
6.3 Načtení profilu (přes
session)
Chat načte profil z user_profiles přes
server-side session (httpOnly cookie z magic-link účtu,
§5.2 — ne handoff token). Profil → system prompt → Petr ví, s kým mluví,
neptá se na známé. Fallback (přímý anonymní vstup bez účtu): anonymní
režim + úvodní disclosure „tvé zprávy zpracovává Google Gemini” (§9.4),
případný merge po vyplnění dotazníku (§6.9.3).
6.4
Strukturované signály + rubrika ZRALOSTI (maturity)
Model produkuje jen prózu. Pro maturity/segment/recommended/lead
potřebujeme strojově čitelný výstup. Tato sekce definuje co
klasifikátor vrací a jak z toho vznikne
maturity — spínač, který spouští nabídku produktu
(§6.5), banner (§6.8), most na Petra (§6.7), vznik leadu (§6.6) a event
maturity_reached (§12). Bez operacionalizace zralosti je
celý lead-gen nereprodukovatelný — proto rubrika níže.
6.4.1 Klasifikátor — co vrací
Druhé levné volání
(gemini-3.1-flash-lite přes OpenRouter, function-calling /
responseSchema) vracející striktní JSON. Mezi spuštěními platí poslední
uložený stav.
Spouštěč klasifikátoru — KONKRÉTNÍ (rozhodnuto 2026-06-30,
council fix): dřívější vágní „levná textová podmínka” byla díra
— při 1 zprávě (= většina uživatelů) by klasifikátor nikdy neproběhl a
warmup klon by nezachytil lead. Pevná pravidla: 1. Vždy po 2.
user zprávě a pak po každé další (do té doby
není dost kontextu na zralost; první zpráva = pozdrav/dotaz). 2.
Okamžitě (mimo pořadí) při lokálním keyword scanu
buy_intent — levný regex/keyword test BĚŽÍ BEZ LLM
po KAŽDÉ zprávě (i té první) na markery koupěchtivosti:
chci, kolik (to) stojí, cena,
kdy (můžu/začín), faktur,
přihlás, koup, jdu do toho,
objedna, zaplat. Hit → vynutí klasifikaci hned
(i po 1. zprávě) → zachytí horký lead v okně nejvyššího zájmu. 3.
Cena vs. hodnota: §9.5 sám říká, že jedno levné volání
je zanedbatelné proti hodnotě obchodu (44–68k mentoring). Šetřit na
teplé konverzaci = špatný trade-off. Klasifikátor proto NEomezovat
kvótou na úkor záchytu leadu; kvótu řešit cache (§6.7) a tím, že běží od
2. zprávy, ne každou.
Klasifikátor vrací surové signály, NE hotové
maturity ani recommended_slugs.
maturity dopočítá maturityEval() (§6.4.3),
výběr produktů matchProducts() (§6.5).
Klasifikátor nevrací hotové maturity —
vrací surové signály, ze kterých maturity
dopočítá aplikační kód (maturityEval()). Důvod: stejně jako
u matchingu (§6.5) — reprodukovatelnost, laditelnost, testovatelnost.
Práh i váhy jsou konstanty, ne věty v promptu. Výjimka: explicitní
koupěchtivost (buy_intent) funguje jako
override (viz 6.4.3). Klasifikátor nevrací
recommended_slugs — výběr produktů dělá
matchProducts() v kódu (§6.5).
Výstupní schéma (vynucené):
{
"signals": {
"shared_own_situation": true, // popsal VLASTNÍ konkrétní situaci (ne obecný dotaz)
"asked_about_solution": true, // ptá se na řešení/postup, ne jen edukaci/teorii
"engagement_depth": 2, // 0-3: smysluplné výměny na vlastní téma
"emotional_signal": false, // frustrace/naléhavost/aspirace vlastními slovy
"repeated_probing": false, // opakovaně se doptává do hloubky téhož problému
"explicit_interest": true, // zájem o ŘEŠENÍ jako téma (ještě ne "chci koupit")
"buy_intent": false // EXPLICITNÍ koupěchtivost → override + hot lead
},
"segment": "individual_high", // odvozeno z buyer_role (§6.5.2)
"contact_delta": { "buyer_role": "self" } // co se nově zjistilo z konverzace
}
Rozšiřitelnost (ZÁMĚR — 6+1 signálů je odrazový bod, ne
strop): seznam signálů poroste, jak budeme pokrývat víc
use-casů. Proto jsou signály data, ne kód — definované
v konfiguračním seznamu { signal, type, weight }, přes
který maturityEval() iteruje. Přidat signál = (1) přidat
řádek do configu, (2) naučit klasifikátor ho vracet, (3) případně zvážit
váhu per preset. Skóre, práh ani logika se nemění. Nové klíče v JSON
staré konverzace nerozbijí (chybějící signál = default 0/false).
Kandidáti na rozšíření: named_concrete_event (jmenoval
konkrétní termín „za 14 dní prezentuju představenstvu”),
compared_options (ptá se na rozdíl mezi produkty),
budget_disclosed, objection_raised (řeší
§6.5.5).
Error-handling klasifikátoru (řeší D5): klasifikátor
je LLM → může vrátit nevalidní JSON, chybět klíč, nebo dát hodnotu mimo
rozsah. Pravidla odolnosti: - Vynucené schéma
(function-calling / responseSchema) sníží riziko; přesto vždy
validovat výstup proti očekávanému tvaru (typy, enum
hodnoty segment). - Nevalidní/parse error →
fallback na předchozí state (poslední platná
klasifikace zůstává) + retry jednou. Klasifikátor běží
jen občas (§6.4.1), takže výpadek jednoho běhu = jen odložená
re-klasifikace, ne chyba pro uživatele. - Chybějící
signál = default (0/false) — neblokuje (viz rozšiřitelnost
výše). - segment mimo enum → zahodit
(segment_guess zůstane předchozí), logovat. - Halucinace
produktu nehrozí — klasifikátor produkty nevrací, výběr
dělá matchProducts() v kódu s validací proti katalogu (§6.5
Příloha 6.5-A). Toto je hlavní pojistka D5. - Nikdy nespadnout
celý tah kvůli klasifikátoru — chat odpověď (hlavní model) jde
uživateli i bez úspěšné klasifikace; signály se doženou příště.
6.4.2 Stav ve
conversations.state
Stav se ukládá do conversations.state (jsonb),
neodvozuje z ořezané historie (10 zpráv, po nich model ztrácí začátek).
Tvar:
{
"maturity": {
"reached": false, // výsledek rubriky (score >= práh ∨ buy_intent)
"score": 4, // poslední spočtené skóre (ladění/telemetrie)
"offered": false, // padla už konkrétní nabídka produktu?
"offered_at": null,
"declined": false, // uživatel odmítl/odložil po nabídce?
"reminded": false, // proběhlo už jedno jemné připomenutí po cooldownu?
"last_classified_at": "..." // kdy klasifikátor naposled běžel
},
"segment_guess": "individual_high",
"offered_products": [], // slug[] — co už klon nabídl (i pro matching §6.5)
"known_fields": ["buyer_role", "situation"]
}
Názvosloví segment vs segment_guess
(sjednoceno, řeší D7): klasifikátor (§6.4.1) vrací pole
segment = jeho aktuální odhad pro tento
běh. Ten se ukládá do conversations.state.segment_guess =
uložený (poslední) odhad napříč konverzací. Jde o totéž
věcně, jen _guess zdůrazňuje, že je to odhad LLM, ne
potvrzená pravda (matching/banner pracují s „potvrzeným” segmentem až po
doptání). API kontrakt i state používají tento vztah konzistentně.
6.4.3 Rubrika zralosti —
skórování (P0-1)
Princip: ne LLM odhad „je to zralé?“, ale
deterministické skóre nad signály + práh dle presetu. Hybrid: silný
explicitní signál práh přebije.
score = Σ ( weight[preset][signal] × value[signal] ) // iterace přes config, ne if-strom
maturity_reached = (score >= THRESHOLD[preset]) ∨ buy_intent // override gate
Váhy a práh — preset warmup (zdarma klon,
STŘEDNÍ práh — víc prodává):
| Signál |
Hodnota |
Váha (warmup) |
shared_own_situation |
bool (0/1) |
3 |
asked_about_solution |
bool (0/1) |
3 |
engagement_depth |
int 0–3 |
1 × stupeň |
emotional_signal |
bool (0/1) |
2 |
repeated_probing |
bool (0/1) |
2 |
explicit_interest |
bool (0/1) |
2 |
THRESHOLD[warmup] = 5. Význam: zralost
nastane zhruba při „vlastní situace (3) + jeden další signál (2)” nebo
„ptá se na řešení (3) + 2 výměny (2)“. = střední poloha
(rozhodnuto): nabídne po pár výměnách + vlastní situaci, nečeká na
explicitní zájem. Maximalizuje záchyt leadů u zdarma nástroje, override
jistí horké.
THRESHOLD[premium] = 8 (budoucí
prémiový klon uvnitř placeného kurzu — viz pozn. níže): vyžaduje vlastní
situaci i zájem o řešení i hloubku →
trpělivý, nedotěrný. Mění se jen tato konstanta + váhy, ne
logika.
- Override:
buy_intent=true →
maturity_reached=true bez ohledu na skóre. Chytí horký
lead, co řekne „chci to / kdy začínáme / pošlete fakturu” po 2 zprávách.
Override zároveň nastaví temperature=hot (6.4.5).
- Guard: maturity se přepočítá jen po běhu
klasifikátoru (levná podmínka §6.4.1); jinak platí
state.maturity.reached.
Dva klony (rozhodnuto 2026-06-29): stavíme nejdřív
warmup klon — bude zdarma, lead-gen ke
kurzu, proto víc prodává (warmup preset,
práh 5). Budoucí prémiový klon poběží uvnitř placeného
kurzu, bude trpělivější (premium preset, práh 8). Preset =
sada konstant (váhy + práh), ne větev v kódu.
maturityEval() je identická pro oba.
Po nabídce, kterou uživatel odmítl/odložil („teď ne”, „rozmyslím
si”), se nabídka utlumí — klon se vrátí k edukaci
(fázové chování §6.7: closing → zpět do edukační fáze). Druhá nabídka
smí padnout maximálně jednou a jen při novém silném
signálu.
canOffer =
maturity_reached ∧ ¬offered // ještě nenabídnuto → první nabídka
∨ ( declined ∧ ¬reminded ∧ newStrongSignal ) // cooldown prošel + nový impuls → 1 připomenutí
newStrongSignal = buy_intent ∨ posun situace (nový shared_own_situation k jinému tématu)
∨ explicitní návrat k nabídce ("ještě k tomu mentoringu...")
- První nabídka:
offered=true, offered_at=now.
- Odmítnutí: klasifikátor zachytí →
declined=true. Banner
i textová nabídka se stáhnou.
- Připomenutí (max 1): při
newStrongSignal → jemně, jeden
next step (§6.7 closing = stručně), reminded=true. Pak už
klon nikdy aktivně nenabízí (jen edukuje), lead zůstává v CRM.
- Lead nezaniká: odmítnutí ≠ ztracený zájem (§6.6) —
warm lead zůstává pro Petra bez ohledu na cooldown.
6.4.5 Teplota leadu (hot / warm /
cold)
leads.temperature určuje rubrika podle kvality zájmu
(pro priorizaci Petra):
temperature |
Podmínka |
Význam pro Petra |
hot |
buy_intent=true (explicitní „chci / kdy / faktura / jak
zaplatit”) |
Volat první. Chce koupit. |
warm |
maturity_reached=true ∧ offered=true ∧
¬buy_intent |
Zralý zájem o téma, nabídka padla, ještě neřekl „chci”.
Nurturovat. |
cold |
vyplněný dotazník bez (zralé) konverzace |
Lead z dotazníku (§6.9.2) — má profil, kontaktovatelný, ale
studený. |
Pozor na dvojí „warm” (oprava nekonzistence P3):
leads.status='warm' (procesní stav v CRM pipeline:
new→warm→contacted→won/lost) je ortogonální k
leads.temperature='warm' (kvalita zájmu). Status = „kde
lead je v pipeline”, temperature = „jak je horký”. Mohou se lišit
(status=contacted + temperature=hot). V adminu i kódu rozlišovat názvem
pole, nikdy nezaměňovat.
6.4.6 Few-shot
příklady (kalibrace klasifikátoru)
Kalibrační sada pro klasifikátor (symetrická s worked příklady
matchingu §6.5.4). Slouží jako few-shot v promptu klasifikátoru + jako
test fixtury pro maturityEval().
ZRALÁ (maturity_reached=true): 1. „Já na
poradách prostě ztichnu, i když mám co říct. Minulý týden mi zase
přebrali nápad. Jak se to dá natrénovat?“* →
shared_own_situation, asked_about_solution,
emotional_signal. Skóre 3+3+2=8 ≥ 5 →
zralé (warm, nabídnout). 2. (po 3 výměnách o
trémě) „A tohle funguje i na velký publikum? Já mluvím na
konferencích.” → engagement_depth=3,
shared_own_situation, explicit_interest. Skóre
3+3+2=8 → zralé. 3. „Zní to dobře. Co to stojí a
jak se přihlásím?“* → buy_intent=true → override,
hot lead (i kdyby skóre bylo nízké).
NEZRALÁ (maturity_reached=false — nenabízet): 4.
*„Co je to aktivní naslouchání?“* → jen
asked_about_solution (obecná edukace, žádná vlastní
situace). Skóre 3 < 5 → nezralé (edukovat dál,
budovat autoritu). 5. „Dík, to dává smysl 👍” → žádný signál.
Skóre 0 → nezralé. 6. „A Petr dělá i workshopy pro
firmy?” (bez vlastního kontextu, jen zvědavost) →
explicit_interest samotný. Skóre 2 < 5 →
nezralé, ale doptat se na kontext (organicky zjistit
buyer_role/situaci — §6.5.2). Pozn.: kdyby pak přidal „my
máme v týmu problém s prezentacemi”, skočí
shared_own_situation → zralé.
6.4.7 Provázání s ostatními
subsystémy
maturity_reached=true je spínač: - Matching
(§6.5): odblokuje guard matchProducts()
(if (!state.maturity) return null). Maturity předá
matchingu: segment_guess, known_fields,
offered_products. - Lead (§6.6):
maturity ∧ offered → vždy warm lead (i bez „chci”).
buy_intent → hot. - Banner (§6.8):
zobrazit až při maturity, dle potvrzeného segmentu. -
Most na Petra (§6.7): „při zralosti” =
maturity_reached. - Event (§12):
maturity_reached se loguje jednou při prvním přechodu
false→true; product_offered při
offered=true.
Příloha
6.4-A — Specifikace maturityEval() (pro ultraplan)
Čistá funkce, symetrická s matchProducts()
(Příloha 6.5-A):
maturityEval(signals, state, preset) -> { reached, score, temperature, canOffer }
// config: signály jako DATA (rozšiřitelné — viz 6.4.1)
WEIGHTS = {
warmup: { shared_own_situation:3, asked_about_solution:3, engagement_depth:1,
emotional_signal:2, repeated_probing:2, explicit_interest:2 },
premium: { /* přísnější — doladí tým až vznikne 2. klon (TBD-by-design) */ }
}
THRESHOLD = { warmup: 5, premium: 8 }
function maturityEval(signals, state, preset='warmup') {
score = Σ over WEIGHTS[preset]: weight × (signals[signal] ?? 0) // chybějící = 0
reached = (score >= THRESHOLD[preset]) || signals.buy_intent // override gate
temperature = signals.buy_intent ? 'hot'
: (reached && state.maturity.offered) ? 'warm'
: 'cold'
canOffer = (reached && !state.maturity.offered)
|| (state.maturity.declined && !state.maturity.reminded && newStrongSignal(signals, state))
return { reached, score, temperature, canOffer }
}
Pravidla: - Volá se po běhu klasifikátoru; výsledek
zapsat do state.maturity (reached, score,
last_classified_at). - canOffer=true → orchestrace pustí
nabídku (matching §6.5) + nastaví offered=true, offered_at.
- Idempotence: maturity_reached event jen na první přechod
false→true (porovnat s předchozím state). -
Testovatelnost: čistá funkce → unit testy nad 6
fixturami z 6.4.6 (signály → očekávané reached/temperature). Změna
agresivity klonu = změna WEIGHTS/THRESHOLD, ne
logiky. - Preset zdroj: konfigurace klona (env/DB), ne
hardcode — warmup teď, premium až vznikne druhý klon.
6.5 Doporučovač produktů
(matching — rubrika)
Příprava na kurz (§1.0): matching je navržen tak,
aby šel online kurz snadno přidat jako prioritní
produkt (products.priority,
category='course'). Do té doby se nabízejí stávající
produkty. Žádný hardcoded strom, který by přidání kurzu bránil. Pravidlo
„kurz vs. ostatní” viz 6.5.6.
Matching je deterministická aplikační logika (čistá
funkce nad strukturovaným profilem + signály z konverzace), ne
věty v promptu a ne volba LLM. LLM (klasifikátor §6.4) jen
dodává vstupní signály (segment, buyer_role, zralostní
signály); výběr produktu dělá kód matchProducts(). Důvod:
reprodukovatelnost, laditelnost, testovatelnost, žádná halucinace
produktu mimo katalog. Klasifikátor nevrací seznam
produktů — výběr je výhradně v kódu.
Sekce má dvě vrstvy: Vrstva 1 — principy a pravidla
(6.5.1–6.5.6, pro čtení) a Vrstva 2 — skórovací
specifikace (Příloha 6.5-A, pro ultraplan/implementaci: vstupy,
váhy, prahy, pseudokód, validace).
6.5.1 Vstupní signály (A2)
| Signál |
Zdroj |
Pole |
Role v matchingu |
| role / profese |
dotazník |
profession |
Hrubý segment a téma. Manažer/OSVČ/zaměstnanec ≠ stejná
nabídka. |
| zájem / cíl |
dotazník |
comm_goal, desired_outcome |
Téma (prezentace / vyjednávání / brand / prodej) — k formulaci, ne k
výběru produktu (produkty jsou tematicky univerzální). |
| situace |
dotazník + chat |
situation |
Hloubka potřeby. Akutní konkrétní problém → intenzivnější formát
(mentoring). Obecný rozvoj → workshop/masterclass. |
| buyer_role (NOVÉ, A4) |
dotazník C2.1 + chat |
nové pole, viz 6.5.2 |
Rozhoduje B2B vs. B2C větev. Kupuju za sebe / vedu
tým / rozhoduju o vzdělávání ve firmě. |
| historie |
dotazník |
prior_experience (+ returning flag) |
Byl už na akci Petra? → ne znovu vstupní workshop, ale
upsell do hloubky (mentoring/masterclass). |
| cenové pásmo |
dotazník |
perceived_value |
Slabý tie-breaker (signál, ne strop). Rozhoduje jen
mezi jinak rovnocennými kandidáty. NIKDY nesráží skóre mentoringu, když
situace sedí. Druhá role: price research pro kurz (§5.3). |
| maturity |
klasifikátor |
state.maturity |
Brána, ne vstup výběru. Matching se spustí, až
maturity=true (§6.4/BLOK B). |
Pozn.: comm_goal/desired_outcome určují
jak se produkt prodá (jaké benefity zdůraznit), ne
který — celé portfolio řeší komunikační dovednosti
napříč tématy. Výjimka: brand/prodej téma → mírně preferovat masterclass
listopad „Brand, komunikace a prodej” a Zbraně vlivu, když jsou jinak
rovnocenné.
6.5.2 Detekce company segmentu
(A4 — P0)
Problém: boolean has_company nestačí.
Zaměstnanec velké korporace („mám firmu” = ano) ≠ manažer s rozpočtem na
vzdělávání týmu. První je B2C (koupí workshop sám), druhý je B2B zakázka
(35–80k). has_company=true splní oba → špatný matching.
Řešení — nové pole buyer_role (3
hodnoty), nahrazuje has_company jako matching signál:
buyer_role |
Význam |
Segment |
Typická nabídka |
self |
Kupuju za sebe, vlastní rozvoj |
individual_* |
workshop / masterclass / mentoring |
team_lead |
Vedu tým, řeším i dovednosti lidí pod sebou |
company (slabě) |
firemní workshop NEBO mentoring pro sebe — doptat se |
org_decision |
Rozhoduju/spolurozhoduju o vzdělávání ve firmě, mám/seženu
rozpočet |
company |
firemní workshop / Zbraně vlivu |
Jak se získá: (1) dotazník — nová
otázka C2.1 „Řešíš to sám za sebe, nebo i za tým?” → radio sám za
sebe / vedu tým / rozhoduju o vzdělávání ve firmě, mapuje 1:1 na
buyer_role. (2) chat — když dotazník chybí
nebo je signál slabý, klasifikátor doplní z konverzace PŘED doporučením.
Nehádat — doptat se organicky.
Schema dopad (ultraplan): přidat
user_profiles.buyer_role text check (buyer_role in ('self','team_lead','org_decision')).
has_company ponechat (sekundární/legacy), ale
matching řídí buyer_role. Klasifikátorový
segment_guess (§6.4) z buyer_role
odvozovat.
Hraniční případ team_lead: vědomě
nejednoznačný. Default = nabídnout obojí k porovnání
(mentoring 1:1 pro sebe + firemní workshop pro tým), nechat konverzaci
rozhodnout. Nepřiřazovat natvrdo do company.
6.5.3 Rozhodovací rubrika —
skórování (A3)
Ne tvrdý strom, ale skóre na produkt: vstupy → váhy → seřazení →
entry offer → 1–2 produkty.
Krok 1 — segmentová větev (buyer_role →
kandidáti): - org_decision → company
(firemní workshop, Zbraně vlivu). Entry: bezplatná konzultace k obsahu.
- team_lead → company + individual_high
(firemní workshop a mentoring, porovnání). - self
→ individual_low + individual_high (workshop open,
masterclass, mentoring).
Krok 2 — skóre uvnitř větve (výchozí =
products.priority, modifikátory):
| Signál |
Podmínka |
Efekt |
| situace |
akutní/konkrétní problém, deadline, vysoká frekvence řečnění |
+mentoring, +Premium masterclass (vč. konzultace) |
| situace |
obecný rozvoj „chci se zlepšit” |
+workshop open, +masterclass standard |
| historie |
byl už na akci Petra |
−vstupní workshop, +mentoring/masterclass (upsell, ne opakovat
vstup) |
| historie |
úplný začátečník |
+workshop open / masterclass standard (nízkoprahový první krok) |
| cena (tie-breaker) |
nízké pásmo |
jen mezi rovnocennými: workshop open (5 990) > masterclass (9
990). Nemaže mentoring, dá ho jako aspirační. |
| cena (tie-breaker) |
horní pásmo / firemní |
jen mezi rovnocennými: potvrzuje, že mentoring/firemní je v
komfortní zóně. |
| téma |
brand / prodej / vyjednávání |
jemně +Zbraně vlivu (company), +masterclass listopad (self) |
Pásma = přemapovaný ceník dle §5.3/C2.2 (horní „45
000+ / firemní rozpočet”), ne současná beta-pásma
(do5k/5-15k/15-30k/30k+). Hranice z products (jeden zdroj
pravdy).
Pravidlo ceny — slabý tie-breaker (rozhodnuto):
perceived_value se uplatní POUZE když dva kandidáti mají po
krocích 1–2 ~rovné skóre. Nikdy nesnižuje skóre dražšího produktu pod
levnější, pokud situace dražší produkt podporuje. Aspirační produkt se
vždy aspoň zmíní. Cena se řeší až ve framingu (6.5.5),
ne ve výběru.
Krok 3 — entry offer + počet produktů (A5): -
Vždy aspoň jeden nízkoprahový vstup vedle aspiračního:
konzultace 7 990 (individual_high vstup) / workshop open 5 990
(individual_low) / bezplatná firemní konzultace (company). - 2
produkty (default) = nízkoprahový + aspirační k porovnání.
Když: profil nejednoznačný, team_lead, žádný silný 1:1
signál, cena nejistá. - 1 produkt + přímý CTA jen při
silném signálu a jasném produktu: explicitní „chci 1:1” ∨ jasná akutní
situace + rozpočet sedí ∨ returning s konkrétní poptávkou. Signál „chce
1:1” se získává z konverzace.
Krok 4 — neúplný profil: chybí-li klíčové pole pro
matching (buyer_role — větev, situation —
intenzita) → doplnit organicky v chatu PŘED doporučením, ne hádat. Bez
nich matching nespouštět, i kdyby maturity=true.
VÝJIMKA pro buy_intent (council fix — sladění
bran): když uživatel projeví explicitní
koupěchtivost (buy_intent=true — „chci to / kolik
/ kdy / faktura”), guard NESMÍ blokovat doptáváním. Horký lead, který se
ptá „kolik to stojí”, nesmí dostat „a řešíš to sám, nebo za tým?“. Místo
toho: nabídnout rovnou bezpečný default (workshop open
5 990 jako univerzální nízkoprahový vstup, nebo konzultace 7 990) +
adresovat cenu (§6.5.5) + buyer_role/situation
doplnit AŽ POTÉ, organicky („abych ti řekl přesně to nej, je to spíš pro
tebe, nebo i pro tým?”). Princip: u buy_intent je horší
ztratit moment než netrefit přesný produkt. Tím je override sladěný
napříč maturitou (§6.4.3) i matchingem — obě brány
buy_intent propouští.
6.5.4 Worked příklady
(kalibrace)
- Freelancer, začátečník, nízké pásmo, „zlepšit
prezentace”.
self, začátečník, obecná situace. →
workshop open 5 990 (entry, nejbližší termín) +
masterclass standard 9 990 (aspirační). Mentoring
zmínit jen jako „když budeš chtít do hloubky 1:1”. 2 produkty.
- Manažer, rozhoduje o vzdělávání týmu, 8 lidí, firemní
rozpočet.
org_decision. → firemní
workshop (délka dle hloubky, custom quote) + bezplatná
konzultace k obsahu. Zbraně vlivu jako tematická alternativa. 2
produkty, jeden CTA = konzultace.
- Obchodník, akutně řeší konkrétní vyjednávání, „natrénovat
1:1”, střední pásmo.
self, akutní+frekventní,
explicitní 1:1. → mentoring 1měsíční 44 990 (1 produkt
+ CTA). Cena nad pásmem → framing ROI/cena za sezení (6.5.5),
neustupovat na workshop. Konzultace 7 990 jako „nezávazný první krok”,
když zaváhá.
- Byl loni na otevřeném workshopu, vrací se, „jít
hlouběji”.
self, returning. →
mentoring nebo masterclass premium
(upsell). NE znovu otevřený workshop. 2 produkty k porovnání.
- Vedoucí menšího týmu, „nevím jestli pro sebe nebo pro
lidi”.
team_lead. → mentoring 1:1
(pro sebe) + firemní workshop (pro tým). Doptat se na
velikost týmu a kdo má rozpočet. 2 produkty.
6.5.5 Handling první ceny v
chatu (A6)
Ceny mentoringu (44–68k) a firemních (35–80k) nejsou na
webu → chat je první místo s číslem. Špatný timing/framing
odpálí lead.
Kdy uvést: až po vybudování hodnoty
(nejdřív co řešení obnáší → pak cena jako vyústění). Když se zeptá hned
(„kolik to stojí?“): neodmítat, krátce rámovat — „Záleží na formátu,
řeknu ti rozsah. Než hodím číslo — kde teď nejvíc tlačí bota?” → 1–2
signály → cena s kontextem. Nikdy holé „44990”.
Jak zarámovat: ROI / cena za sezení (mentoring
1měsíční = 5 sezení + neomezená podpora → „~9 000 za sezení s podporou
mezi nimi”). Hodnota vs. cena vázaná na konkrétní situaci z konverzace.
Kotva odshora (zmínit dražší první → konzultace 7 990 pak vypadá jako
nízkoprahový vstup).
Reakce na cenový šok: nevycouvat hned na nejlevnější
— nejdřív reframe (cena/sezení, ROI), pak nabídnout
nižší vstup (konzultace 7 990 / workshop 5 990) jako „první krok
nezávazně”. Fakturace/splátky: v katalogu pro to ZATÍM
není pole — klon je nesmí slibovat, dokud Petr
nepotvrdí; max. „na fakturu se domluvíme” → předat na Petra (otevřený
bod k §6.6). Closing fáze = stručně (§6.7): jeden next
step, ne zeď textu.
6.5.6 Placeholder „kurz
vs. ostatní” (A7)
Až vznikne online kurz (category='course'): dostane
priority vyšší než ostatní jen pro profily, kde
dává smysl — NE tlačit všem. - Vhodný profil:
self, začátečník/mírně pokročilý, nízké–střední pásmo, chce
samostudium/flexibilitu, není akutní 1:1 potřeba (= překryv s dnešním
„workshop open / masterclass standard”). - Nevhodný (priorita
kurzu NEpřebíjí):
org_decision/team_lead (chtějí živé školení),
akutní 1:1 (mentoring), returning chtějící hloubku. - Implementace:
priority = výchozí skóre v Kroku 2, ale segmentová
větev (Krok 1) ho ohraničí — kurz se nedostane do company
větve, nemůže přebít firemní workshop. Tím je „kurz se přidá bez přepisu
stromu” splněno bezpečně.
Ostatní matching signály v §3.4/§6.4/§6.6:
recommended_product_ids → leads (§3.4); urgence
spots_left jen pravdivě a jen u termínových produktů, NIKDY
evergreen mentoring/firemní; ceny/místa z
product_variants/product_terms (aktuální, ne
natvrdo v promptu). Handling námitek viz 6.5.5 + §6.6.
Příloha 6.5-A
— Skórovací specifikace (pro ultraplan)
Vstup:
matchProducts(profile, state, catalog) -> { primary, secondary?, entryOffer, count, reasoning }
- profile: { buyer_role, profession, situation, comm_goal,
desired_outcome, prior_experience, perceived_value, has_company } -
state: { maturity, segment_guess, offered_products[],
known_fields[] } (z conversations.state, §6.4) - catalog:
products(active=true) + variants + terms (live z DB — nikdy natvrdo v
promptu)
Guard:
if (!state.maturity) return null // brána §6.4
// VÝJIMKA buy_intent (council fix): horký lead se neblokuje doptáváním
if (signals.buy_intent)
return { primary: SAFE_DEFAULT, entryOffer: SAFE_DEFAULT, count: 1, askAfter: missingFields }
// SAFE_DEFAULT = workshop open 5990 / konzultace 7990; buyer_role+situation doplnit AŽ POTÉ (§6.5.3 Krok 4)
if (missing(profile.buyer_role) || missing(profile.situation))
return { action: 'ask', field: firstMissing } // doplnit v chatu, nehádat
Krok 1 — větev (kandidáti C):
org_decision → C = bySegment('company')
team_lead → C = bySegment('company') ∪ bySegment('individual_high')
self → C = bySegment('individual_low') ∪ bySegment('individual_high')
Krok 2 — skóre s(p) pro p ∈ C:
s(p) = p.priority // kurz = vysoká až vznikne
+ W_situation * fSituation(profile.situation, p)
+ W_history * fHistory(profile.prior_experience, p)
+ W_theme * fTheme(profile.comm_goal, p)
Váhy (laditelné konstanty):
W_situation=3, W_history=2, W_theme=1. Priorita kurzu
řádově +5 jen v individual_low překryvu.
Krok 2b — cena jako slabý tie-breaker:
if (abs(s(a)-s(b)) <= TIE_EPS) // jen rovnocenní kandidáti
preferCheaperWithinBudget(a, b, profile.perceived_value)
// NIKDY s(p) -= cena. Cena nesnižuje skóre, jen rozhoduje remízy. TIE_EPS ~ 1.
// Aspirační (dražší) kandidát zůstává v nabídce vždy → secondary.
Krok 3 — výběr + entry offer:
ranked = sortDesc(C, s); primary = ranked[0]
entryOffer = lowestThresholdEntry(branch) // self→workshop 5990 / konzultace 7990; company→bezplatná konzultace
count = strongSingleSignal(profile, state) ? 1 : 2
secondary = count==2 ? pickComplement(ranked, primary, entryOffer) : null
// pokud primary == aspirační a entryOffer ho nepokrývá → secondary = entryOffer
strongSingleSignal = explicitní „chci 1:1” ∨ (akutní
situace ∧ rozpočet sedí) ∨ (returning ∧ konkrétní poptávka).
Krok 4 — výstup + validace:
recommended_product_ids = [primary.id, secondary?.id].filter(Boolean) // → leads (§3.4)
// validace: každý id ∈ catalog & active (řeší D5 halucinaci slugu)
reasoning = humanExplain(...) // pro sales_briefing + ladění
Framování ceny (6.5.5) se aplikuje až ve formulaci
odpovědi (prompt dostane vybraný produkt + variants + pravidlo „cena v
kontextu hodnoty / cena za sezení”), ne v matchProducts().
Testovatelnost: čistá funkce → unit testy nad 5
fixturami z 6.5.4 (profil → očekávaný primary/secondary); změna vah =
změna konstant, ne logiky.
6.6 Sběr leadu + sales briefing
- Kombinace: organické doptávání v hovoru + krátké
strukturované potvrzení na konci (vybraný produkt + termín; kontakt z
profilu, jen potvrdit „Mám tě jako Honza, honza@x — sedí?“).
- Warm leady (NOVÉ): pokud
maturity=true
a padla nabídka, VŽDY založit lead (status warm) i bez
explicitního „chci” — zavřené okno ≠ ztracený zájem.
sales_briefing (ne profil): nabídnutý
produkt + reakce (zájem/váhání/námitka), konkrétní námitka, teplota,
citát zájemce vlastními slovy, deadline/urgence, doporučený next step.
Generovat jako „předávací briefing pro obchodníka”. Minimalizace (§9.4):
shrnovat jen obchodní kontext, nespekulovat o osobních
charakteristikách; Petr může opravit/smazat.
6.7
Hluboké strukturované odpovědi — PRINCIP OVĚŘEN V LEGACY TESTU
(2026-06-28)
Cíl: pryč od strohých „5 řádků basic” (kde si uživatel řekne „tohle
mi ChatGPT řekne líp”) → NotebookLM-style hluboké, strukturované,
vizualizované odpovědi s reálnou přidanou hodnotou. Princip
ověřen naživo v legacy testu a podložen researchem (viz níže).
V přestavbě se znovupostaví nad přebíranými moduly
(chat.ts/knowledge.ts) s novým,
sales/orchestrace-aware promptem — technika (celá KB + caching +
reasoning + kostra) zůstává, obsah promptu je nový.
Klíčový poznatek z researche: Problém mělkých
odpovědí NEBYL retrieval, ale prompting + málo kontextu. Celá KB má jen
~49k tokenů = 5 % okna Gemini. „Lost in the middle” při 49k neexistuje
(ověřeno Google studií arXiv 2511.05850). Gemini je defaultně stručný —
hloubku nutno vynutit.
Architektura (ověřená technika, přenáší se do
přestavby): - Vše přes OpenRouter
(/chat/completions, OpenAI-kompatibilní) — sjednoceno s
embeddingy, jeden klíč, nabité kredity. NE přímý Google (free tier nemá
caching, limit=0). - Model
gemini-3.1-flash-lite — novější generace,
nastavitelné reasoning, levný (0,25 $/1M vstup). POZN:
gemini-3.5-flash je prémiový (1,50 $/9 $ = 5× dráž),
nepoužívat pro warmup chat. (Toto je jediný platný chat
model napříč specem — §0b/§6.1 dřív omylem uváděly 3.5-flash jako
primární, opraveno v0.7.) - CELÁ KB v kontextu (ne
top-5 chunků) — src/lib/knowledge.ts načte všech 208 chunků
z documents, pošle do system promptu. Model odpovídá nad
celkem, ne nad fragmenty. - Implicit caching — KB +
system prompt jdou s cache_control: ephemeral → OpenRouter
cachuje statický prefix (90% sleva). Ověřeno: cache HIT ~75k tokenů,
~$0,011/zpráva. - Reasoning —
reasoning: {effort: "medium"} → model „přemýšlí” před
odpovědí (~800 reasoning tokenů). Konfigurovatelné přes
REASONING_EFFORT.
Struktura odpovědi (kostra v
src/lib/prompt.ts): Pro věcné dotazy z oboru:
úvod (háček) → 3–6 bodů s vysvětlením a příkladem → mermaid
diagram (když dává smysl) → shrnutí → konkrétní úkol → most na Petra
(při zralosti). Verbosity HIGH + měřitelná minima per sekce.
Kostra na KONCI promptu (Gemini brzké instrukce v dlouhém promptu
dropuje). Few-shot příklad hloubky.
Mermaid diagramy — model generuje
```mermaid bloky, frontend (page.tsx +
mermaid.js z CDN) je vykresluje jako SVG. Ověřeno naživo (vyjednávací
flowchart s Petrovými pojmy). Pravidla syntaxe v promptu (uvozovky u
textu se závorkami, jen -->, ID bez mezer).
Fázové chování (pravidlo v promptu): - Edukační
fáze: bohaté, strukturované, s příklady a diagramem (buduje autoritu). -
Closing fáze (po nabídce, při námitce): stručně, jeden next
step — zeď textu oddaluje rozhodnutí. - Mimo téma / pozdrav /
krátká upřesňující otázka: přirozeně a stručně, BEZ kostry.
Soubory (legacy zdroje k přebrání):
src/lib/chat.ts (OpenRouter completions + caching +
reasoning) a knowledge.ts (celá KB) se
přebírají (§0b); prompt.ts (kostra) se
přepisuje pro sales/orchestraci. Config zůstává:
CHAT_MODEL, REASONING_EFFORT,
MAX_OUTPUT_TOKENS.
Budoucí rozšíření (Honzova vize „druhý mozek”): až
KB poroste, zvážit hierarchický summary (RAPTOR-style) jako navigační
mapu nad KB + RAG na detaily. Při 49k zatím netřeba — celá KB se vejde.
Cache na prod přes placený tier (garantovaná, ne jen implicit).
6.8 Banner (přepracováno)
Zobrazit až při maturity (ne od začátku
— stálý prodejní banner sráží důvěru mentora), měnit jen podle
potvrzeného segmentu, sladit s tím co Petr právě
nabídl. Pozor na dvě konkurenční CTA (banner + nabídka v textu). A/B
testovat.
6.9
Životní cyklus konverzace, re-entry, lead bez maturity, merge (NOVÉ —
řeší D2/D3/D4)
6.9.1 Stavový model
konverzace + re-entry (D2 / P0-5)
conversations.status (NOVÉ, schema): active
(běží) / dormant (pauza) / closed (ukončeno).
Trigger přechodů: - active → dormant: poslední zpráva
starší než TTL (návrh 7 dní; konfigurovatelné). Nastaví
batch/cron nebo lazy při dalším čtení. - dormant → active:
uživatel se vrátí (viz níže). - * → closed: lead
won/lost, nebo GDPR výmaz, nebo uživatel
explicitně ukončí.
Re-entry (ROZHODNUTO 2026-06-30): navázat na poslední
konverzaci. - Při vstupu do chatu (přes session) najít
poslední active/dormant
konverzaci profilu (index idx_conversations_user_status).
Pokud existuje → pokračovat v ní (dormant → active), ne
zakládat novou. - Klon zná kontext — system prompt
dostane profil + posledních N zpráv + state
(maturity/segment/offered_products zůstávají). Přivítá zpět („Minule
jsme řešili…“). - Maturity se NEresetuje, ale
re-klasifikuje při dalším tahu (signály se přepočtou; pokud mezitím
„vychladl”, skóre to zachytí).
offered/declined/reminded
zůstávají → cooldown (§6.4.4) platí napříč návštěvami. - Jedna
konverzace = jeden lead pro Petra → čistá data, žádné
duplicity. Pokud výjimečně vznikne víc konverzací na profil, „ta
správná” pro briefing = poslední s nejvyšší
state.maturity.score.
6.9.2 Lead bez
maturity — cold lead z dotazníku (D3 / P1)
Problém: kdo si jen povídá a nedozraje (nebo chat vůbec nezapne) →
bez maturity nevznikne lead, i když dal kontakt. Ale dotazník
dal plný profil (§5). Spor s tab-petr slibem „každý zájemce má
profil/je kontaktovatelný”.
Pravidlo: každý dokončený dotazník SE SOUHLASEM =
cold lead (council fix — GDPR). - Cold lead vzniká
jen když
consents[qualification_contact].granted=true
(souhlas s kontaktováním za účelem kvalifikace/nabídky). Bez něj se
uloží profil + research data (na základě
ai_processing_google souhlasu, který je nutný pro funkci
klona), ale žádný kontaktovatelný lead — Petr nesmí
kontaktovat člověka bez právního titulu. Research data
(raw_answers) slouží jen agregované analýze cílovky, ne
adresnému oslovení. - Při server-side zápisu profilu (§5.2,
POST /api/profile na dokončení) vzniká (při souhlasu)
lead se status='new',
temperature='cold', source='questionnaire',
recommended_product_ids=NULL (matching ještě neproběhl —
chybí konverzace). - Cold lead nemá
sales_briefing (není o čem — nebyla konverzace),
jen profil + kontakt. Petr ho vidí v adminu jako „vyplnil dotazník,
ještě nekonverzoval”. - Když pak konverzace dozraje → stejný
lead se povýší (cold → warm/hot, doplní se
briefing + produkty), nezakládá se druhý. Vazba přes
user_id. - Partial dotazník (odpadl v
půlce): díky průběžnému ukládání (§5.5 C-4) existuje
profil-draft, ale lead nevzniká dokud není dotazník
dokončen (jinak by Petr dostal nekompletní leady). Draft je jen pro
retenci kontaktu + drop-off metriku.
6.9.3 Anon → identita merge (D4 /
P1)
Scénář: přijdu anonymně do chatu (bez dotazníku —
conversations.user_id=NULL), povídám si, pak vyplním
dotazník → jak spojit anon konverzaci s novým profilem?
Mechanismus: - Anon konverzace běží na
session id (httpOnly cookie, bez user_id).
state se plní normálně. - Když uživatel v průběhu/po chatu
vyplní dotazník (nebo nastaví účet) → server zná session
id (cookie) i nový
user_profiles.id. - Merge:
UPDATE conversations SET user_id = <profile> WHERE id = <anon session conv> AND user_id IS NULL.
Konverzace se „adoptuje” pod profil, historie zpráv zůstává (FK
messages.conversation_id se nemění). -
Kolize (profil už má jinou konverzaci): platí re-entry
pravidlo 6.9.1 — necháme obě, „ta správná” = nejvyšší maturity. Anon
konverzaci nepřepisujeme, jen připojíme. - Pozor
(bezpečnost): merge jen pro konverzaci z téže
session (cookie), nikdy podle hádaného id → zabrání adopci cizí
konverzace (IDOR). user_id nikdy z URL/body (§5.2).
7. Subsystém D — Admin pro
Petra
7.1 Funkce (v1)
Seznam leadů (filtr status, řazení dle data/teploty), detail = profil
+ konverzace + sales briefing + nabídnutý/vybraný
produkt + kontakt, změna statusu, správa katalogu (produkty, varianty,
termíny + volná místa).
7.2 Autentizace
(přepracováno — blocker, ne TBD)
- Supabase Auth (magic link) s povoleným účtem Petra
NEBO basic auth za reverse proxy + 2FA. Vždy rate-limit + HTTPS.
- Kontrola role na serveru u každého
/admin a /api/admin (ne jen skrytí UI).
- service_role jen v admin-only server modulech, nikdy do
klientského bundlu.
- Audit log přístupů k detailu leadu (kdo/kdy).
- Bez vyřešené auth NENASAZOVAT admin nad reálná
data.
8. Klíčové datové toky
8.1 Happy path (s dotazníkem)
- Web → gamifikovaný dotazník (WP).
- Dotazník → POST na server endpoint → zápis profilu
+ consent → lehký účet (Supabase Auth) + session cookie
→ redirect na
/chat (BEZ tokenu/user_id v URL). Magic link
na e-mail jako záloha pro návrat.
- Chat čte profil ze session → konverzace
přizpůsobená profilu (perzistovaná po tazích).
- Klasifikátor průběžně →
maturity → aplikační matching →
1–2 produkty kontextově.
- Zájemce projeví zájem (nebo konverzace dozraje bez „chci”) → zápis
leads (new/warm) + sales
briefing.
- (volitelně, vědomě) notifikace Petrovi → admin → kontaktuje.
8.2 Fallback (bez dotazníku)
Anonymní konverzace (user_id NULL), úvodní disclosure o
Gemini. Při maturity stejný matching bez profilu (méně
přesné). Lead z anon konverzace: leads.user_id NULL nebo
lazy profil.
9. Bezpečnost, GDPR a
nasazování
9.1
Nasazování (od 2026-07-01 — „nic ven / local only” ZRUŠENO)
Dřívější pravidlo „při dev/testu nic neopustí Mac kromě Gemini” je
překonané. Jedeme normální pipeline: Docker jen
lokální test → push na main → migrace do Supabase Cloud EU
+ deploy appky na server. Co zůstává jako opatrnost (ne blok):
(a) osobní data subjektů jdou do Gemini přes OpenRouter
— na prod nastavit vypnutý logging/trénink + placené kredity (§D1); (b)
nikdy neodeslat reálný e-mail/webhook subjektům při
testu (notifikace jen Petrovi = vlastník dat). Lokální Supabase
e-maily → Mailpit past (dev). (Memory ai-clon-local-only =
přepsaná na tuhle realitu.)
9.2 Prod kanály (vědomě,
schváleně)
Na prod legitimně vzniká: dotazník → prod Supabase; CRM → notifikace
Petrovi (jen hot leady); chat → Gemini přes OpenRouter. Každý kanál
schválen, žádný potají. Deploy appky/serveru = na výslovný pokyn
Honzy.
9.3 Bezpečnostní jádro (z
review)
- RLS default-deny (§3.6); anon klíč jen čtení katalogu; zápis
osobních dat jen server+service_role.
- Identita: magic-link účet + httpOnly session
cookie,
user_id NIKDY v URL/body (§5.2). Handoff
token v URL zrušen (council fix — Referer/historie leak).
- Admin auth jako blocker (§7.2).
- CORS: dotazník i
/api/profile na téže subdoméně
ai.sebacademy.cz = same-origin →
cross-origin allow-list není potřeba (K OVĚŘENÍ ve vrstvě 1, §5.4).
Anti-bot (Turnstile/hCaptcha) + rate-limit na endpointu dotazníku platí
nezávisle.
9.4 GDPR (ČR/EU)
- Granulární souhlas per účel (
consents,
§3.7), doložitelný (verze, znění, čas). Poučení/souhlas
PŘED odesláním citlivých dat.
- Právní titul per účel (council fix, ověřeno ÚOOÚ — viz
§5.5): AI zpracování (Gemini/OpenRouter) = plnění
služby (čl. 6/1/b, jen informace, ne souhlas — bez něj klon
nefunguje); kontakt s nabídkou = aktivní souhlas
(nepředzaškrtnutý oddělený checkbox, ne „pokračováním souhlasíš”);
marketing = samostatný opt-in. Cold lead jen při kontakt-souhlasu
(§6.9.2).
- Výmaz = anonymizace + log (§3.8). Retenční lhůty
per status.
- Minimalizace (čl. 5/1/c): zvážit neukládat
raw_answers trvale; AI briefing nespekuluje o osobních
charakteristikách.
- I anonymní chat: úvodní disclosure o zpracování Googlem.
9.5 Přenos do Gemini/Google
(KRITICKÉ)
Chat posílá obsah konverzace + profil = osobní údaje na US endpoint.
- Celá AI komunikace jde přes OpenRouter (rozhodnuto —
jeden konzistentní stack, žádné přímé napojení na model mimo
OpenRouter). Osobní data zájemců tedy tečou: appka → OpenRouter →
upstream model (Gemini). OpenRouter i Google jsou příjemci/zpracovatelé
(US) — viz §9.7. - Zákaz trénování na datech (klíčové):
na PRODUKCI vypnout logging a trénink dat. OpenRouter to umožňuje — v
nastavení účtu / hlavičkou požadavku (data-collection off), aby se obsah
konverzací NEpoužíval k tréninku. To je produkční must
(default režim může data logovat). ROZHODNUTO: prod = OpenRouter s
vypnutým tréninkem + placené kredity. - Dev: na testování jen
syntetická data (nikdy reálné údaje lidí), ať na režimu
logování nezáleží. - Náklady (řádový odhad — ověřit aktuální
ceník před prod): Flash modely jsou levné. 1 konverzace ~20–60k
tokenů (vstup dominuje kvůli RAG+historii) → řádově 0,30–1,50
Kč/konverzace. Při 100 konv./měsíc ~30–150 Kč,
při 1000 ~300–1500 Kč/měsíc. Zanedbatelné proti hodnotě
jednoho uzavřeného obchodu. Hlavní nákladový faktor = počet volání na
tah (chat+embedding+klasifikátor) → proto optimalizace v §6.4. Free
vs. placený se v ceně za token téměř neliší — přechod je kvůli GDPR, ne
ceně. - Před ostrým spuštěním (než do appky vstoupí
reální lidé): ověřit, že je na OpenRouteru vypnutý trénink/logging dat,
a přejít na placené kredity. - Nová sekce do privacy policy:
OpenRouter (zprostředkovatel AI volání) +
Google (Gemini) (upstream model) +
Supabase (databáze) jako příjemci/zpracovatelé — účel,
region (US), právní titul (EU-US DPF / SCC), DPA u každého.
9.6 dev→prod parita, secrets a
zálohy
- Stejné RLS i lokálně (parita). Oddělené dev/prod
klíče, rotace, prod secrets přes runtime (ne do image). Ověřit
.dockerignore že .env.local není v image.
- Prod auth flagy:
enable_signup=false,
enable_confirmations=true (dnes dev defaulty = prod mina).
Endpointy přes env, žádné hardcoded hosty v kódu mimo compose.
- Zálohy (Free tarif nemá managed backup — §2.3):
denní
pg_dump cron na NextHostingu → záloha na disk exonu
(rotace 30 dní) + ověření neprázdnosti + alert při selhání + jednou
otestovaný restore. Drží zároveň projekt naživu (anti-uspání). Osobní
data (GDPR) = ztráta bez zálohy je katastrofa, proto povinné už od
ostrého spuštění.
- Dev: lokální Supabase přes CLI (Docker), Git
workflow. Prod: Supabase Cloud EU (Free). Migrace verzované, aplikované
na prod kontrolovaně.
9.7 Procesní
GDPR (doplnit před spuštěním dotazníku)
Definovat správce (SEBA Academy / Petr Vojnar) a
zpracovatele (Google, Supabase). ROPA (čl. 30), aktualizace privacy
policy na sebacademy.cz, posouzení DPIA (čl. 35 —
systematické profilování + matching ji pravděpodobně opodstatňuje).
10. Fáze stavby (vrstvy —
opraveno)
Vrstva 1 NENÍ jen „schéma” — obsahuje bezpečnostní a integrační
základ, jinak se vrstva 2 zadrhne nebo se schéma přepisuje zpětně.
- Datový + bezpečnostní + integrační základ:
- Schéma (§3) vč. variants/terms, consents, soft delete, indexy,
ENUM/CHECK.
- RLS policies (default-deny) + adversariální
test.
- Serverový zápisový endpoint pro profil +
konverzace/leady + jeho kontrakt.
- Magic-link účet (Supabase Auth) + session cookie
(ne handoff token) — viz §5.2, api-contracts §1/§2.
- Events tabulka (funnel metriky §12) — viz
schema.sql.
- Kontrakt dotazník→chat (jaká pole, jak identita)
zafixovat jako rozhraní HNED.
- Naplnit katalog.
- Chat + orchestrace (E) + matching: perzistence
konverzací, klasifikátor (JSON signály), aplikační matching, sběr leadu
(vč. warm), sales briefing, fázové odpovědi. Test: scénáře (profil
reálný z DB, ne mock — kontrakt už existuje z vrstvy 1).
- Admin (D): auth (blocker), leady + briefing, správa
katalogu, audit. Test: CRUD + auth.
- Dotazník (A): gamifikovaný React + embed do WP +
POST přes server endpoint + handoff. Test: vyplnění → profil v DB →
vstup do chatu přes token.
- End-to-end: celý happy path + warm lead + GDPR
výmaz.
Každá vrstva = vlastní implementační plán (writing-plans).
10b.
Identita, účet a limity volání (NOVÉ — abuse ochrana + CX)
Proč: bez limitu si někdo udělá z chatu vlastní
ChatGPT a SEBA platí jeho Gemini tokeny. Limit je nutný. ALE klon je
sales nástroj — limit nesmí useknout legitimní zralou konverzaci (=
nejteplejší lead). Řešení = limit + chytré přepnutí na osobní kontakt.
Detaily k dořešení s týmem; níže rozhodnutý směr + CX nápady.
10b.1 Dva oddělené problémy
(neplést)
- Cost abuse („vlastní ChatGPT”) → řeší limit zpráv
(níže).
- Bot/spam abuse (skripty, DoS, scraping) → řeší
anti-bot (Turnstile/hCaptcha) + rate-limit na server endpointech (§5.4,
§9.3). Limit zpráv na tohle NESTAČÍ.
10b.2
Progressive commitment — identita a účet (Honzův návrh, CX-first)
Postupné zvyšování závazku podle rostoucí hodnoty — nízká bariéra na
vstupu, větší závazek později: 1. Vstup bez tření:
dotazník vyplněn → rovnou do chatu. Žádná bariéra,
člověk hned mluví s Petrem. Na e-mail přijde magic link
jako záloha pro návrat. 2. Měkká výzva v chatu: jakmile
je uvnitř a vidí hodnotu → nenásilná výzva „Nastav si heslo, ať se
sem kdykoli pohodlně vrátíš.” Dobrovolné, motivované pohodlím. 3.
Fallback magic link: když heslo nezadá, návrat jen přes
magic link z mailu (musí ho mít uložený / hledat v historii) = jemné
tření, které motivuje k nastavení hesla.
→ Login z dotazníku NAHRAZUJE křehký handoff token z
v0.2. Identita vázaná na účet (e-mail) řeší tři věci najednou:
(a) bezpečný most dotazník→chat, (b) limit vázaný na
člověka, ne na obejitelnou IP/cookie, (c) personalizaci
profilu napříč zařízeními. - Technicky: Supabase Auth
(magic link, je dnes vypnutý, jen zapnout). Heslo volitelně doplnitelné.
Profil + pokyny pro AI (personalizace „na míru”) vázané na účet →
konverzace se ubírá správným směrem.
10b.3 Limit
zpráv — rozhodnutý směr (čísla doladí tým)
Kombinace měkkého a tvrdého limitu + chytré chování (Honza zvolil
možnosti 1+3): - Měkký práh (návrh ~30 zpráv/den,
doladit): varování, ne useknutí. Chytí náhodné/lehké nadužití. -
Tvrdý strop (vyšší denní/měsíční na účet): proti
cílenému zneužití. - Vrstvení metrik: měkký na
IP+session (chytne anonymní), tvrdý na účet (chytne cílené). Žádná
metoda není neprůstřelná sama — i nedokonalý limit odradí drtivou
většinu. - Chytré přepnutí po dosažení (KLÍČOVÉ pro CX i
konverzi): Petr NEUSEKNE zeď „limit vyčerpán”. Místo toho
přepne: „Hele, dneska jsme toho probrali pořádný
kus — pojďme to dotáhnout naživo, ať ti to ušiju na míru.” +
nabídne kontakt/konzultaci/produkt. Limit se stává konverzním
momentem, ne frustrací. (U zralé konverzace = ideální chvíle na
předání leadu.)
10b.4 CX
nápady a otevřené body (brainstorm pro tým)
- Rozlišit limit pro nepřihlášeného (přísnější —
anonymní, riziko abuse) vs. přihlášeného s profilem
(volnější — known lead, chceme ho nechat mluvit).
- Limit komunikovat dopředu a lidsky (ne až náraz):
např. jemný indikátor / Petrova hláška „ještě pár otázek a pak to
dotáhneme osobně”.
- Reset limitu: denně o půlnoci? Klouzavé okno 24h? (Doladit.)
- Zvýhodnit „warm” leady: kdo dal kontakt / projevil zájem o produkt →
vyšší limit (odměna za závazek).
- Anti-abuse bez poškození UX: captcha jen při podezření (ne na
každého), ne otravovat skutečné zájemce.
- Vše k finální diskuzi s týmem — konkrétní čísla,
mechanika resetu, tone of voice hlášek.
11.
Otevřené otázky (tvrdé brány před příslušnou vrstvou)
Stav 2026-06-30: všechny technické otázky ROZHODNUTY
(před spuštěním ultraplanu). Zbývá jen doladit čísla limitů s týmem
(mechaniku ultraplan postaví) a externí milník (kurz, čeká na
Petra).
Rozhodnuté 2026-06-30
Embed dotazníku — VYŘEŠENO:
odkaz/tlačítko z WP na samostatnou appku
ai.sebacademy.cz (NE iframe/script). Odpadá CORS/CSRF
problém, appka běží nativně na vlastní subdoméně, plná kontrola UX.
(Nahrazuje dřívější „iframe vs. script”.)
Admin auth — VYŘEŠENO: Supabase
Auth (magic link), jen Petrův účet + role check na serveru u
každého /admin a /api/admin (§7.2). Stejná
technologie jako identita zájemce — žádný nový mechanismus.
Notifikace Petrovi — VYŘEŠENO:
ano, transakční mail jen u HORKÝCH leadů
(temperature=hot, §6.4.5) — obsahuje odkaz do adminu, NE
celý profil v mailu (minimalizace). U warm/cold ne (záplava). Interní
notifikace správci = GDPR-čisté (Petr je vlastník dat). Kanál:
SMTP/transakční služba ze serveru appky.
Aktualizace cen/termínů — VYŘEŠENO:
Petr v adminu (§7.1) + měsíční cron
připomínka aktualizovat termíny/spots_left před
akcemi (ať klon nelže o dostupnosti).
Prod Supabase / hosting — VYŘEŠENO
(§2.3): appka na NextHosting PRO (Node/Passenger,
subdoména), Supabase Cloud EU, Free tarif + DPA externě
(NextHosting nemá PostgreSQL/pgvector). Free slabiny (uspání + zálohy)
řeší denní pg_dump cron na exonu (záloha + keep-alive).
WP na hlavní doméně, sdílení dat přes REST API. Upgrade na Pro ($25) až
s provozem.
Identita cross-device — VYŘEŠENO
(§5.2/§10b): účet z dotazníku + magic link → heslo. Handoff token v URL
zrušen (council fix).
Duplicitní profily — VYŘEŠENO:
UNIQUE+upsert dle e-mailu (partial unique, §3.1). Pro sdílené/firemní
e-maily ověřit vlastnictví (magic-link klik) před merge anon konverzace.
Lead = 1:1 s user_id, vždy UPSERT.
Cenové pásmo — VYŘEŠENO (§5.3): price
research + signál (slabý tie-breaker), ne strop.
Metriky — VYŘEŠENO (§12): obojí (Clarity
+ CRM eventy, events tabulka).
Zbývá (neblokuje start
ultraplanu)
- Limity volání (§10b) — DEFAULT zapsán, čísla doladit s týmem
před vrstvou 2: výchozí ~20 zpráv/den měkký práh (varování),
~30/den tvrdý (stop milým tónem — „dneonemocní” brand), denní
reset (date_trunc bucket, atomicky triviální — ne klouzavé
okno), nepřihlášený nižší limit než přihlášený. Mechaniku postaví
ultraplan; čísla jsou konstanty, mění se bez přepisu logiky. Vrstvený
limit: account tvrdý → session měkký → IP jen anti-bot (ne tvrdý blok
kvůli NAT/firmám).
- Online kurz Petra Vojnara (EXTERNÍ, čeká na Petra):
kdy bude a jaké parametry → pak do katalogu jako prioritní produkt
(§1.0). Hlavní budoucí milník.
12. Metriky
konverzního trychtýře — ROZHODNUTO: OBOJÍ
„Sales nástroj” se nedá ladit bez měření. Sbírat: dotazníky → chaty →
nabídky → leady → won; drop-off per krok (zejména na potvrzení kontaktu
§6.6); atribuce zdroje (questionnaire vs
direct). Umožní A/B test (banner při zralosti vs. stálý, 1
vs. 2 produkty, value vs. budget framing). - Microsoft
Clarity (už napojený na sebacademy.cz — memory
clarity-sebacademy): chování na webu — heatmapy, nahrávky
dotazníku i chatu, kde lidé odpadají, jak scrollují. Rozšířit Clarity i
na chat appku. - Vlastní eventy v Supabase CRM:
obchodní funnel — přesné napojení na konkrétní leady/produkty/výsledek
(dotazník dokončen, chat zahájen, dosažena zralost, produkt nabídnut,
lead vznikl, status změněn). Vidět v adminu. - Rozdělení rolí: Clarity =
jak se chovají, CRM eventy = obchodní výsledek.
Dohromady kompletní obraz trychtýře (i ke kurzu, až bude).
13. RAG knowledge
— připravit na rozšíření (z review)
documents nemá metadata/filtr (ověřeno: bigserial PK,
match_documents bez filtru). Až přibude víc produktů/online
program, RAG nemá jak filtrovat podle kontextu. Přidat sloupec
metadata/source/product_slug +
filtr do match_documents PŘED ingestem víc obsahu
(re-embedding 1000+ chunků je drahý, projekt na kvótu právě čeká).
14. Shrnutí klíčových
rozhodnutí
- Celý flow jeden spec, stavba po vrstvách; vrstva 1 =
data+bezpečnost+integrace. ✔
- 5 subsystémů (přibyla orchestrace stavu E). ✔
- DB = jediná autorita; perzistence konverzací serverově. ✔
- Identita: magic-link účet + httpOnly session cookie,
user_id NIKDY v URL/body (token v URL zrušen). ✔
- RLS default-deny; zápis osobních dat jen server+service_role; anon
jen čtení katalogu. ✔
- GDPR: granulární souhlas, výmaz=anonymizace, prod =
OpenRouter s vypnutým tréninkem dat + placené kredity,
dev=syntetická data. ✔
- Strukturované signály přes klasifikátor (JSON), matching v aplikační
logice. ✔
- Sales: rozpočet = signál ne strop; entry offer vedle aspiračního;
warm leady; sales briefing; handling námitek; fázové odpovědi. ✔
- Datový model: product_variants (Standard/Premium + měna,
price_minor), termíny bez ceny, CHECK/ENUM, indexy. ✔
- Metriky trychtýře + RAG metadata jako součást rozsahu. ✔
- Dotazník = custom React embed do WP, brutální gamifikace, telefon
volitelný, zápis přes server endpoint. ✔