// SEO GUY — Onboarding 4 étapes (analyse → cibles & plan → GSC → 1er article) const { useState: useOS, useEffect: useOE, useMemo: useOM, useCallback: useOC } = React; const OB_STEPS = [ { id: 1, label: "Analyse" }, { id: 2, label: "Cibles & plan" }, { id: 3, label: "Google" }, { id: 4, label: "1er article" }, ]; const OB_RESUME_LABELS = [ "Analyser votre site", "Définir vos cibles et votre formule", "Connecter Google", "Voir votre premier article", ]; const SEO_GUY_CPV_URL = "https://assistantguy.com/legal/cpv-abonnements/"; const WP_PLUGIN_ZIP_URL = "https://assistantguy.com/downloads/assistant-guy.zip"; const AUTOPILOT_MODE_DESCRIPTION = "SEO GUY rédige, illustre et publie vos articles sur votre site selon votre rythme — sans relecture ni validation préalable de votre part."; const AUTOPILOT_LIABILITY_NOTICE = "En activant le pilotage automatique, vous reconnaissez que la publication s'effectue sans contrôle préalable de votre part. Vous demeurez seul responsable des contenus publiés (exactitude, conformité légale et réglementaire, droits des tiers, image de marque) ainsi que de leurs conséquences commerciales, SEO ou juridiques. EMARKETIS ne saurait être tenue responsable des contenus diffusés en mode automatique, dans les limites autorisées par les Conditions particulières de vente."; const AUTOPILOT_LIABILITY_CHECKBOX_LABEL = "J'accepte que la publication automatique relève de ma seule responsabilité et je décharge EMARKETIS de toute responsabilité au titre des contenus publiés sans relecture préalable, conformément à l'article 8 des Conditions particulières de vente."; function defaultAutopilotSlots(planCode) { if (!planCode || planCode === "discovery") { return [{ weekday: 1, hour: 9, minute: 0 }]; } if (planCode === "starter") { return [1, 2, 3, 4].map((weekday) => ({ weekday, hour: 9, minute: 0 })); } const slots = []; for (const weekday of [1, 2, 3, 4]) { slots.push({ weekday, hour: 12, minute: 0 }); slots.push({ weekday, hour: 18, minute: 0 }); } return slots; } function parseAudienceLabel(audience) { const raw = (audience || "").trim(); if (!raw) return { name: "", description: "" }; const parts = raw.split(/\s*—\s*/).map((p) => p.trim()).filter(Boolean); if (parts.length <= 1) return { name: "", description: parts[0] || "" }; if (parts.length >= 2 && parts[0] === parts[1]) { return { name: parts[0], description: parts.slice(2).join(" — ") || parts[1] }; } return { name: parts[0], description: parts.slice(1).join(" — ") }; } const PERSONA_INTEREST_MARKER = " · Recherche : "; function splitInterests(text) { const raw = (text || "").trim(); const idx = raw.indexOf(PERSONA_INTEREST_MARKER.trim()); if (idx === -1) return { description: raw, interests: [] }; // On coupe sur le marqueur (avec ou sans espaces autour). const markerIdx = raw.indexOf("Recherche :"); const description = raw.slice(0, markerIdx).replace(/[·•|\-\s]+$/, "").trim(); const interests = raw .slice(markerIdx + "Recherche :".length) .split(",") .map((s) => s.trim()) .filter(Boolean); return { description, interests }; } function defaultPersonasFromAudiences(audiences) { const ids = ["p1", "p2", "p3"]; return ids.map((id, i) => { const preset = PERSONA_PRESETS[i % PERSONA_PRESETS.length]; const raw = audiences[i]; if (!raw) { return { id, name: preset.label, description: "", tone: i === 0 ? PERSONA_TONES[0].label : "", interests: [], customPhotoUrl: null, }; } const audienceText = raw.audience || raw.desc || ""; const { description: cleaned, interests } = splitInterests(audienceText); const { name, description } = parseAudienceLabel(cleaned); return { id, name: raw.name || name || preset.label, description: raw.desc || description, tone: raw.tone || PERSONA_TONES[0].label, interests: Array.isArray(raw.interests) ? raw.interests : interests, customPhotoUrl: raw.avatarUrl || null, }; }); } function personasToTargets(personas) { return personas .map((p) => { const name = (p.name || "").trim(); const desc = (p.description || "").trim(); const tone = (p.tone || "").trim(); const interests = Array.isArray(p.interests) ? p.interests.filter(Boolean) : []; let audience = desc; if (name && desc) audience = `${name} — ${desc}`; else if (name) audience = name; if (!audience) return null; if (interests.length) audience = `${audience}${PERSONA_INTEREST_MARKER}${interests.join(", ")}`; const avatarUrl = p.customPhotoUrl?.trim() || null; return { audience, tone, avatarUrl }; }) .filter(Boolean); } // Limite de personas affichables/éditables dans l'assistant. Pour un plan // "illimité" on plafonne l'UI à 6 (au-delà ce n'est pas pertinent en onboarding). const SEGMENTS_UI_CAP = 6; function segmentLimitForPlan(code, plans) { const plan = (plans || []).find((p) => p.code === code); const feat = plan?.raw?.features?.segmentsCount; if (!feat) return 3; if (feat.kind === "limit") return feat.limit === -1 ? SEGMENTS_UI_CAP : Math.max(1, feat.limit); return 3; } const PLAN_FEATURE_LABELS = { articlesPerWeek: "Articles par semaine", interviewsPerPeriod: "Interviews incluses", keywordsCount: "Mots-clés en base", segmentsCount: "Segments éditoriaux", competitorsCount: "Concurrents suivis", googleSearchConsole: "Google Search Console", editorialPlan: "Plan éditorial auto", seoRevenueAttribution: "Attribution CA SEO", proSeoAnalysis: "Analyse SEO Pro", aiKeywordsSuggest: "IA mots-clés", expertInterviews: "Interviews expert", prioritySupport: "Support prioritaire", }; function featureLimitLabel(feat) { if (!feat) return "—"; if (feat.kind === "bool") return feat.enabled ? "Inclus" : "—"; if (feat.limit === -1) return "Illimité"; return String(feat.limit); } function planArticlesLabel(plan) { const f = plan.raw?.features?.articlesPerWeek; if (!f || f.kind !== "limit") return "—"; return f.limit === -1 ? "Illimité" : `${f.limit} / semaine`; } function planInterviewsLabel(plan) { const f = plan.raw?.features?.interviewsPerPeriod; if (!f || f.kind !== "limit" || f.limit === 0) return "À l'unité (5 €)"; if (plan.code === "starter") return `${f.limit} / trimestre`; return `${f.limit} / mois`; } function planPriceLabel(plan, cycle) { const parsed = parseInt(String(plan.price).replace(/\D/g, ""), 10); const monthly = plan.raw?.monthlyPriceEur ?? (Number.isFinite(parsed) ? parsed : 0); const yearly = plan.raw?.yearlyPriceEur; if (monthly === 0) return "0 €"; if (cycle === "yearly" && yearly) return `${Math.round(yearly / 12)} €`; return `${monthly} €`; } function planBillingNote(plan, cycle) { const monthly = plan.raw?.monthlyPriceEur ?? 0; if (monthly === 0) return "Gratuit · sans carte bancaire"; const yearly = plan.raw?.yearlyPriceEur; if (cycle === "yearly" && yearly) return `Facturé ${yearly} € / an via Shopify Billing`; return `Facturé ${monthly} € / mois via Shopify Billing`; } function ObBackBtn({ onBack, disabled }) { return ( ); } function difficultyLabel(score) { if (score == null) return "—"; const n = score <= 1 ? score * 100 : score; if (n <= 33) return "faible"; if (n <= 66) return "moyenne"; return "élevée"; } function OnboardProgress({ step }) { return (
{OB_STEPS.map((s) => (
= s.id ? " is-done" : ""}${step === s.id ? " is-current" : ""}`}> {step > s.id ? : s.id} {s.label}
))}
); } function StepAnalyse({ insights, onConfirm, initialSummary, initialProducts, quotaReached, quota, isWebsite }) { const hasInitialSummary = !!(initialSummary && initialSummary.trim()); const [analyzing, setAnalyzing] = useOS(!hasInitialSummary); const [summary, setSummary] = useOS(initialSummary || ""); const [analysis, setAnalysis] = useOS(null); const [products, setProducts] = useOS(initialProducts || []); const [productsLoading, setProductsLoading] = useOS(!isWebsite); const [saving, setSaving] = useOS(false); const [err, setErr] = useOS(""); const ranRef = React.useRef(false); async function runAnalysis() { setAnalyzing(true); setErr(""); try { const res = await GuyAPI.onboardingSuggestDescription(); if (res?.description) setSummary(res.description); if (res?.analysis) setAnalysis(res.analysis); const pages = res?.pages || []; if (isWebsite && pages.length) { setProducts((prev) => { if (prev.some((p) => (p.name || "").trim() || (p.url || "").trim())) return prev; return pages.slice(0, 8).map((p, i) => ({ name: p.name, url: p.url, selected: i < 4, })); }); } } catch (e) { setErr( e.message || (isWebsite ? "Impossible d'analyser le site. Vous pouvez rédiger le résumé à la main." : "Impossible d'analyser la boutique. Vous pouvez rédiger le résumé à la main."), ); } finally { setAnalyzing(false); if (isWebsite) setProductsLoading(false); } } useOE(() => { if (ranRef.current) return; ranRef.current = true; if (!hasInitialSummary) runAnalysis(); else if (isWebsite) setProductsLoading(false); if (isWebsite) return; if (!(initialProducts && initialProducts.length)) { (async () => { setProductsLoading(true); try { const res = await GuyAPI.shopifyProducts(); const list = (res?.products || []) .filter((p) => p.url) .map((p) => ({ name: p.title, url: p.url, imageUrl: p.imageUrl || "", shopifyProductId: p.id || "", selected: true, })); setProducts(list); } catch { setProducts([]); } finally { setProductsLoading(false); } })(); } else { setProductsLoading(false); } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); function toggleProduct(i) { setProducts((prev) => prev.map((p, idx) => (idx === i ? { ...p, selected: !p.selected } : p))); } function updateManual(i, field, value) { setProducts((prev) => prev.map((p, idx) => (idx === i ? { ...p, [field]: value } : p))); } function removeProduct(i) { setProducts((prev) => prev.filter((_, idx) => idx !== i)); } function addManualProduct() { setProducts((prev) => [...prev, { name: "", url: "", selected: true, manual: true }]); } async function confirm() { setSaving(true); setErr(""); const selected = products .filter((p) => p.selected && (p.name || "").trim() && /^https?:\/\//i.test((p.url || "").trim())) .map((p) => { const row = { name: p.name.trim(), url: p.url.trim() }; if (p.imageUrl && /^https?:\/\//i.test(p.imageUrl)) row.imageUrl = p.imageUrl; if (p.shopifyProductId) row.shopifyProductId = p.shopifyProductId; return row; }); try { await onConfirm({ summary: summary.trim(), products: selected }); } catch (e) { setErr(e.message || "Enregistrement impossible."); } finally { setSaving(false); } } const pageCount = analysis?.pagesFromHome; const catalogCount = (analysis?.productsFromCatalog || analysis?.productsFromHome) ?? insights.productCount; const productCount = isWebsite ? pageCount : catalogCount; const productLabel = isWebsite ? productCount != null ? `${productCount} page${productCount > 1 ? "s" : ""}` : analyzing ? "Site en cours d'analyse" : "Site analysé" : productCount != null ? `${productCount} produits` : "Catalogue analysé"; const kwLabel = `${insights.keywordCount || 0} mots-clés`; const top = insights.topKeyword; const selectedCount = products.filter((p) => p.selected).length; return (
SEO GUY
Bonjour, je suis SEO GUY 👋

{isWebsite ? "J'ai analysé votre site" : "J'ai analysé votre boutique"}

{analyzing ? isWebsite ? "J'inspecte votre site, vos pages et votre activité…" : "J'inspecte votre site, votre catalogue et vos mots-clés…" : "Voici ce que j'ai compris de votre activité. Vérifiez et ajustez si besoin."}

{quotaReached && (
Quota hebdomadaire atteint{quota ? ` (${quota.used}/${quota.limit})` : ""}. {" "}Vous pouvez refaire la configuration ; le premier article ne sera pas généré à la dernière étape.
)}

{productLabel}

{isWebsite ? "repérées sur votre site" : "repérés dans votre catalogue"}

{kwLabel}

dans votre base SEO

{top && (

Meilleure opportunité

« {top.keyword} » —{" "} {top.volume != null ? `${top.volume.toLocaleString("fr-FR")} rech./mois` : "volume à estimer"} · Difficulté {difficultyLabel(top.difficulty)}

)}

Résumé de votre activité (rédigé par SEO GUY)

{!analyzing && ( )}
{analyzing ? (
SEO GUY rédige le résumé de votre {isWebsite ? "activité" : "boutique"}…
) : (