// Reusable icons (lucide-style stroke icons)
const Icon = ({name, size=20, stroke=1.7, ...rest}) => {
const s = size;
const sw = stroke;
const props = { width:s, height:s, viewBox:"0 0 24 24", fill:"none", stroke:"currentColor", strokeWidth:sw, strokeLinecap:"round", strokeLinejoin:"round", ...rest };
switch(name){
case "telegram": return ;
case "arrow-right": return ;
case "arrow-down": return ;
case "check": return ;
case "x": return ;
case "zoom-in": return ;
case "plus": return ;
case "play": return ;
case "shuffle": return ;
case "sparkles": return ;
case "file-x": return ;
case "compass": return ;
case "camera": return ;
case "shapes": return ;
case "users": return ;
case "shopping-bag": return ;
case "bag": return ;
case "diamond": return ;
case "book-open": return ;
case "book": return ;
case "palette": return ;
case "instagram": return (
);
default: return null;
}
};
// data.jsx кладёт данные на window; отдельный Babel-файл не видит const из data.jsx — читаем явно.
const {
PERSONAS, PROBLEMS, USE_CASES, MODULES, STEPS, FOR_WHO, TESTIMONIALS, FAQ, TG_POSTS,
FOUNDER_IMG, FOUNDER_HERO_IMG, FOUNDER_VIDEO_DESKTOP, FOUNDER_VIDEO_MOBILE, FOUNDER_VIDEO_POSTER, CONSISTENCY, LIVE_TG_START, LIVE_TG_STUDENTS, HERO_STATS,
STATS_ENDPOINT,
} = window;
// Cross-browser matchMedia listener (Safari 13- doesn't have addEventListener)
const mqlSubscribe = (mql, handler) => {
if (mql.addEventListener) {
mql.addEventListener("change", handler);
return () => mql.removeEventListener("change", handler);
}
mql.addListener(handler);
return () => mql.removeListener(handler);
};
// Persona card (procedural visual)
const PersonaBg = ({hue, seed=0}) => {
// Deterministic offsets from id/hue
const a = (hue + seed*40) % 360;
const b = (hue + 60 + seed*30) % 360;
return (
);
};
/** Count-up on first viewport entry (GPU-friendly). */
const CountUp = ({ to, duration = 1400, decimals = 0, className = "", suffix = "" }) => {
const ref = React.useRef(null);
const [val, setVal] = React.useState(0);
React.useEffect(() => {
const el = ref.current;
if (!el) return;
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const run = () => {
const start = performance.now();
const from = 0;
const tick = (t) => {
const p = Math.min(1, (t - start) / duration);
const eased = 1 - Math.pow(1 - p, 3);
const cur = from + (to - from) * eased;
setVal(decimals ? Number(cur.toFixed(decimals)) : Math.round(cur));
if (p < 1) requestAnimationFrame(tick);
};
if (reduce) { setVal(to); return; }
requestAnimationFrame(tick);
};
const io = new IntersectionObserver((entries) => {
entries.forEach((e) => {
if (e.isIntersecting) { run(); io.disconnect(); }
});
}, { threshold: 0.2 });
io.observe(el);
return () => io.disconnect();
}, [to, duration, decimals]);
return (
{val}{suffix}
);
};
const LiveCounter = ({ start = 0 }) => {
const STAGE_1_DELAY = 20000;
const STAGE_2_DELAY = 30000;
const TOAST_DURATION = 1800; /* было 1200 */
const [display, setDisplay] = React.useState(start);
const [showToast, setShowToast] = React.useState(false);
const realRef = React.useRef(null);
const counterRef = React.useRef(null);
const isVisibleRef = React.useRef(false);
const stageRef = React.useRef(0);
const elapsedRef = React.useRef(0);
const lastTickRef = React.useRef(null);
const tickIntervalRef = React.useRef(null);
const toastTimerRef = React.useRef(null);
const animationPlayedForRef = React.useRef(null);
const showToastAnimation = () => {
if (toastTimerRef.current) clearTimeout(toastTimerRef.current);
setShowToast(false);
requestAnimationFrame(() => {
setShowToast(true);
toastTimerRef.current = setTimeout(() => setShowToast(false), TOAST_DURATION);
});
};
const tick = React.useCallback(() => {
if (!isVisibleRef.current) {
lastTickRef.current = null;
return;
}
const now = Date.now();
if (lastTickRef.current === null) {
lastTickRef.current = now;
return;
}
const delta = now - lastTickRef.current;
lastTickRef.current = now;
elapsedRef.current += delta;
const real = realRef.current;
if (!real) return;
const stage = stageRef.current;
if (stage === 0 && elapsedRef.current >= STAGE_1_DELAY) {
stageRef.current = 1;
elapsedRef.current = 0;
setDisplay(real - 1);
showToastAnimation();
} else if (stage === 1 && elapsedRef.current >= STAGE_2_DELAY) {
stageRef.current = 2;
elapsedRef.current = 0;
setDisplay(real);
showToastAnimation();
if (tickIntervalRef.current) {
clearInterval(tickIntervalRef.current);
tickIntervalRef.current = null;
}
}
}, []);
const startAnimation = React.useCallback(() => {
const real = realRef.current;
if (!real || real < 3) return;
if (animationPlayedForRef.current === real) return;
animationPlayedForRef.current = real;
stageRef.current = 0;
elapsedRef.current = 0;
lastTickRef.current = isVisibleRef.current ? Date.now() : null;
setDisplay(real - 2);
if (tickIntervalRef.current) clearInterval(tickIntervalRef.current);
tickIntervalRef.current = setInterval(tick, 1000);
}, [tick]);
const handleRealUpdate = React.useCallback((newReal) => {
const oldReal = realRef.current;
realRef.current = newReal;
if (oldReal === null) {
if (isVisibleRef.current) startAnimation();
return;
}
if (newReal === oldReal) return;
if (newReal < oldReal) {
setDisplay(newReal);
animationPlayedForRef.current = newReal;
stageRef.current = 2;
if (tickIntervalRef.current) {
clearInterval(tickIntervalRef.current);
tickIntervalRef.current = null;
}
return;
}
animationPlayedForRef.current = null;
if (isVisibleRef.current) startAnimation();
}, [startAnimation]);
React.useEffect(() => {
if (typeof STATS_ENDPOINT === "undefined" || !STATS_ENDPOINT) return;
let cancelled = false;
let pollTimer = null;
const fetchStats = async () => {
try {
const resp = await fetch(STATS_ENDPOINT, { method: "GET" });
if (!resp.ok) throw new Error(`Stats API ${resp.status}`);
const data = await resp.json();
if (cancelled) return;
if (typeof data.subscribers === "number" && data.subscribers > 0) {
handleRealUpdate(data.subscribers);
}
} catch (err) {
console.warn("[LiveCounter] fetch failed:", err.message);
}
};
fetchStats();
pollTimer = setInterval(fetchStats, 120000);
return () => {
cancelled = true;
if (pollTimer) clearInterval(pollTimer);
if (tickIntervalRef.current) clearInterval(tickIntervalRef.current);
if (toastTimerRef.current) clearTimeout(toastTimerRef.current);
};
}, [handleRealUpdate]);
React.useEffect(() => {
const node = counterRef.current;
if (!node) return;
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
isVisibleRef.current = true;
lastTickRef.current = null;
if (realRef.current !== null && animationPlayedForRef.current !== realRef.current) {
startAnimation();
}
} else {
isVisibleRef.current = false;
lastTickRef.current = null;
}
});
},
{ threshold: 0.3 }
);
observer.observe(node);
return () => observer.disconnect();
}, [startAnimation]);
return (
{display.toLocaleString("ru-RU")}
{showToast && (
+1
)}
);
};
const useTilt = () => {
const ref = React.useRef(null);
const onMove = React.useCallback((e) => {
const el = ref.current;
if (!el || (window.matchMedia && !window.matchMedia("(hover: hover)").matches)) return;
const r = el.getBoundingClientRect();
const px = (e.clientX - r.left) / r.width;
const py = (e.clientY - r.top) / r.height;
const rx = (py - 0.5) * -10;
const ry = (px - 0.5) * 12;
el.style.setProperty("--mx", `${px * 100}%`);
el.style.setProperty("--my", `${py * 100}%`);
const inner = el.querySelector(".persona-inner");
if (inner) inner.style.transform = `perspective(820px) rotateX(${rx}deg) rotateY(${ry}deg) translateZ(0)`;
}, []);
const onLeave = React.useCallback(() => {
const el = ref.current;
if (!el) return;
const inner = el.querySelector(".persona-inner");
if (inner) inner.style.transform = "perspective(820px) rotateX(0deg) rotateY(0deg)";
}, []);
return { ref, onMove, onLeave };
};
/** Hero particle field — inspired by Linear-style ambient depth (CSS/canvas, not Three.js). */
const ParticlesCanvas = ({ enabled }) => {
const canvasRef = React.useRef(null);
React.useEffect(() => {
if (!enabled) return;
const canvas = canvasRef.current;
if (!canvas) return;
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (reduce) return;
const ctx = canvas.getContext("2d");
let raf;
let mx = 0, my = 0, tx = 0, ty = 0;
const onMm = (e) => {
const sec = canvas.closest(".hero-wrap");
if (!sec) return;
const b = sec.getBoundingClientRect();
mx = (e.clientX - b.left) / b.width - 0.5;
my = (e.clientY - b.top) / b.height - 0.5;
};
const pts = [];
const resize = () => {
const sec = canvas.closest(".hero-wrap");
if (!sec) return;
const dpr = Math.min(2, window.devicePixelRatio || 1);
const w = sec.clientWidth, h = sec.clientHeight;
canvas.width = w * dpr;
canvas.height = h * dpr;
canvas.style.width = w + "px";
canvas.style.height = h + "px";
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
pts.length = 0;
const n = Math.min(55, Math.floor((w * h) / 9000));
for (let i = 0; i < n; i++) {
pts.push({ x: Math.random() * w, y: Math.random() * h, vx: (Math.random() - 0.5) * 0.15, vy: (Math.random() - 0.5) * 0.15 });
}
};
resize();
window.addEventListener("resize", resize);
window.addEventListener("mousemove", onMm, { passive: true });
let t0 = performance.now();
const loop = (t) => {
const sec = canvas.closest(".hero-wrap");
if (!sec) return;
const w = sec.clientWidth, h = sec.clientHeight;
tx += (mx - tx) * 0.06;
ty += (my - ty) * 0.06;
const driftX = tx * 40, driftY = ty * 36;
ctx.clearRect(0, 0, w, h);
pts.forEach((p) => {
p.x += p.vx + driftX * 0.002;
p.y += p.vy + driftY * 0.002;
if (p.x < 0 || p.x > w) p.vx *= -1;
if (p.y < 0 || p.y > h) p.vy *= -1;
});
const dt = Math.min(40, t - t0);
t0 = t;
ctx.strokeStyle = "rgba(167,139,250,0.12)";
ctx.lineWidth = 1;
for (let i = 0; i < pts.length; i++) {
for (let j = i + 1; j < pts.length; j++) {
const dx = pts[i].x - pts[j].x, dy = pts[i].y - pts[j].y;
const d = Math.hypot(dx, dy);
if (d < 72) {
ctx.globalAlpha = (1 - d / 72) * 0.35;
ctx.beginPath();
ctx.moveTo(pts[i].x, pts[i].y);
ctx.lineTo(pts[j].x, pts[j].y);
ctx.stroke();
}
}
}
ctx.globalAlpha = 0.45;
ctx.fillStyle = "rgba(196,181,253,0.35)";
pts.forEach((p) => {
ctx.beginPath();
ctx.arc(p.x, p.y, 1.1, 0, Math.PI * 2);
ctx.fill();
});
ctx.globalAlpha = 1;
raf = requestAnimationFrame(loop);
};
raf = requestAnimationFrame(loop);
return () => {
cancelAnimationFrame(raf);
window.removeEventListener("resize", resize);
window.removeEventListener("mousemove", onMm);
};
}, [enabled]);
if (!enabled) return null;
return ;
};
const Persona = ({ p, ix, onOpen }) => {
const { ref, onMove, onLeave } = useTilt();
const src = p.img || "";
return (
onOpen && onOpen(p, e.currentTarget)}
role="button"
tabIndex={0}
onKeyDown={(e) => {
if ((e.key === "Enter" || e.key === " ") && onOpen) {
e.preventDefault();
onOpen(p, e.currentTarget);
}
}}
>
{src ? (
) : (
)}
META
Стиль — {p.style}
Среда — {p.env}
Эстетика — {p.aesthetic}
№ {String(p.id).padStart(2, "0")}
{p.tag.split(" / ")[0]}
{p.subtitle}
);
};
// ─── Sections ───────────────────────────────────────────────────────────────
const SectionHead = ({tag, title, subtitle, align="left", children}) => (
{tag}
{title}
{subtitle &&
{subtitle}
}
{children}
);
const ConsistencyCompare = () => {
const L = CONSISTENCY.left;
const R = CONSISTENCY.right;
return (
01 · Хаос
Без системы
Каждая генерация: некачественное лицо
02 · Контроль
С системой
Один персонаж в любых сценах
);
};
const AnimatedProcess = () => {
const [isMobileProcess, setIsMobileProcess] = React.useState(() =>
typeof window !== "undefined" ? window.innerWidth <= 720 : false
);
React.useEffect(() => {
const check = () => setIsMobileProcess(window.innerWidth <= 720);
check();
window.addEventListener("resize", check);
return () => window.removeEventListener("resize", check);
}, []);
if (isMobileProcess) {
return (
01
Идея
Внешность, характер, история персонажа
02
Генерация
Подготовка контента по проверенной стратегии
03
Продвижение
Набор аудитории для ИИ-персонажа
04
Результат
Раскрытие потенциала и масштабирование проекта
);
}
return (
{[
{ cx: 120, label: "Идея", sub: ["Внешность, характер,", "история персонажа"] },
{ cx: 300, label: "Генерация", sub: ["Подготовка контента", "по проверенной стратегии"], core: true },
{ cx: 460, label: "Продвижение", sub: ["Набор аудитории", "для ИИ-персонажа"] },
{ cx: 620, label: "Результат", sub: ["Раскрытие потенциала", "и масштабирование"] },
].map((n, i) => (
{!n.core && }
{n.core && (
<>
>
)}
{!n.core && }
{n.label}
{n.sub[0]}
{n.sub[1]}
))}
);
};
// ScrollVideoHero — Apple-style scroll-driven canvas player.
// Pre-loads frame sequence, redraws canvas based on scroll progress.
// Mobile (pointer:coarse) falls back to autoplay-loop mp4.
const FRAME_COUNT = 120;
const FRAME_PATH = (i) => `frames/frame_${String(i).padStart(4, "0")}.webp`;
const MOBILE_FALLBACK_VIDEO = "mobile_loop.mp4";
const POSTER_IMG = "poster.jpg";
const INITIAL_PRELOAD = 30;
const ScrollVideoHero = ({ onCTA }) => {
const containerRef = React.useRef(null);
const canvasRef = React.useRef(null);
const imagesRef = React.useRef([]);
const currentFrameRef = React.useRef(0);
const rafRef = React.useRef(null);
const [loadProgress, setLoadProgress] = React.useState(0);
const [ready, setReady] = React.useState(false);
const [isMobile, setIsMobile] = React.useState(() =>
typeof window !== "undefined"
&& (window.matchMedia("(pointer: coarse)").matches
|| window.matchMedia("(max-width: 720px)").matches)
);
React.useEffect(() => {
const mqCoarse = window.matchMedia("(pointer: coarse)");
const mqNarrow = window.matchMedia("(max-width: 720px)");
const update = () => setIsMobile(mqCoarse.matches || mqNarrow.matches);
update();
const unsubCoarse = mqlSubscribe(mqCoarse, update);
const unsubNarrow = mqlSubscribe(mqNarrow, update);
window.addEventListener("resize", update);
return () => {
unsubCoarse();
unsubNarrow();
window.removeEventListener("resize", update);
};
}, []);
React.useEffect(() => {
if (isMobile) return;
let cancelled = false;
const imgs = new Array(FRAME_COUNT);
let loadedCount = 0;
let initialLoadedCount = 0;
const loadOne = (i) => new Promise((resolve) => {
const img = new Image();
img.onload = () => {
if (cancelled) return resolve();
imgs[i] = img;
loadedCount++;
if (i < INITIAL_PRELOAD) initialLoadedCount++;
setLoadProgress(Math.round((loadedCount / FRAME_COUNT) * 100));
if (initialLoadedCount >= Math.min(INITIAL_PRELOAD, FRAME_COUNT)) setReady(true);
resolve();
};
img.onerror = () => {
console.warn(`Frame ${i + 1} failed`);
loadedCount++;
if (i < INITIAL_PRELOAD) initialLoadedCount++;
setLoadProgress(Math.round((loadedCount / FRAME_COUNT) * 100));
if (initialLoadedCount >= Math.min(INITIAL_PRELOAD, FRAME_COUNT)) setReady(true);
resolve();
};
img.src = FRAME_PATH(i + 1);
});
const initial = [];
for (let i = 0; i < Math.min(INITIAL_PRELOAD, FRAME_COUNT); i++) {
initial.push(loadOne(i));
}
Promise.all(initial).then(() => {
if (cancelled) return;
const loadBatch = async () => {
for (let i = INITIAL_PRELOAD; i < FRAME_COUNT; i += 10) {
if (cancelled) return;
const batch = [];
for (let j = i; j < Math.min(i + 10, FRAME_COUNT); j++) {
batch.push(loadOne(j));
}
await Promise.all(batch);
}
};
loadBatch();
});
imagesRef.current = imgs;
return () => { cancelled = true; };
}, [isMobile]);
const drawFrame = React.useCallback((idx) => {
const canvas = canvasRef.current;
if (!canvas) return;
const img = imagesRef.current[idx];
if (!img) return;
const ctx = canvas.getContext("2d");
const dpr = Math.min(window.devicePixelRatio || 1, 2);
const cw = canvas.width / dpr;
const ch = canvas.height / dpr;
const iw = img.naturalWidth;
const ih = img.naturalHeight;
const scale = Math.max(cw / iw, ch / ih);
const w = iw * scale;
const h = ih * scale;
const x = (cw - w) / 2;
const y = (ch - h) / 2;
ctx.clearRect(0, 0, cw, ch);
ctx.drawImage(img, x, y, w, h);
}, []);
React.useEffect(() => {
if (!ready || isMobile) return;
drawFrame(0);
}, [ready, isMobile, drawFrame]);
React.useEffect(() => {
if (isMobile) return;
const canvas = canvasRef.current;
if (!canvas) return;
const resize = () => {
const dpr = Math.min(window.devicePixelRatio || 1, 2);
canvas.width = window.innerWidth * dpr;
canvas.height = window.innerHeight * dpr;
canvas.style.width = window.innerWidth + "px";
canvas.style.height = window.innerHeight + "px";
const ctx = canvas.getContext("2d");
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
drawFrame(currentFrameRef.current);
};
resize();
window.addEventListener("resize", resize);
return () => window.removeEventListener("resize", resize);
}, [isMobile, ready, drawFrame]);
React.useEffect(() => {
if (isMobile || !ready) return;
const container = containerRef.current;
if (!container) return;
const onScroll = () => {
if (rafRef.current) return;
rafRef.current = requestAnimationFrame(() => {
rafRef.current = null;
const rect = container.getBoundingClientRect();
const total = container.offsetHeight - window.innerHeight;
const scrolled = Math.max(0, Math.min(1, -rect.top / Math.max(1, total)));
const targetFrame = Math.min(FRAME_COUNT - 1, Math.floor(scrolled * FRAME_COUNT));
if (targetFrame !== currentFrameRef.current) {
currentFrameRef.current = targetFrame;
drawFrame(targetFrame);
}
});
};
window.addEventListener("scroll", onScroll, { passive: true });
onScroll();
return () => {
window.removeEventListener("scroll", onScroll);
if (rafRef.current) cancelAnimationFrame(rafRef.current);
};
}, [ready, isMobile, drawFrame]);
if (isMobile) {
return (
Обучение · Создание ИИ-персонажей
Один ИИ-персонаж.
Одно лицо.
Десятки способов применить навык.
Системная программа по созданию ИИ-персонажей с нуля. Подходит и новичкам, и тем, кто уже пробовал создавать ИИ-блогера, но не получил стабильного результата.
Получить бесплатный урок
Бесплатные материалы → попробуй → решай сам
);
}
return (
{!ready && (
Загрузка кадров… {loadProgress}%
)}
Обучение · Создание ИИ-персонажей
Один ИИ-персонаж.
Одно лицо.
Десятки способов применить навык.
Системная программа по созданию ИИ-персонажей с нуля. Подходит и новичкам, и тем, кто уже пробовал создавать ИИ-блогера, но не получил стабильного результата.
Бесплатные материалы → попробуй → решай сам
);
};
const Hero = ({ variant, onCTA, particlesOn }) => {
const wrapRef = React.useRef(null);
const glowRef = React.useRef(null);
React.useEffect(() => {
const el = wrapRef.current;
const glow = glowRef.current;
if (!el || !glow) return;
const fine = window.matchMedia("(pointer: fine)").matches;
if (!fine) return;
let lx = 50, ly = 40, tx = 50, ty = 40;
const onMove = (e) => {
const r = el.getBoundingClientRect();
tx = ((e.clientX - r.left) / r.width) * 100;
ty = ((e.clientY - r.top) / r.height) * 100;
};
let raf;
const smooth = () => {
lx += (tx - lx) * 0.08;
ly += (ty - ly) * 0.08;
glow.style.setProperty("--hx", `${lx}%`);
glow.style.setProperty("--hy", `${ly}%`);
raf = requestAnimationFrame(smooth);
};
raf = requestAnimationFrame(smooth);
el.addEventListener("mousemove", onMove, { passive: true });
return () => { cancelAnimationFrame(raf); el.removeEventListener("mousemove", onMove); };
}, []);
if (variant === "video-scroll") {
return
;
}
return (
Обучение · Создание ИИ-персонажей
Создавай реалистичных
ИИ-персонажей с нуля.
Без опыта и сложного ПО.
Системная программа: от первого промпта до управляемого персонажа, который сохраняет лицо в десятках сцен. Доступно из любой точки мира — нужен только ноутбук и интернет.
Бесплатные материалы → попробуй → решай сам
{HERO_STATS.students != null && (
+ учеников
)}
{HERO_STATS.personas != null && (
+ персонажей
)}
{HERO_STATS.rating != null && (
★ рейтинг материалов
)}
);
};
const HeroVisual = ({variant}) => {
if (variant === "mesh") return (
CONSISTENCY ENGINE / v1.0
live preview
);
if (variant === "collage") return (
{[
{ n: "01", nm: "Editorial", img: PERSONAS[0]?.img },
{ n: "02", nm: "Urban", img: PERSONAS[1]?.img },
{ n: "03", nm: "Studio", img: PERSONAS[2]?.img },
{ n: "04", nm: "Outdoor", img: PERSONAS[8]?.img },
].map((t, i) => (
{t.img ? (
) : (
)}
№ {t.n}
{t.nm}
))}
);
// default: video
return (
Юлия
основатель программы · приветствие
);
};
// Magnetic CTA — pulls toward cursor on desktop
const MagneticBtn = ({className="", onClick, children, ...rest}) => {
const ref = React.useRef();
React.useEffect(() => {
const el = ref.current; if (!el) return;
if (window.matchMedia && window.matchMedia("(pointer: coarse)").matches) return;
const r = 120;
const handle = (e) => {
const rect = el.getBoundingClientRect();
const cx = rect.left + rect.width/2, cy = rect.top + rect.height/2;
const dx = e.clientX - cx, dy = e.clientY - cy;
const dist = Math.hypot(dx, dy);
if (dist < r + Math.max(rect.width, rect.height)/2) {
const f = Math.max(0, 1 - dist/(r*2));
el.style.transform = `translate(${dx*0.18*f}px, ${dy*0.18*f}px)`;
} else {
el.style.transform = "";
}
};
const reset = () => { el.style.transform = ""; };
window.addEventListener("mousemove", handle);
window.addEventListener("mouseleave", reset);
return () => { window.removeEventListener("mousemove", handle); window.removeEventListener("mouseleave", reset); };
}, []);
return (
{children}
);
};
const Problems = () => (
{PROBLEMS.map((p,i) => (
))}
Знакомо? Это нормально. Большинство застревает именно здесь — пока не выстроится система.
);
const Showcase = ({galleryStyle, onOpen}) => {
const [active, setActive] = React.useState(0);
return (
{galleryStyle === "marquee" ? (
{[...PERSONAS, ...PERSONAS].map((p,i) =>
)}
) : galleryStyle === "spotlight" ? (
{PERSONAS[active].img ? (
) : (
<>
>
)}
{PERSONAS[active].tag}
{PERSONAS[active].subtitle}
{PERSONAS[active].aesthetic}
onOpen(PERSONAS[active], e.currentTarget)} style={{position:"absolute",right:18,bottom:18}}>Открыть
{PERSONAS.slice(0,8).map((p,i) => (
setActive(i)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
setActive(i);
}
}}
role="button"
tabIndex={0}
aria-label={`Выбрать персонажа № ${String(p.id).padStart(2, "0")}, ${p.subtitle}`}
>
{p.img ?
:
}
{p.tag.split(" / ")[0]}
{p.subtitle}
))}
) : (
{PERSONAS.map((p,i) =>
)}
)}
);
};
const UseCases = () => (
{USE_CASES.map((u,i) => (
))}
);
const Modules = () => (
{MODULES.map((m,i) => (
{m.ix}
{m.duration}
{m.format}
))}
Как это устроено
Как создаётся ИИ-персонаж за 4 шага
Каждый этап решает свою задачу. Пропустишь, результат сломается.
);
const FounderVideo = () => {
const videoRef = React.useRef(null);
const [paused, setPaused] = React.useState(true);
const [started, setStarted] = React.useState(false);
const [videoSrc] = React.useState(() => {
if (typeof window === "undefined") return FOUNDER_VIDEO_DESKTOP;
return window.innerWidth <= 720 ? FOUNDER_VIDEO_MOBILE : FOUNDER_VIDEO_DESKTOP;
});
const startPlay = () => {
const v = videoRef.current;
if (!v) return;
v.play().catch(() => {/* autoplay restrictions — ignore */});
setStarted(true);
};
return (
setPaused(false)}
onPause={() => setPaused(true)}
onEnded={() => setPaused(true)}
/>
{paused && !started && (
)}
);
};
const Founder = () => (
практик · эксперт
Автор · MODELCORE
Юлия
Автор
Кто ведёт обучение
«Реалистичный ИИ-блогер — это уже не будущее, а настоящее. Я учу тому, что реально даёт результаты.»
Юлия. Работаю с генеративными нейросетями и созданием цифровых персонажей с мая 2025 года . Сейчас веду 6 ИИ-персонажей для контент-проектов под разные задачи.
Большинство в этой нише работает анонимно. Я веду обучение под своим именем, потому что отвечаю за качество методики. Результат каждого ученика складывается из работы по рекомендациям и собственных усилий.
Программа собрана из того, что работает на практике у меня и учеников. Без воды, без копипасты гайдов из интернета, с фокусом на результат.
);
const Testimonials = () => {
const [openIdx, setOpenIdx] = React.useState(null);
const [showAll, setShowAll] = React.useState(false);
const [isNarrowT, setIsNarrowT] = React.useState(() =>
typeof window !== "undefined" && window.innerWidth <= 720
);
const VISIBLE_COUNT = 6;
const visibleTestimonials = (showAll || isNarrowT) ? TESTIMONIALS : TESTIMONIALS.slice(0, VISIBLE_COUNT);
const triggerRef = React.useRef(null);
const closeRef = React.useRef(null);
const openLightbox = (idx, btn) => {
triggerRef.current = btn;
setOpenIdx(idx);
};
React.useEffect(() => {
const check = () => setIsNarrowT(window.innerWidth <= 720);
check();
window.addEventListener("resize", check);
return () => window.removeEventListener("resize", check);
}, []);
React.useEffect(() => {
if (openIdx === null) {
triggerRef.current?.focus();
return;
}
const t = setTimeout(() => closeRef.current?.focus(), 50);
const onKey = (e) => { if (e.key === "Escape") setOpenIdx(null); };
document.body.style.overflow = "hidden";
window.addEventListener("keydown", onKey);
return () => {
clearTimeout(t);
document.body.style.overflow = "";
window.removeEventListener("keydown", onKey);
};
}, [openIdx]);
const avatarGradient = (name) => {
let h = 0;
for (let i = 0; i < name.length; i++) h = (h * 31 + name.charCodeAt(i)) >>> 0;
const hue1 = h % 360;
const hue2 = (hue1 + 60) % 360;
return `linear-gradient(135deg, hsl(${hue1} 70% 55%) 0%, hsl(${hue2} 65% 45%) 100%)`;
};
return (
Отзывы
Что говорят ученики
Реальные отзывы из закрытого чата учеников про систему и поддержку.
{visibleTestimonials.map((t) => (
{t.name.charAt(0).toUpperCase()}
«{t.quote}»
{
const i = TESTIMONIALS.findIndex((x) => x.id === t.id);
if (i >= 0) openLightbox(i, e.currentTarget);
}}
aria-label={`Открыть скриншот отзыва от ${t.name}`}
>
Открыть скрин
))}
← листай →
{TESTIMONIALS.length > VISIBLE_COUNT && (
setShowAll((s) => !s)}
aria-expanded={showAll}
>
{showAll
? `Свернуть до ${VISIBLE_COUNT}`
: `Показать ещё ${TESTIMONIALS.length - VISIBLE_COUNT} отзыва`}
)}
{openIdx !== null && (
setOpenIdx(null)}
role="dialog"
aria-modal="true"
aria-label="Скриншот отзыва"
>
setOpenIdx(null)}
aria-label="Закрыть скриншот"
>
e.stopPropagation()}
/>
e.stopPropagation()}>
{TESTIMONIALS[openIdx].name}
{TESTIMONIALS[openIdx].role}
)}
);
};
const GetStarted = ({onCTA}) => (
{STEPS.map((s,i) => (
))}
Шаг 01 — открыть Телеграм
);
const ForWho = () => (
Подходит
{FOR_WHO.yes.map((t,i) => {t} )}
Не подходит
{FOR_WHO.no.map((t,i) => {t} )}
);
const TgChannel = ({onCTA}) => (
M
MODELCORE
Закрытый Телеграм-канал · по приглашению
{LIVE_TG_START != null && (
В канале сейчас
)}
{LIVE_TG_START != null ? (
{LIVE_TG_STUDENTS} учеников в закрытом чате
) : (
Растущее сообщество
Бесплатные уроки, разборы и анонсы открытия программы
)}
Бесплатные уроки, разборы домашних, анонсы открытия программы. Без спама — только польза и практика.
Присоединиться к каналу
{TG_POSTS.map((p,i) => (
{p.when}
{p.title}
{p.excerpt}
))}
);
const FaqList = () => {
const [open, setOpen] = React.useState(0);
return (
{FAQ.map((f,i) => (
setOpen(open === i ? -1 : i)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
setOpen(open === i ? -1 : i);
}
}}
>
{f.q}
))}
);
};
const FinalCTA = ({onCTA, tgUrl, formEndpoint}) => {
const [name, setName] = React.useState("");
const [tg, setTg] = React.useState("");
const [status, setStatus] = React.useState(null);
const [errs, setErrs] = React.useState({});
const [submitting, setSubmitting] = React.useState(false);
const submitInFlightRef = React.useRef(false);
const submit = async (e) => {
e.preventDefault();
const newErrs = {};
if (!name.trim() || name.trim().length < 2) newErrs.name = true;
const tgClean = tg.trim().replace(/^https?:\/\/(t\.me\/|telegram\.me\/)/i, "").replace(/^@/, "");
if (!tgClean || !/^[a-z][a-z0-9_]{3,30}[a-z0-9]$/i.test(tgClean)) newErrs.tg = true;
setErrs(newErrs);
if (Object.keys(newErrs).length) {
setStatus({ok:false, text:"Проверь поля — нужно имя (от 2 символов) и ник в Телеграме (от 5 символов, латиница, цифры, _)."});
return;
}
if (submitInFlightRef.current) return;
submitInFlightRef.current = true;
setSubmitting(true);
setStatus({ok:null, text:"Отправляем…"});
try {
const r = await fetch(formEndpoint, {
method: "POST",
headers: {"Content-Type": "application/json", "Accept": "application/json"},
body: JSON.stringify({
name: name.trim(),
telegram: `@${tgClean}`,
source: "modelcoreai-landing",
page: window.location.href,
ts: new Date().toISOString(),
}),
});
if (!r.ok) throw new Error(`HTTP ${r.status}`);
setStatus({ok:true, text:`Заявка принята. Юлия свяжется в Телеграме (@${tgClean}) в ближайшее время.`});
setName("");
setTg("");
} catch (err) {
setStatus({ok:false, text:"Не удалось отправить — попробуй ещё раз или напиши в Телеграм-канал напрямую."});
console.error("Form submit failed:", err);
} finally {
submitInFlightRef.current = false;
setSubmitting(false);
}
};
const msgClass =
status == null
? ""
: status.ok === true
? "ok form-success"
: status.ok === false
? status.text && status.text.includes("Проверь поля")
? "err"
: "err form-error"
: "pending";
return (
Поехали?
Готов создать своего ИИ-персонажа?
Начни с бесплатных материалов в Телеграме. Решение остаётся за тобой, после того как попробуешь.
Открыть Телеграм-канал
Или сразу оставь заявку на полную программу
{status && (
{status.text}
)}
Образовательная программа. Результат зависит от усилий и практики ученика.
);
};
const Footer = ({tgUrl, supportUrl, email}) => (
);
// Lightbox
const Lightbox = ({ persona, onClose, openTriggerRef }) => {
const closeRef = React.useRef(null);
React.useEffect(() => {
if (!persona) {
openTriggerRef?.current?.focus();
return;
}
const t = setTimeout(() => closeRef.current?.focus(), 50);
const onKey = (e) => { if (e.key === "Escape") onClose(); };
document.body.style.overflow = "hidden";
window.addEventListener("keydown", onKey);
return () => {
clearTimeout(t);
document.body.style.overflow = "";
window.removeEventListener("keydown", onKey);
};
}, [persona, onClose, openTriggerRef]);
return (
{persona && (
e.stopPropagation()}>
{persona.img ? (
) : (
<>
>
)}
{persona.tag} · № {String(persona.id).padStart(2,"0")}
{persona.tag} · № {String(persona.id).padStart(2,"0")}
{persona.subtitle}
ENVIRONMENT
{persona.env}
AESTHETIC
{persona.aesthetic}
Все визуалы — SFW, реалистичные портреты, lifestyle, fashion в нейтральной эстетике.
)}
);
};
Object.assign(window, { Icon, Persona, PersonaBg, SectionHead, ConsistencyCompare, AnimatedProcess, CountUp, LiveCounter, ParticlesCanvas, ScrollVideoHero, Hero, HeroVisual, MagneticBtn, Problems, Showcase, UseCases, Modules, Founder, Testimonials, GetStarted, ForWho, TgChannel, FaqList, FinalCTA, Footer, Lightbox });