// ROBOCASH chat message components // Loaded after React + data.js const { useState, useEffect, useRef, useMemo } = React; const RC = window.ROBOCASH; // ─── Brand tokens ─────────────────────────────────────────── const RCT = { green: '#00C48F', greenDeep: '#00A176', greenSoft: '#D7F5E9', greenHover: '#00B380', ink: '#0E1F1B', inkSoft: '#3A4A45', mute: '#6B7975', hint: '#94A6A0', line: 'rgba(14,31,27,0.08)', lineSoft: 'rgba(14,31,27,0.05)', paper: '#FFFFFF', cream: '#F7F4ED', creamDeep: '#EEEADE', navy: '#1B2A3A', coral: '#FF7A59', }; window.RCT = RCT; // ─── Solomiya Avatar ──────────────────────────────────────── function SolomiyaAvatar({ size = 32, online = false, ring = false }) { // Friendly cartoon-portrait placeholder: gradient circle + simple geometric face return (
{/* simple geometric portrait */} {/* hair back */} {/* face */} {/* hair bangs */} {/* eyes */} {/* cheeks */} {/* smile */}
{online && (
)}
); } // ─── Wrappers ─────────────────────────────────────────────── function MsgRow({ side = 'agent', children, withAvatar = true }) { if (side === 'user') { return (
{children}
); } return (
{withAvatar && }
{children}
); } function AgentBubble({ children, first = false, last = true }) { return (
{children}
); } function UserBubble({ children }) { return (
{children}
); } // ─── Typing indicator ─────────────────────────────────────── function Typing() { return (
{[0, 1, 2].map(i => (
))}
); } // ─── Partner strip (shown in welcome) ─────────────────────── function PartnerStrip() { return (
{RC.PARTNERS.slice(0, 5).map((p, i) => (
{p.logoUrl ? ( {p.name} { const box = e.currentTarget.parentElement; e.currentTarget.remove(); if (box) box.textContent = p.monogram; }} /> ) : p.monogram}
))}
+{RC.MFOS.length - 5} партнерів
); } // ─── Slider widget (amount / term) ────────────────────────── function SliderWidget({ label, min, max, step, defaultValue, format, presets, onConfirm, confirmLabel = 'Далі', accent = RCT.green }) { const [v, setV] = useState(defaultValue); const pct = ((v - min) / (max - min)) * 100; return (
{label}
{format(v)}
setV(Number(e.target.value))} style={{ position: 'relative', width: '100%', height: 28, WebkitAppearance: 'none', appearance: 'none', background: 'transparent', margin: 0, padding: 0, cursor: 'pointer', }} className="rc-range" />
{format(min)}{format(max)}
{presets && (
{presets.map(p => ( ))}
)}
); } // ─── Quick replies ────────────────────────────────────────── function QuickReplies({ options, onPick, columns = 1 }) { const hasPrimary = options.some(opt => opt.primary); return (
{options.map(opt => { const primary = !!opt.primary; return ( ); })}
); } function normalizeLocalPhone(value) { let digits = String(value || '').replace(/\D/g, ''); if (digits.startsWith('380')) digits = digits.slice(3); else if (digits.startsWith('80')) digits = digits.slice(2); else if (digits.startsWith('0')) digits = digits.slice(1); return digits.slice(0, 9); } function formatUAphone(localDigits) { const d = normalizeLocalPhone(localDigits); let s = '+380 '; if (d.length > 0) s += d.slice(0, 2); if (d.length > 2) s += ' ' + d.slice(2, 5); if (d.length > 5) s += ' ' + d.slice(5, 7); if (d.length > 7) s += ' ' + d.slice(7, 9); return s; } // ─── Phone / ИНН input widget ─────────────────────────────── function PhoneWidget({ onConfirm }) { const [phone, setPhone] = useState(''); const [consent, setConsent] = useState(true); const formatted = useMemo(() => formatUAphone(phone), [phone]); const valid = phone.length === 9; return (
Номер телефону
🇺🇦 setPhone(normalizeLocalPhone(e.target.value))} placeholder="+380 __ ___ __ __" inputMode="numeric" autoComplete="tel" style={{ flex: 1, border: 'none', background: 'transparent', fontSize: 16, fontWeight: 600, color: RCT.ink, fontFamily: 'inherit', outline: 'none', letterSpacing: 0.3, }} /> {valid && ( )}
Шифрування TLS 1.3 • Не передаємо третім особам
); } // ─── Processing / progress bar ────────────────────────────── function ProcessingWidget({ onDone }) { const [stage, setStage] = useState(0); const stages = [ 'Аналізую ваш профіль…', 'Перевіряю партнерські МФО…', 'Розраховую % одобрення…', 'Формую список пропозицій…', ]; useEffect(() => { const t1 = setTimeout(() => setStage(1), 700); const t2 = setTimeout(() => setStage(2), 1500); const t3 = setTimeout(() => setStage(3), 2300); const t4 = setTimeout(() => onDone && onDone(), 3300); return () => [t1, t2, t3, t4].forEach(clearTimeout); }, []); const pct = ((stage + 1) / stages.length) * 100; return (
Підбираю кредити
{stages[stage]}
); } // ─── MFO card ─────────────────────────────────────────────── function MFOCard({ mfo, requestedAmount, isTop = false }) { return (
{isTop && (
★ Рекомендуємо
)}
{mfo.logoUrl ? ( {mfo.name} { const box = e.currentTarget.parentElement; e.currentTarget.remove(); if (box) box.textContent = mfo.monogram; }} /> ) : mfo.monogram}
{mfo.name}
{mfo.tag}
Шанс одобрення
{mfo.approval}%
{mfo.perks.slice(0, 3).map(p => (
{p}
))}
); } function Stat({ label, value }) { return (
{label}
{value}
); } // ─── MFO vertical list ───────────────────────────────────── function MFOCarousel({ mfos, requestedAmount }) { return (
{mfos.map((m, i) => ( ))}
); } // ─── Summary chip (collected user answer summary) ─────────── function SummaryChip({ icon, label, value }) { return (
{label}: {value}
); } Object.assign(window, { SolomiyaAvatar, MsgRow, AgentBubble, UserBubble, Typing, PartnerStrip, SliderWidget, QuickReplies, PhoneWidget, ProcessingWidget, MFOCard, MFOCarousel, SummaryChip, });