// SEO GUY Web V2 — écrans étendus (couverture inventaire complet) const { useState: useS, useEffect: useE, useRef: useR } = React; function Sparkline({ data, w = 720, h = 220 }) { const series = Array.isArray(data) ? data.filter((v) => Number.isFinite(v)) : []; if (series.length === 0) return null; const max = Math.max(...series, 1); const pts = series.map((v, i) => { const x = series.length === 1 ? w / 2 : (i / (series.length - 1)) * w; const y = h - (v / max) * (h - 24) - 12; return `${x},${y}`; }).join(" "); return ( ); } function PerformanceScreen({ publishedThisMonth, performance, kpis, traffic }) { const [days, setDays] = useS("30"); const [perfLoading, setPerfLoading] = useS(false); const [perfOverride, setPerfOverride] = useS(null); useE(() => { let cancelled = false; setPerfLoading(true); GuyAPI.performanceOverall(days) .then((body) => { if (!cancelled && body?.performance) setPerfOverride(body.performance); }) .catch(() => { if (!cancelled) setPerfOverride(null); }) .finally(() => { if (!cancelled) setPerfLoading(false); }); return () => { cancelled = true; }; }, [days]); const perf = perfOverride || performance?.data || performance; const kpiData = kpis && !perfOverride ? kpis : mapKpisFromPerformance(perf); const trafficData = traffic && !perfOverride ? traffic : mapTrafficSeries(perf); const top = perf?.topArticles ?? []; const baseline = perf?.baseline; const weekCount = perf?.trafficSeries?.length ?? 0; const weeksLabel = weekCount === 0 ? (perf?.gscConnected ? "Aucune donnée" : "GSC non connectée") : weekCount === 1 ? "1 semaine" : `${weekCount} semaines`; return (
{["7", "30", "90"].map((d) => ( ))}
{perfLoading ? Actualisation… : null}
{kpiData.map((k) => )}

Évolution du trafic SEO

{weeksLabel}
{trafficData.length > 0 ? ( ) : (

{perf?.gscConnected ? "Pas encore de données sur cette période." : "Connectez Google Search Console pour afficher le trafic."}

)}

Impact AVANT / APRÈS SEO GUY

{publishedThisMonth} articles validés ce mois-ci

Avant install

{baseline?.visitsBefore != null ? `${baseline.visitsBefore.toLocaleString("fr-FR")}` : "—"} visites/mois

{baseline?.visitsBefore != null ? "Baseline figée à la connexion GSC" : "Baseline en cours de collecte"}

Aujourd'hui

{baseline?.visitsAfter ?? "—"} visites

Valeur trafic {perf?.trafficValueEur ? `${perf.trafficValueEur} €` : "estimée"}

Top publications performantes

{top.length === 0 ? (

Publiez des articles et connectez la GSC pour voir les performances.

) : (
{top.map((a, i) => (
{i + 1}

{a.title}

{a.revenue ? `CA ${a.revenue} €` : ""}

#{a.position ?? a.pos ?? "—"}position
{a.visits}visites
))}
)}
); } function AssistantHubCard({ icon, title, pill, description, bullets, ctaLabel, ctaPrimary = true, onClick }) { return (
{pill ? {pill.label} : null}

{title}

{description}

); } function AssistantsHubScreen({ onOpen, onStart, segmentsCount = 0, articlesCount = 0, onboardingCompleted = false, interviewCredits = 0, editorialPlanAllowed = false, publicationScheduleAllowed = false, planCode = "", }) { const stats = [ { label: "Segments éditoriaux", value: String(segmentsCount), hint: "Audiences et angles de contenu", icon: "users", }, { label: "Articles dans Guy", value: String(articlesCount), hint: "Titres, brouillons et publiés", icon: "docs", }, { label: "Site configuré", value: onboardingCompleted ? "Oui" : "—", hint: onboardingCompleted ? "Assistant démarrage terminé" : "À lancer en premier", icon: "check", }, ]; return (
Assistant de démarrage} sub="Lancez le parcours guidé pour configurer votre site, vos cibles et votre premier article SEO." />
{stats.map((s) => (

{s.value}

{s.label}

{s.hint}

))}
Le grand tour : décrivez votre activité, choisissez vos pages ou produits prioritaires, vos cibles et vos mots-clés. Guy crée votre premier{" "} segment éditorial et un article modèle. } bullets={[ "Analyse du site + suggestions IA", "Persona, ton, précautions de rédaction", "Génération + relecture du 1er article", ]} ctaLabel={onboardingCompleted ? "Relancer" : "Commencer"} onClick={onStart} />

Astuce : vous pouvez relancer l'assistant à tout moment depuis cette page.

); } function SegmentPersonaCard({ persona, index, onEdit }) { const enriched = (persona.sellingPoints || []).length >= 1 && !!(persona.differentiation || "").trim(); const bullets = (persona.sellingPoints || []).filter(Boolean).slice(0, 3); const fallbackBullets = bullets.length ? bullets : (persona.interests || []).filter(Boolean).slice(0, 3).length ? persona.interests.slice(0, 3) : [ persona.tone ? `Ton : ${persona.tone}` : "Ton éditorial à définir", (persona.preferredProducts || []).length ? `${persona.preferredProducts.length} produit(s) associé(s)` : "Produits à associer au segment", "Parcours guidé en 4 étapes + synthèse IA", ]; return (
{persona.avatarUrl ? ( ) : ( )} {enriched ? (persona.tone || "Enrichi") : "À compléter"}

{persona.name}

{persona.desc}

); } function SegmentAddCard({ atLimit, slotsLeft, onAdd }) { return (
{atLimit ? "Limite atteinte" : `${slotsLeft} slot${slotsLeft > 1 ? "s" : ""} restant${slotsLeft > 1 ? "s" : ""}`}

Créer un segment de plus

Ajoutez une nouvelle cible (autre persona, autre univers produit) à votre stratégie. Idéal pour adresser plusieurs audiences sans diluer le ton.

); } function SegFunnelStepIntro({ icon, title, sub }) { return (

{title}

{sub}

); } function AssistantSegmentScreen({ onBack, personas, targets = [], segmentsLimit = 3, onEditSegment, onAddSegment, }) { const list = personas || PERSONAS; const count = targets?.length ?? list.length; const atLimit = count >= segmentsLimit; const enrichedCount = list.filter( (p) => (p.sellingPoints || []).length >= 1 && (p.differentiation || "").trim(), ).length; const slotsLeft = Math.max(0, segmentsLimit - count); const stats = [ { label: "Segments éditoriaux", value: String(count), hint: "Audiences et angles de contenu", icon: "users", }, { label: "Segments enrichis", value: String(enrichedCount), hint: "Arguments + différenciation renseignés", icon: "star", }, { label: "Limite du plan", value: `${count}/${segmentsLimit}`, hint: atLimit ? "Passez à un plan supérieur" : `${slotsLeft} slot${slotsLeft > 1 ? "s" : ""} disponible${slotsLeft > 1 ? "s" : ""}`, icon: "target", }, ]; return (
Segments & personas} sub="Enrichissez chaque cible en 4 étapes : profil, produits, arguments et différenciation — Guy synthétise une fiche persona prête pour vos articles." />
{stats.map((s) => (

{s.value}

{s.label}

{s.hint}

))}
{list.map((p, i) => ( ))}

Astuce : les arguments vendeurs et la différenciation de chaque segment alimentent automatiquement vos pré-prompts de rédaction.

); } const SEG_FUNNEL_STEP_META = [ { icon: "user", title: "Client idéal", sub: "Profil, contexte de vie et motivations d'achat pour ce segment." }, { icon: "card", title: "Produits préférés", sub: "Sélectionnez dans votre catalogue ou décrivez des familles produits." }, { icon: "star", title: "Arguments vendeurs", sub: "Pourquoi ce segment devrait acheter — réutilisés dans vos pré-prompts articles." }, { icon: "trending", title: "Vs concurrent", sub: "Votre différenciation concrète pour ce segment (service, expertise, promesse…)." }, { icon: "spark", title: "Synthèse IA", sub: "Guy génère une fiche persona enrichie — relisez et ajustez avant enregistrement." }, ]; const SEG_FUNNEL_STEPS = SEG_FUNNEL_STEP_META.map((m) => m.title); const SEG_ANALYSIS_MSGS = [ "Guy croise profil et catalogue…", "Affinage des arguments vendeurs…", "Rédaction de la fiche persona…", ]; function normalizeSegmentDraft(d) { return { name: (d.name || "").trim(), profile: (d.profile || "").trim(), motivations: (d.motivations || "").trim(), selectedProducts: [...(d.selectedProducts || [])].map((s) => s.trim()).filter(Boolean).sort(), productFamilies: (d.productFamilies || "").trim(), sellingPoints: (d.sellingPoints || ["", "", ""]).map((s) => (s || "").trim()), differentiation: (d.differentiation || "").trim(), }; } function segmentDraftHasChanges(current, initial) { return JSON.stringify(normalizeSegmentDraft(current)) !== JSON.stringify(normalizeSegmentDraft(initial)); } function segmentDraftHasMinimalContent(d) { const n = normalizeSegmentDraft(d); return !!( n.name || n.profile || n.motivations || n.selectedProducts.length || n.productFamilies || n.sellingPoints.some((s) => s.length > 0) || n.differentiation ); } function buildTargetFromDraft(draft, existingTarget, existingPersona) { const parsed = existingTarget?.audience ? cfgParseAudience(existingTarget.audience) : { name: "", description: "", interests: [] }; const name = draft.name.trim() || parsed.name || existingPersona?.name || ""; const description = draft.profile.trim() || parsed.description || existingPersona?.desc || ""; const interests = (existingPersona?.interests || parsed.interests || []).filter(Boolean); const tone = existingTarget?.tone || existingPersona?.tone || ""; const sellingPoints = draft.sellingPoints.map((s) => s.trim()).filter(Boolean).slice(0, 3); const differentiation = draft.differentiation.trim() || existingTarget?.differentiation || ""; const preferredProducts = draft.selectedProducts.length ? draft.selectedProducts : (existingTarget?.preferredProducts || existingPersona?.preferredProducts || []); const productFamilies = draft.productFamilies.trim() || existingTarget?.productFamilies || ""; let audience = description; if (name && description) audience = `${name} — ${description}`; else if (name) audience = name; else if (existingTarget?.audience) audience = existingTarget.audience.split(" · Recherche :")[0].trim(); if (interests.length && audience) { audience = `${audience}${CFG_INTEREST_MARKER}${interests.join(", ")}`; } const target = { audience: audience || existingTarget?.audience || "", tone, avatarUrl: existingTarget?.avatarUrl ?? null, }; const sp = sellingPoints.length ? sellingPoints : (existingTarget?.sellingPoints || []); if (sp.length) target.sellingPoints = sp; if (differentiation) target.differentiation = differentiation; if (preferredProducts.length) target.preferredProducts = preferredProducts; if (productFamilies) target.productFamilies = productFamilies; return target; } function funnelDraftFromTarget(target, persona) { const t = target || persona?.raw; const parsed = t?.audience ? cfgParseAudience(t.audience) : { name: persona?.name || "", description: persona?.desc || "", interests: [] }; const selling = t?.sellingPoints || persona?.sellingPoints || []; return { name: parsed.name || persona?.name || "", profile: parsed.description || persona?.desc || "", motivations: "", selectedProducts: [...(t?.preferredProducts || persona?.preferredProducts || [])], productFamilies: t?.productFamilies || persona?.productFamilies || "", sellingPoints: [selling[0] || "", selling[1] || "", selling[2] || ""], differentiation: t?.differentiation || persona?.differentiation || "", }; } function SegmentFunnelModal({ mode = "create", editIndex = 0, targets = [], personas = [], featuredProducts = [], onClose, onSaved, }) { const existingTarget = mode === "enrich" ? targets[editIndex] : null; const existingPersona = personas[editIndex]; const initialDraft = funnelDraftFromTarget(existingTarget, existingPersona); const initialDraftRef = useR(initialDraft); const [step, setStep] = useS(0); const [draft, setDraft] = useS(initialDraft); const [productQuery, setProductQuery] = useS(""); const [catalogHits, setCatalogHits] = useS([]); const [catalogLoading, setCatalogLoading] = useS(false); const [synthesizing, setSynthesizing] = useS(false); const [msgIdx, setMsgIdx] = useS(0); const [result, setResult] = useS(null); const [review, setReview] = useS(null); const [error, setError] = useS(""); const [saving, setSaving] = useS(false); useE(() => { document.body.style.overflow = "hidden"; return () => { document.body.style.overflow = ""; }; }, []); useE(() => { let interval; if (synthesizing) { interval = window.setInterval( () => setMsgIdx((i) => (i + 1) % SEG_ANALYSIS_MSGS.length), 2600, ); } return () => { if (interval) window.clearInterval(interval); }; }, [synthesizing]); const featuredNames = (featuredProducts || []).map((p) => p.name).filter(Boolean); const productOptions = [...new Set([...featuredNames, ...catalogHits.map((p) => p.title)])]; async function searchCatalog(q) { const needle = (q || "").trim(); if (needle.length < 2) { setCatalogHits([]); return; } setCatalogLoading(true); try { const data = await GuyAPI.shopifyProducts(needle); setCatalogHits(data.products || []); } catch { setCatalogHits([]); } finally { setCatalogLoading(false); } } function patchDraft(patch) { setDraft((d) => ({ ...d, ...patch })); } function toggleProduct(name) { const label = (name || "").trim(); if (!label) return; setDraft((d) => { const has = d.selectedProducts.includes(label); return { ...d, selectedProducts: has ? d.selectedProducts.filter((x) => x !== label) : [...d.selectedProducts, label].slice(0, 25), }; }); } function stepValid(idx) { if (idx < 4) return true; return Boolean((review?.target?.audience || "").trim()); } function applyReviewWithoutAi() { const built = existingTarget?.audience && !segmentDraftHasChanges(draft, initialDraftRef.current) ? { ...existingTarget } : buildTargetFromDraft(draft, existingTarget, existingPersona); const parsed = cfgParseAudience(built.audience || ""); setReview({ target: built, name: parsed.name || draft.name.trim(), description: parsed.description || draft.profile.trim(), tone: built.tone || "", interests: parsed.interests || existingPersona?.interests || [], sellingPoints: built.sellingPoints || [], differentiation: built.differentiation || "", summary: mode === "enrich" && existingTarget?.audience ? "Aucune modification détectée — la fiche existante est conservée." : "Aucune information saisie. Revenez en arrière pour compléter le segment.", skippedAi: true, }); } async function runSynthesis() { setSynthesizing(true); setError(""); try { const data = await GuyAPI.enrichSegment({ mode, editIndex, idealClient: { name: draft.name.trim(), profile: draft.profile.trim(), motivations: draft.motivations.trim(), }, products: { selected: draft.selectedProducts, familiesFreeText: draft.productFamilies.trim(), }, sellingPoints: draft.sellingPoints.map((s) => s.trim()).filter(Boolean), differentiation: draft.differentiation.trim(), interestOptions: PERSONA_INTERESTS, toneOptions: PERSONA_TONES.map((t) => t.label), }); setResult(data); const parsed = cfgParseAudience(data.target?.audience || ""); setReview({ target: data.target, name: parsed.name || data.persona?.name || draft.name, description: parsed.description || data.persona?.description || "", tone: data.target?.tone || data.persona?.tone || "", interests: data.persona?.interests || parsed.interests || [], sellingPoints: data.target?.sellingPoints || [], differentiation: data.target?.differentiation || "", summary: data.summary || "", }); } catch (e) { setError(e.message || "Synthèse impossible."); setStep(3); } finally { setSynthesizing(false); } } async function goNext() { if (step < 3) { setStep((s) => s + 1); return; } if (step === 3) { setError(""); const hasChanges = segmentDraftHasChanges(draft, initialDraftRef.current); if (!hasChanges) { applyReviewWithoutAi(); setStep(4); return; } if (mode === "create" && !segmentDraftHasMinimalContent(draft)) { applyReviewWithoutAi(); setStep(4); return; } setStep(4); await runSynthesis(); } } function goBack() { if (step === 0) { onClose?.(); return; } if (step === 4 && synthesizing) return; setStep((s) => Math.max(0, s - 1)); } async function saveSegment() { if (!review?.target?.audience) return; setSaving(true); setError(""); try { const parsed = cfgParseAudience(review.target.audience); let audience = review.description.trim(); const name = review.name.trim(); if (name && audience) audience = `${name} — ${audience}`; else if (name) audience = name; const interests = (review.interests || []).filter(Boolean); if (interests.length) { audience = `${audience}${CFG_INTEREST_MARKER}${interests.join(", ")}`; } const target = { ...review.target, audience, tone: review.tone || review.target.tone || "", sellingPoints: (review.sellingPoints || []).map((s) => s.trim()).filter(Boolean).slice(0, 3), differentiation: (review.differentiation || "").trim(), preferredProducts: draft.selectedProducts, productFamilies: draft.productFamilies.trim() || undefined, avatarUrl: existingTarget?.avatarUrl ?? review.target.avatarUrl ?? null, }; const next = [...targets]; if (mode === "enrich" && editIndex < next.length) { next[editIndex] = target; } else { next.push(target); } await GuyAPI.saveConfig({ targetAudiences: next }); onSaved?.(); } catch (e) { setError(e.message || "Enregistrement impossible."); } finally { setSaving(false); } } const progressPct = Math.round(((step + 1) / SEG_FUNNEL_STEPS.length) * 100); return (
e.stopPropagation()}>
SEO GUY · Segments

{mode === "enrich" ? "Enrichir un segment" : "Nouveau segment"}

Étape {step + 1} sur {SEG_FUNNEL_STEPS.length} — {SEG_FUNNEL_STEPS[step]}

{progressPct}%
{SEG_FUNNEL_STEPS.map((label, i) => ( {i + 1}. {label} ))}
{error ?

{error}

: null} {step === 0 && (

Décrivez votre client idéal

Profil, contexte de vie et motivations d'achat pour ce segment. Facultatif

patchDraft({ name: e.target.value })} />