From 6f7d2450dec240eabb375bfa970b4a1bff62727e Mon Sep 17 00:00:00 2001 From: Jules Neny Date: Wed, 6 May 2026 21:28:27 +0200 Subject: [PATCH] fix(codev): algo Solution tokenize direct + seuils releves + fiches demo enrichies --- pages/codev/demo.vue | 115 +++++++++++++++++++--------------------- utils/codev/matching.ts | 31 +++++++---- 2 files changed, 75 insertions(+), 71 deletions(-) diff --git a/pages/codev/demo.vue b/pages/codev/demo.vue index 81047b5..6cde6d6 100644 --- a/pages/codev/demo.vue +++ b/pages/codev/demo.vue @@ -73,98 +73,93 @@ import type { CodevFiche, CodevMatch } from '~/types/codev' import { computeMatches } from '~/utils/codev/matching' -// 10 fiches factices - hashtags alignes pour demontrer les 3 modes : +// 10 fiches sans hashtags — textes enrichis pour que scoreDirect discrimine bien les 3 modes : // -// Solution : Lea(besoin coaching) -> Maya(offre coaching) -// Sami(besoin formation+vente) -> Ines(offre vente+formation) -// Tom(besoin tiers-lieu) -> Zoe(offre facilitation+tiers-lieu) +// Solution (scoreDirect besoinA vs offreB) : +// Sami(besoin vendre formation) -> Ines(offre vente formations) ✓ +// Nael(besoin site web formation) -> Sami(offre developpement web) ✓ +// Eva(besoin coaching vente) -> Ines(offre vente formations) ✓ +// Tom(besoin tiers-lieu) -> Zoe(offre facilitation tiers-lieux) ✓ // -// Alliance : Lea + Maya (hashtag coaching commun dans besoins) -// Sami + Kenji (hashtag formation+vente dans besoins) -// Tom + Zoe (hashtag tiers-lieu dans besoins) +// Alliance (besoins similaires) : +// Lea + Maya (coaching, lancer, offre) ✓ +// Tom + Zoe (tiers-lieu, co-creer) ✓ +// Sami + Kenji (vendre, formations) ✓ // -// Surprise : Lea + Zoe (hashtag facilitation dans offres) -// Tom + Roman (hashtag archi dans offres) +// Surprise (offres similaires) : +// Lea + Zoe (facilitation, groupes) ✓ +// Tom + Roman (architecture) ✓ +// Ines + Nael (marketing, formations) ✓ const FICHES_DEMO: CodevFiche[] = [ { - id: 1, - nom: 'Lea', - besoin: 'Structurer mon offre de coaching pour la lancer en septembre', - offre: 'Animation de groupes, facilitation de cercles de parole', - hashtags: ['coaching', 'facilitation'], + id: 1, nom: 'Lea', + besoin: 'Structurer et lancer mon offre de coaching professionnel cet automne', + offre: 'Facilitation de groupes et animation de cercles de parole', + hashtags: [], created_at: '2026-05-08T10:00:00Z', }, { - id: 2, - nom: 'Sami', - besoin: 'Comprendre comment vendre une formation en ligne', - offre: 'Developpement web, sites Astro et Nuxt', - hashtags: ['formation', 'vente'], + id: 2, nom: 'Sami', + besoin: 'Vendre ma formation en ligne et attirer mes premiers clients', + offre: 'Developpement web sur mesure, creation de sites et applications', + hashtags: [], created_at: '2026-05-08T10:01:00Z', }, { - id: 3, - nom: 'Ines', - besoin: 'Aide pour la facilitation de mes ateliers ecriture', - offre: 'Vente de formations en ligne, marketing direct', - hashtags: ['vente', 'formation'], + id: 3, nom: 'Ines', + besoin: 'Ameliorer la facilitation de mes ateliers collaboratifs', + offre: 'Vente de formations en ligne et marketing pour formateurs', + hashtags: [], created_at: '2026-05-08T10:02:00Z', }, { - id: 4, - nom: 'Tom', - besoin: 'Trouver un associe pour un projet de tiers-lieu', - offre: 'Architecture eco-responsable, conception bioclimatique', - hashtags: ['tiers-lieu', 'archi'], + id: 4, nom: 'Tom', + besoin: 'Trouver des associes pour co-creer un tiers-lieu rural', + offre: 'Architecture bioclimatique et eco-construction pour tiers-lieux', + hashtags: [], created_at: '2026-05-08T10:03:00Z', }, { - id: 5, - nom: 'Maya', - besoin: 'Structurer mon offre de coaching freelance', - offre: 'Coaching de carriere, accompagnement transition pro', - hashtags: ['coaching', 'carriere'], + id: 5, nom: 'Maya', + besoin: 'Creer et lancer mon offre de coaching en transition professionnelle', + offre: 'Accompagnement coaching de carriere et transitions professionnelles', + hashtags: [], created_at: '2026-05-08T10:04:00Z', }, { - id: 6, - nom: 'Kenji', - besoin: 'Apprendre a vendre mes formations sans me sentir vendeur', - offre: 'Photographie, direction artistique de projets editoriaux', - hashtags: ['formation', 'vente'], + id: 6, nom: 'Kenji', + besoin: 'Apprendre a vendre mes formations sans pression commerciale', + offre: 'Photographie professionnelle et direction artistique editoriale', + hashtags: [], created_at: '2026-05-08T10:05:00Z', }, { - id: 7, - nom: 'Zoe', - besoin: 'Trouver des associes pour mon projet de tiers-lieu rural', - offre: 'Animation et facilitation de collectifs, intelligence collective', - hashtags: ['tiers-lieu', 'facilitation'], + id: 7, nom: 'Zoe', + besoin: 'Co-creer un tiers-lieu avec des porteurs de projet alignes', + offre: 'Facilitation de collectifs et animation en intelligence collective', + hashtags: [], created_at: '2026-05-08T10:06:00Z', }, { - id: 8, - nom: 'Nael', - besoin: 'Construire un site web pour ma formation', - offre: 'Strategie marketing, lancement de produits digitaux', - hashtags: ['web', 'strategie'], + id: 8, nom: 'Nael', + besoin: 'Creer un site web pour presenter et vendre ma formation', + offre: 'Strategie marketing digital et lancement de produits en ligne', + hashtags: [], created_at: '2026-05-08T10:07:00Z', }, { - id: 9, - nom: 'Eva', - besoin: 'Lancer mon offre de coaching avec une page de vente', - offre: 'Ecriture longue forme, articles essais et tribunes', - hashtags: ['coaching', 'ecriture'], + id: 9, nom: 'Eva', + besoin: 'Lancer mon coaching avec une page de vente qui convertit', + offre: 'Ecriture longue forme, articles de fond et tribunes editoriales', + hashtags: [], created_at: '2026-05-08T10:08:00Z', }, { - id: 10, - nom: 'Roman', - besoin: 'Ameliorer mes articles de blog sur la renovation', - offre: 'Architecture, plans techniques pour renovation energetique', - hashtags: ['archi', 'reno'], + id: 10, nom: 'Roman', + besoin: 'Ecrire de meilleurs articles pour mon blog et ma newsletter', + offre: 'Architecture technique et plans pour renovation energetique', + hashtags: [], created_at: '2026-05-08T10:09:00Z', }, ] @@ -186,7 +181,7 @@ function setMode(newMode: typeof mode.value) { if (newMode === 'none') { matches.value = [] } else { - matches.value = computeMatches(fiches.value, newMode) + matches.value = computeMatches(fiches.value, newMode, 0.12) } } diff --git a/utils/codev/matching.ts b/utils/codev/matching.ts index fe3135b..d8dc7e9 100644 --- a/utils/codev/matching.ts +++ b/utils/codev/matching.ts @@ -41,15 +41,21 @@ function score(textA: string, hashtagsA: string[], textB: string, hashtagsB: str return jaccard(tokenize(textA), tokenize(textB)) } -const THRESHOLD = 0.15 +// scoreDirect tokenise TOUJOURS les textes, ignore les hashtags +// Utilise pour matchSolution : besoin vs offre doivent etre compares par leur contenu reel +function scoreDirect(textA: string, textB: string): number { + return jaccard(tokenize(textA), tokenize(textB)) +} -export function matchSolution(fiches: CodevFiche[]): CodevMatch[] { +export function matchSolution(fiches: CodevFiche[], threshold = 0.18): CodevMatch[] { const matches: CodevMatch[] = [] for (const a of fiches) { for (const b of fiches) { if (a.id === b.id) continue - const s = score(a.besoin, a.hashtags, b.offre, b.hashtags) - if (s >= THRESHOLD) { + // Solution : on compare le TEXTE besoin de A avec le TEXTE offre de B + // On ignore les hashtags pour differencier besoin et offre + const s = scoreDirect(a.besoin, b.offre) + if (s >= threshold) { matches.push({ fromId: a.id, toId: b.id, score: s, mode: 'solution' }) } } @@ -57,13 +63,14 @@ export function matchSolution(fiches: CodevFiche[]): CodevMatch[] { return matches } -export function matchAlliance(fiches: CodevFiche[]): CodevMatch[] { +export function matchAlliance(fiches: CodevFiche[], threshold = 0.25): CodevMatch[] { const matches: CodevMatch[] = [] for (let i = 0; i < fiches.length; i++) { for (let j = i + 1; j < fiches.length; j++) { const a = fiches[i], b = fiches[j] + // Alliance : besoins similaires — on compare hashtags si presents, sinon textes const s = score(a.besoin, a.hashtags, b.besoin, b.hashtags) - if (s >= THRESHOLD) { + if (s >= threshold) { matches.push({ fromId: a.id, toId: b.id, score: s, mode: 'alliance' }) } } @@ -71,13 +78,14 @@ export function matchAlliance(fiches: CodevFiche[]): CodevMatch[] { return matches } -export function matchSurprise(fiches: CodevFiche[]): CodevMatch[] { +export function matchSurprise(fiches: CodevFiche[], threshold = 0.25): CodevMatch[] { const matches: CodevMatch[] = [] for (let i = 0; i < fiches.length; i++) { for (let j = i + 1; j < fiches.length; j++) { const a = fiches[i], b = fiches[j] + // Surprise : offres similaires const s = score(a.offre, a.hashtags, b.offre, b.hashtags) - if (s >= THRESHOLD) { + if (s >= threshold) { matches.push({ fromId: a.id, toId: b.id, score: s, mode: 'surprise' }) } } @@ -88,10 +96,11 @@ export function matchSurprise(fiches: CodevFiche[]): CodevMatch[] { export function computeMatches( fiches: CodevFiche[], mode: 'solution' | 'alliance' | 'surprise', + threshold?: number, ): CodevMatch[] { switch (mode) { - case 'solution': return matchSolution(fiches) - case 'alliance': return matchAlliance(fiches) - case 'surprise': return matchSurprise(fiches) + case 'solution': return matchSolution(fiches, threshold) + case 'alliance': return matchAlliance(fiches, threshold) + case 'surprise': return matchSurprise(fiches, threshold) } }