import { useEffect, useState } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { api } from '../api.js';
import InvSelect from '../components/InvSelect.jsx';
import { useAuth } from '../context/AuthContext.jsx';
import ResultBanner from '../components/ResultBanner.jsx';
import CountrySelect, { COUNTRIES, FlagIcon } from '../components/CountrySelect.jsx';
const LOGO_BASE = (import.meta.env.VITE_API_URL || '/api').replace(/\/api$/, '') + '/api/logos/';
const TYPES_INV = {
p2p: 'Prêt P2P',
dette: 'Dette',
equity: 'Capitaux propres',
tokenise: 'Tokenisé',
achat_louer: 'Achat à louer',
mini_obligations:'Mini-obligations',
mixte: 'Mixte',
};
const SECTEURS = {
immobilier: 'Immobilier',
pme: 'PME',
startups: 'Startups',
litige: 'Litige',
energie: 'Énergie verte',
sante_science: 'Santé & Science',
logistique: 'Logistique',
personnel: 'Prêts personnels',
art: 'Art',
autre: 'Autre',
};
const INV_TYPES = {
particulier: 'Particulier',
entreprise: 'Entreprise',
les_deux: 'Particulier & Entreprise',
};
const countryLabel = code => COUNTRIES.find(c => c.code === code)?.name ?? code ?? '—';
// ── Composants helpers ────────────────────────────────────────────────────────
function SectionTitle({ children }) {
return (
{children}
);
}
function KpiCell({ label, value, editing, input }) {
return (
{label}
{editing
? input
:
{value ?? 'N/A'}
}
);
}
function InfoRow({ label, value, editing, input }) {
return (
{label}
{editing
? input
: {value ?? '—'}
}
);
}
function FeatureRow({ label, field, data, editing, onChange }) {
const val = !!(data?.[field]);
return (
{editing ? (
onChange(field, e.target.checked)} style={{ width: 'auto' }} />
{label}
) : (
<>
{label}
>
)}
);
}
// ── Page principale ────────────────────────────────────────────────────────────
export default function PlatformeProfile() {
const { id } = useParams();
const navigate = useNavigate();
const { isAdmin } = useAuth();
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [err, setErr] = useState(null);
const [editing, setEditing] = useState(false);
const [form, setForm] = useState(null);
const [saving, setSaving] = useState(false);
const [saveErr, setSaveErr] = useState(null);
const [catsInv, setCatsInv] = useState([]); // categories_inv globales
const [secteursInv, setSecteursInv] = useState([]); // secteurs_inv globaux
useEffect(() => {
load();
if (isAdmin) {
api.get('/ref-categories').then(setCatsInv).catch(() => {});
api.get('/ref-secteurs').then(setSecteursInv).catch(() => {});
}
}, [id]);
async function load() {
try {
setLoading(true);
const endpoint = isAdmin ? `/referentiel/${id}` : `/referentiel-public/${id}`;
const d = await api.get(endpoint);
setData(d);
} catch (e) { setErr(e.message); }
finally { setLoading(false); }
}
function startEdit() {
const paysOp = Array.isArray(data.pays_operation)
? data.pays_operation.join(', ')
: (data.pays_operation || '');
setForm({
...data,
pays_operation_str: paysOp,
categories_inv_ids: (data.categories_inv || []).map(c => c.id),
secteurs_inv_ids: (data.secteurs_inv || []).map(s => s.id),
});
setEditing(true);
setSaveErr(null);
}
function cancelEdit() { setEditing(false); setForm(null); }
const setField = (key, val) => setForm(f => ({ ...f, [key]: val }));
const setInput = key => e => setField(key, e.target.type === 'checkbox' ? e.target.checked : e.target.value);
async function save() {
setSaving(true); setSaveErr(null);
try {
const paysArr = form.pays_operation_str
? form.pays_operation_str.split(',').map(s => s.trim()).filter(Boolean)
: [];
const payload = {
nom: form.nom,
url: form.url || null,
domiciliation: form.domiciliation,
fiscalite: form.fiscalite,
taux_fiscalite_locale: form.taux_fiscalite_locale !== '' && form.taux_fiscalite_locale != null ? Number(form.taux_fiscalite_locale) : null,
type_produit_fiscal: form.type_produit_fiscal || '2TT',
logo_filename: data.logo_filename ?? null,
description: form.description || null,
categories: data.categories || [],
notation: data.notation || [],
categories_inv_ids: form.categories_inv_ids || [],
secteurs_inv_ids: form.secteurs_inv_ids || [],
// Profil enrichi
annee_creation: form.annee_creation ? Number(form.annee_creation) : null,
investisseurs_types: form.investisseurs_types || null,
regulateur: form.regulateur || null,
numero_licence: form.numero_licence || null,
is_regule: form.is_regule ? 1 : 0,
pays_inscription: form.pays_inscription || null,
pays_siege: form.pays_siege || null,
pays_operation: paysArr,
investissement_minimum: form.investissement_minimum !== '' && form.investissement_minimum != null ? Number(form.investissement_minimum) : null,
rendement_annonce: form.rendement_annonce !== '' && form.rendement_annonce != null ? Number(form.rendement_annonce) : null,
nb_investisseurs: form.nb_investisseurs !== '' && form.nb_investisseurs != null ? Number(form.nb_investisseurs) : null,
volume_total_finance: form.volume_total_finance !== '' && form.volume_total_finance != null ? Number(form.volume_total_finance) : null,
duree_moyenne_pret: form.duree_moyenne_pret !== '' && form.duree_moyenne_pret != null ? Number(form.duree_moyenne_pret) : null,
garantie_rachat: form.garantie_rachat ? 1 : 0,
statistiques_publiques: form.statistiques_publiques ? 1 : 0,
bonus_inscription: form.bonus_inscription ? 1 : 0,
marche_secondaire: form.marche_secondaire ? 1 : 0,
investissement_auto: form.investissement_auto ? 1 : 0,
url_trustpilot: form.url_trustpilot || null,
url_linkedin: form.url_linkedin || null,
};
const updated = await api.put(`/referentiel/${id}`, payload);
setData(updated);
setCatsInv(await api.get('/ref-categories').catch(() => catsInv));
setEditing(false);
setForm(null);
} catch (e) { setSaveErr(e.message); }
finally { setSaving(false); }
}
if (loading) return Chargement…
;
if (err) return {err}
;
if (!data) return null;
const d = editing ? form : data;
const paysOp = Array.isArray(data.pays_operation) ? data.pays_operation : [];
return (
{/* ── Breadcrumb ─────────────────────────────────────────────────────── */}
navigate(-1)}
style={{ background: 'none', border: 'none', cursor: 'pointer', padding: '2px 6px', borderRadius: 4, color: 'var(--text-muted)', fontSize: 13, display: 'flex', alignItems: 'center', gap: 4 }}
onMouseEnter={e => e.currentTarget.style.background = 'var(--surface-2)'}
onMouseLeave={e => e.currentTarget.style.background = 'none'}
>
← Retour
/
Référentiel
/
{data.nom}
{/* ── Hero ───────────────────────────────────────────────────────────── */}
{/* Logo */}
{data.logo_filename
?
{ e.currentTarget.style.display = 'none'; }} />
:
{data.nom[0].toUpperCase()}
}
{/* Titre + badges */}
{editing
?
:
{data.nom}
}
{data.is_regule
? ✓ Réglementé
: isAdmin && Non réglementé
}
{data.domiciliation && (
{countryLabel(data.domiciliation)}
)}
{editing
?
: data.description && (
{data.description}
)
}
{/* Actions admin */}
{isAdmin && (
{editing ? (
<>
{saving ? 'Enregistrement…' : 'Enregistrer'}
Annuler
>
) : (
✎ Modifier
)}
)}
{saveErr &&
{saveErr}
}
{/* ── Corps : 2 colonnes ─────────────────────────────────────────────── */}
{/* ── Colonne gauche ──────────────────────────────────────────────── */}
{/* KPIs */}
{/* Features */}
{/* Catégories */}
{(data.categories?.length > 0) && (
Catégories
{data.categories.map(c => {c} )}
)}
{/* ── Colonne droite ───────────────────────────────────────────────── */}
{/* Identité */}
Identité
}
/>
{/* Catégories d'investissement (multi-select global) */}
Catégories d'investissement
{editing ? (
setField('categories_inv_ids', ids)}
addApiPath="/categories-inv"
onItemAdded={cat => setCatsInv(prev => [...prev, cat].sort((a,b) => a.nom.localeCompare(b.nom)))}
emptyLabel="Aucune catégorie d'investissement"
addLabel="Ajouter une catégorie d'investissement"
inputPlaceholder="Nom de la catégorie…"
inheritedIds={(data.categories_inv || []).filter(c => c.is_inherited).map(c => c.id)}
/>
) : (
{(data.categories_inv || []).length === 0
? —
: (data.categories_inv || []).map(c => (
{c.nom}
{c.is_inherited && Réf }
))
}
)}
{/* Secteurs d'investissement (multi-select global) */}
Secteurs d'investissement
{editing ? (
setField('secteurs_inv_ids', ids)}
addApiPath="/secteurs-inv"
onItemAdded={sect => setSecteursInv(prev => [...prev, sect].sort((a,b) => a.nom.localeCompare(b.nom)))}
emptyLabel="Aucun secteur d'investissement"
addLabel="Ajouter un secteur d'investissement"
inputPlaceholder="Nom du secteur…"
inheritedIds={(data.secteurs_inv || []).filter(s => s.is_inherited).map(s => s.id)}
/>
) : (
{(data.secteurs_inv || []).length === 0
? —
: (data.secteurs_inv || []).map(s => (
{s.nom}
{s.is_inherited && Réf }
))
}
)}
—
Particulier
Entreprise
Particulier & Entreprise
}
/>
{/* Régulation */}
Régulation
{editing ? (
Plateforme réglementée
) : (
Statut
{data.is_regule ? '✓ Réglementé' : '—'}
)}
}
/>
}
/>
{/* Géographie */}
Géographie
}
/>
{countryLabel(data.domiciliation)} : '—'}
editing={editing}
input={
setField('domiciliation', code)}
showCode
/>
}
/>
Pays d'opération
{editing
?
: paysOp.length > 0
?
{paysOp.map(p => (
{p}
))}
:
—
}
{/* Liens */}
Liens
{editing ? (
) : (
{/* Bouton site web */}
{data.url && (
)}
{/* Trustpilot */}
{data.url_trustpilot && (
Avis
)}
{/* Icônes réseaux sociaux */}
{data.url_linkedin && (
)}
{!data.url && !data.url_trustpilot && !data.url_linkedin && (
Aucun lien renseigné
)}
)}
{/* ── Outils admin IA ─────────────────────────────────────────────────── */}
{isAdmin && (
)}
);
}
// ── Champs profil lisibles ────────────────────────────────────────────────────
const PROFILE_FIELD_LABELS = {
description: 'Description',
annee_creation: 'Année de création',
categories_inv: "Catégories d'investissement",
secteurs_inv: 'Secteurs d\'investissement',
investisseurs_types: 'Investisseurs acceptés',
is_regule: 'Réglementé',
regulateur: 'Régulateur',
numero_licence: 'N° de licence',
pays_inscription: 'Pays inscription',
domiciliation: 'Domiciliation',
pays_operation: "Pays d'opération",
investissement_minimum: 'Invest. minimum (€)',
rendement_annonce: 'Rendement annoncé (%)',
nb_investisseurs: 'Nb investisseurs',
volume_total_finance: 'Volume total financé (€)',
duree_moyenne_pret: 'Durée moy. prêt (mois)',
garantie_rachat: 'Garantie de rachat',
statistiques_publiques: 'Stats publiques',
bonus_inscription: "Bonus d'inscription",
marche_secondaire: 'Marché secondaire',
investissement_auto: 'Invest. automatique',
url: 'Site web',
url_trustpilot: 'Trustpilot',
url_linkedin: 'LinkedIn',
};
// Champs du profil enrichi (pas les champs fiscaux/admin)
const PROFILE_FIELDS = Object.keys(PROFILE_FIELD_LABELS);
function fmtProfileVal(val) {
if (val === null || val === undefined) return '—';
if (typeof val === 'boolean') return val ? 'Oui' : 'Non';
if (typeof val === 'number' && (val === 0 || val === 1) && typeof val === 'number') return val ? 'Oui' : 'Non';
if (Array.isArray(val)) return val.join(', ') || '—';
return String(val);
}
// ── Bloc import JSON ──────────────────────────────────────────────────────────
function ProfilImportBlock({ data, id, onApplied, catsInv = [], secteursInv = [] }) {
const [open, setOpen] = useState(false);
const [json, setJson] = useState('');
const [parsed, setParsed] = useState(null);
const [parseErr, setParseErr] = useState(null);
const [selected, setSelected] = useState({});
const [applying, setApplying] = useState(false);
const [result, setResult] = useState(null);
const [toCreate, setToCreate] = useState({}); // { nom: bool } pour items inconnus
const [step, setStep] = useState('json'); // 'json' | 'creation' | 'diff'
// Helper : résout les noms IA → IDs + détecte les inconnus
const resolveTags = (aiNames, knownList, currentItems) => {
const norm = s => s.toLowerCase().trim().normalize('NFD').replace(/[̀-ͯ]/g, '');
const lev = (a, b) => {
const m = a.length, n = b.length;
const dp = Array.from({length: m+1}, (_, i) => Array.from({length: n+1}, (_, j) => j===0?i:0));
for (let j=1;j<=n;j++) dp[0][j]=j;
for (let i=1;i<=m;i++) for (let j=1;j<=n;j++)
dp[i][j] = a[i-1]===b[j-1] ? dp[i-1][j-1] : 1+Math.min(dp[i-1][j],dp[i][j-1],dp[i-1][j-1]);
return dp[m][n];
};
const isSim = (a, b) => { const na=norm(a),nb=norm(b); return na===nb||na.includes(nb)||nb.includes(na)||lev(na,nb)<=2; };
const resolvedIds = [], resolvedNames = [], unknownNames = [];
for (const aiName of (aiNames || [])) {
const match = knownList.find(k => isSim(aiName, k.nom));
if (match) { resolvedIds.push(match.id); resolvedNames.push(match.nom); }
else unknownNames.push(aiName);
}
const currentNames = (currentItems || []).map(c => c.nom);
const currentIds = (currentItems || []).map(c => c.id).sort().join(',');
const newIds = [...resolvedIds].sort().join(',');
const hasChange = newIds !== currentIds || unknownNames.length > 0;
return { resolvedIds, resolvedNames, unknownNames, currentNames, hasChange };
};
const parse = () => {
setParseErr(null); setParsed(null); setSelected({}); setResult(null); setToCreate({}); setStep('json');
let obj;
try {
const cleaned = json.replace(/^```(?:json)?\s*/i, '').replace(/\s*```$/, '').trim();
obj = JSON.parse(cleaned);
} catch (e) { setParseErr('JSON invalide : ' + e.message); return; }
if (typeof obj !== 'object' || Array.isArray(obj)) {
setParseErr('Le JSON doit être un objet (pas un tableau).'); return;
}
// Champs normaux (hors cats/secteurs qui ont un traitement spécial)
const SKIP = new Set(['categories_inv', 'secteurs_inv']);
const changes = [];
for (const field of PROFILE_FIELDS) {
if (SKIP.has(field) || !(field in obj)) continue;
const incoming = obj[field];
const current = data[field];
const normVal = v => {
if (Array.isArray(v)) return v.join(',');
if (v === true || v === 1) return '1';
if (v === false || v === 0) return '0';
return String(v ?? '');
};
if (normVal(incoming) !== normVal(current)) {
changes.push({ field, before: current, after: incoming });
}
}
// Résolution catégories / secteurs
const catAiNames = Array.isArray(obj.categories_inv) ? obj.categories_inv : [];
const sectAiNames = Array.isArray(obj.secteurs_inv) ? obj.secteurs_inv : [];
const catRes = resolveTags(catAiNames, catsInv, data.categories_inv || []);
const sectRes = resolveTags(sectAiNames, secteursInv, data.secteurs_inv || []);
if (catRes.hasChange && catAiNames.length > 0) {
changes.push({
field: 'categories_inv',
before: catRes.currentNames,
after: [...catRes.resolvedNames, ...catRes.unknownNames.map(n => n + ' ✦')],
_resolution: catRes,
});
}
if (sectRes.hasChange && sectAiNames.length > 0) {
changes.push({
field: 'secteurs_inv',
before: sectRes.currentNames,
after: [...sectRes.resolvedNames, ...sectRes.unknownNames.map(n => n + ' ✦')],
_resolution: sectRes,
});
}
if (changes.length === 0) {
setParseErr('Aucune différence détectée avec les données actuelles.');
return;
}
const allUnknowns = [...catRes.unknownNames, ...sectRes.unknownNames];
const initCreate = {};
allUnknowns.forEach(n => { initCreate[n] = true; }); // tous cochés par défaut
setToCreate(initCreate);
setParsed({ obj, changes, catRes, sectRes });
const sel = {};
changes.forEach(c => { sel[c.field] = true; });
setSelected(sel);
// Si inconnues → étape création, sinon directement diff
setStep(allUnknowns.length > 0 ? 'creation' : 'diff');
};
const apply = async () => {
setApplying(true); setResult(null);
const obj = parsed.obj;
const selectedChanges = parsed.changes.filter(c => selected[c.field]);
if (selectedChanges.length === 0) { setApplying(false); return; }
// Construire le payload complet (base = data actuel, on écrase les champs sélectionnés)
const payload = {
nom: data.nom,
url: data.url || null,
domiciliation: data.domiciliation,
fiscalite: data.fiscalite,
taux_fiscalite_locale: data.taux_fiscalite_locale ?? null,
type_produit_fiscal: data.type_produit_fiscal || '2TT',
logo_filename: data.logo_filename ?? null,
description: data.description || null,
categories: data.categories || [],
notation: data.notation || [],
annee_creation: data.annee_creation ?? null,
investisseurs_types: data.investisseurs_types ?? null,
regulateur: data.regulateur ?? null,
numero_licence: data.numero_licence ?? null,
is_regule: data.is_regule ? 1 : 0,
pays_inscription: data.pays_inscription ?? null,
pays_siege: data.pays_siege ?? null,
pays_operation: Array.isArray(data.pays_operation) ? data.pays_operation : [],
investissement_minimum: data.investissement_minimum ?? null,
rendement_annonce: data.rendement_annonce ?? null,
nb_investisseurs: data.nb_investisseurs ?? null,
volume_total_finance: data.volume_total_finance ?? null,
duree_moyenne_pret: data.duree_moyenne_pret ?? null,
garantie_rachat: data.garantie_rachat ? 1 : 0,
statistiques_publiques: data.statistiques_publiques ? 1 : 0,
bonus_inscription: data.bonus_inscription ? 1 : 0,
marche_secondaire: data.marche_secondaire ? 1 : 0,
investissement_auto: data.investissement_auto ? 1 : 0,
url_trustpilot: data.url_trustpilot ?? null,
url_linkedin: data.url_linkedin ?? null,
};
// Écraser avec les champs sélectionnés
for (const c of selectedChanges) {
const v = c.after;
if (c.field === 'pays_operation') {
payload.pays_operation = Array.isArray(v) ? v : [];
} else if (c.field === 'is_regule' || c.field === 'garantie_rachat' ||
c.field === 'statistiques_publiques' || c.field === 'bonus_inscription' ||
c.field === 'marche_secondaire' || c.field === 'investissement_auto') {
payload[c.field] = v ? 1 : 0;
} else {
payload[c.field] = v ?? null;
}
}
// Résoudre categories_inv_ids et secteurs_inv_ids
let catIds = (data.categories_inv || []).map(c => c.id);
let sectIds = (data.secteurs_inv || []).map(s => s.id);
const catChange = selectedChanges.find(c => c.field === 'categories_inv');
const sectChange = selectedChanges.find(c => c.field === 'secteurs_inv');
try {
if (catChange) {
catIds = [...catChange._resolution.resolvedIds];
for (const nom of catChange._resolution.unknownNames) {
if (toCreate[nom]) {
const created = await api.post('/ref-categories', { nom });
catIds.push(created.id);
}
}
}
if (sectChange) {
sectIds = [...sectChange._resolution.resolvedIds];
for (const nom of sectChange._resolution.unknownNames) {
if (toCreate[nom]) {
const created = await api.post('/ref-secteurs', { nom });
sectIds.push(created.id);
}
}
}
payload.categories_inv_ids = catIds;
payload.secteurs_inv_ids = sectIds;
await api.put('/referentiel/' + id, payload);
setResult({ ok: true, msg: `✔ ${selectedChanges.length} champ${selectedChanges.length > 1 ? 's' : ''} mis à jour.` });
onApplied();
setOpen(false); setJson(''); setParsed(null); setSelected({}); setStep('json');
} catch (e) {
setResult({ ok: false, msg: `✗ ${e.message}` });
} finally { setApplying(false); }
};
const nbSelected = Object.values(selected).filter(Boolean).length;
return (
setOpen(o => !o)} style={{
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
width: '100%', padding: '12px 16px', background: 'var(--surface-2)',
border: 'none', cursor: 'pointer', textAlign: 'left',
borderBottom: open ? '1px solid var(--border)' : 'none',
}}>
Importer les données IA
{parsed && — {parsed.changes.length} champ{parsed.changes.length > 1 ? 's' : ''} modifié{parsed.changes.length > 1 ? 's' : ''} }
{open && (
setResult(null)} style={{ marginBottom: 12 }} />
{step === 'json' && (
<>
Collez ici le JSON retourné par l'IA (avec ou sans bloc ```json```).
)}
);
}
// ── Prompt par défaut ─────────────────────────────────────────────────────────
const DEFAULT_PROFIL_PROMPT = `Tu es un assistant spécialisé dans l'analyse de plateformes de crowdfunding. Analyse la page indiquée et extrais les informations de profil de la plateforme.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
PLATEFORME À ANALYSER
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Nom : {{PLATFORM_NAME}}
URL à analyser : {{PLATFORM_URL}}
Tu peux aussi chercher le profil CrowdSpace :
https://thecrowdspace.com/fr/platform/[slug-de-la-plateforme]/
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
DONNÉES ACTUELLEMENT EN BASE
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
{{CURRENT_DATA}}
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
CHAMPS À EXTRAIRE
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Retourne un objet JSON avec les champs suivants (null si information absente ou incertaine) :
- description : description courte en français (2-3 phrases max)
- annee_creation : année de fondation (entier, ex: 2017)
- categories_inv : tableau de noms de catégories. Privilégie les valeurs disponibles : {{CATEGORIES_INV_LIST}}. Si la plateforme appartient clairement à une catégorie absente de cette liste (et non une reformulation d'une existante), indique-la quand même. Null si inconnu.
- secteurs_inv : tableau de noms de secteurs. Privilégie les valeurs disponibles : {{SECTEURS_INV_LIST}}. Si la plateforme opère dans un secteur absent de cette liste (et non une reformulation d'un existant), indique-le quand même. Null si inconnu.
- investisseurs_types : "particulier" | "entreprise" | "les_deux"
- is_regule : true | false
- regulateur : nom de l'autorité (ex: "AMF", "ACPR", "FCA")
- numero_licence : numéro de licence ou d'agrément
- pays_inscription : pays d'immatriculation légale
- pays_siege : pays du siège social
- pays_operation : tableau de pays (ex: ["France", "Allemagne"])
- investissement_minimum : montant minimum en EUR (ex: 20)
- rendement_annonce : taux annoncé en % (ex: 8.5)
- nb_investisseurs : nombre d'investisseurs enregistrés
- volume_total_finance : volume total en EUR (ex: 50000000)
- duree_moyenne_pret : durée moyenne en mois (ex: 18)
- garantie_rachat : true | false
- statistiques_publiques : true | false
- bonus_inscription : true | false
- marche_secondaire : true | false
- investissement_auto : true | false
- url : URL officielle du site
- url_trustpilot : URL Trustpilot complète
- url_linkedin : URL LinkedIn complète
- source : URL de la page consultée
- date_collecte : date d'aujourd'hui (ISO 8601)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
INSTRUCTIONS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. Consulte l'URL fournie et/ou la page CrowdSpace.
2. N'inclus que les informations vérifiables sur les pages.
3. Si une information est incertaine, mets null.
4. Pour les booléens, base-toi uniquement sur ce qui est explicitement mentionné.
5. Retourne UNIQUEMENT le JSON, sans commentaires ni explication.`;
// ── Bloc prompt IA ────────────────────────────────────────────────────────────
function ProfilPromptBlock({ data, catsInv = [], secteursInv = [] }) {
const LS_KEY = 'cl_profil_prompt';
const [open, setOpen] = useState(false);
const [editing, setEditing] = useState(false);
const [prompt, setPrompt] = useState(() => localStorage.getItem(LS_KEY) || DEFAULT_PROFIL_PROMPT);
const [draft, setDraft] = useState('');
const [url, setUrl] = useState('');
const [copied, setCopied] = useState(false);
const currentDataLines = () => {
const fields = [
['description', data.description],
['annee_creation', data.annee_creation],
['categories_inv', (data.categories_inv || []).map(c => c.nom).join(', ') || null],
['secteurs_inv', (data.secteurs_inv || []).map(s => s.nom).join(', ') || null],
['investisseurs_types', data.investisseurs_types],
['is_regule', data.is_regule ? 'oui' : 'non'],
['regulateur', data.regulateur],
['numero_licence', data.numero_licence],
['pays_inscription', data.pays_inscription],
['domiciliation', countryLabel(data.domiciliation)],
['pays_operation', Array.isArray(data.pays_operation) ? data.pays_operation.join(', ') : data.pays_operation],
['investissement_minimum', data.investissement_minimum != null ? data.investissement_minimum + ' €' : null],
['rendement_annonce', data.rendement_annonce != null ? data.rendement_annonce + ' %' : null],
['nb_investisseurs', data.nb_investisseurs],
['volume_total_finance', data.volume_total_finance != null ? data.volume_total_finance + ' €' : null],
['duree_moyenne_pret', data.duree_moyenne_pret != null ? data.duree_moyenne_pret + ' mois' : null],
['garantie_rachat', data.garantie_rachat ? 'oui' : 'non'],
['statistiques_publiques', data.statistiques_publiques ? 'oui' : 'non'],
['bonus_inscription', data.bonus_inscription ? 'oui' : 'non'],
['marche_secondaire', data.marche_secondaire ? 'oui' : 'non'],
['investissement_auto', data.investissement_auto ? 'oui' : 'non'],
['url', data.url],
['url_trustpilot', data.url_trustpilot],
['url_linkedin', data.url_linkedin],
];
return fields.map(([k, v]) => `- ${k}: ${v ?? '(vide)'}`).join('\n');
};
const resolvedPrompt = () => {
const catsList = (catsInv || []).map(c => c.nom).join(' | ') || 'aucune catégorie définie';
const sectsList = (secteursInv || []).map(s => s.nom).join(' | ') || 'aucun secteur défini';
return prompt
.replace('{{PLATFORM_NAME}}', data.nom)
.replace('{{PLATFORM_URL}}', url || '(à renseigner ci-dessus)')
.replace('{{CURRENT_DATA}}', currentDataLines())
.replace('{{CATEGORIES_INV_LIST}}', catsList)
.replace('{{SECTEURS_INV_LIST}}', sectsList);
};
const startEdit = () => { setDraft(prompt); setEditing(true); };
const saveEdit = () => { setPrompt(draft); localStorage.setItem(LS_KEY, draft); setEditing(false); };
const reset = () => { setPrompt(DEFAULT_PROFIL_PROMPT); localStorage.removeItem(LS_KEY); setEditing(false); };
const copy = async () => {
const text = resolvedPrompt();
try { await navigator.clipboard.writeText(text); }
catch { const t = document.createElement('textarea'); t.value = text; document.body.appendChild(t); t.select(); document.execCommand('copy'); document.body.removeChild(t); }
setCopied(true); setTimeout(() => setCopied(false), 2000);
};
return (
setOpen(o => !o)} style={{
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
width: '100%', padding: '12px 16px', background: 'var(--surface-2)',
border: 'none', cursor: 'pointer', textAlign: 'left',
borderBottom: open ? '1px solid var(--border)' : 'none',
}}>
PROMPT IA — Collecte du profil plateforme
{open && (
Le prompt inclut les données actuelles de la plateforme. Renseignez l'URL à analyser (CrowdSpace ou site officiel), copiez, puis collez dans un assistant IA capable de naviguer sur le web.
{/* URL input */}
URL de la page à analyser
setUrl(e.target.value)}
placeholder="https://thecrowdspace.com/fr/platform/..."
style={{ width: '100%', fontSize: 13, boxSizing: 'border-box' }} />
{copied ? '✓ Copié !' : '📋 Copier le prompt'}
{!editing ? (
✏️ Modifier le template
) : (
<>
Enregistrer
setEditing(false)} style={{ fontSize: 12, padding: '6px 14px' }}>Annuler
Remettre par défaut
>
)}
{editing ? (
)}
);
}