fix(codev): algo Solution tokenize direct + seuils releves + fiches demo enrichies

This commit is contained in:
Jules Neny
2026-05-06 21:28:27 +02:00
parent e7c7d302ea
commit 6f7d2450de
2 changed files with 75 additions and 71 deletions

View File

@@ -73,98 +73,93 @@
import type { CodevFiche, CodevMatch } from '~/types/codev' import type { CodevFiche, CodevMatch } from '~/types/codev'
import { computeMatches } from '~/utils/codev/matching' 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) // Solution (scoreDirect besoinA vs offreB) :
// Sami(besoin formation+vente) -> Ines(offre vente+formation) // Sami(besoin vendre formation) -> Ines(offre vente formations) ✓
// Tom(besoin tiers-lieu) -> Zoe(offre facilitation+tiers-lieu) // 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) // Alliance (besoins similaires) :
// Sami + Kenji (hashtag formation+vente dans besoins) // Lea + Maya (coaching, lancer, offre) ✓
// Tom + Zoe (hashtag tiers-lieu dans besoins) // Tom + Zoe (tiers-lieu, co-creer) ✓
// Sami + Kenji (vendre, formations) ✓
// //
// Surprise : Lea + Zoe (hashtag facilitation dans offres) // Surprise (offres similaires) :
// Tom + Roman (hashtag archi dans offres) // Lea + Zoe (facilitation, groupes)
// Tom + Roman (architecture) ✓
// Ines + Nael (marketing, formations) ✓
const FICHES_DEMO: CodevFiche[] = [ const FICHES_DEMO: CodevFiche[] = [
{ {
id: 1, id: 1, nom: 'Lea',
nom: 'Lea', besoin: 'Structurer et lancer mon offre de coaching professionnel cet automne',
besoin: 'Structurer mon offre de coaching pour la lancer en septembre', offre: 'Facilitation de groupes et animation de cercles de parole',
offre: 'Animation de groupes, facilitation de cercles de parole', hashtags: [],
hashtags: ['coaching', 'facilitation'],
created_at: '2026-05-08T10:00:00Z', created_at: '2026-05-08T10:00:00Z',
}, },
{ {
id: 2, id: 2, nom: 'Sami',
nom: 'Sami', besoin: 'Vendre ma formation en ligne et attirer mes premiers clients',
besoin: 'Comprendre comment vendre une formation en ligne', offre: 'Developpement web sur mesure, creation de sites et applications',
offre: 'Developpement web, sites Astro et Nuxt', hashtags: [],
hashtags: ['formation', 'vente'],
created_at: '2026-05-08T10:01:00Z', created_at: '2026-05-08T10:01:00Z',
}, },
{ {
id: 3, id: 3, nom: 'Ines',
nom: 'Ines', besoin: 'Ameliorer la facilitation de mes ateliers collaboratifs',
besoin: 'Aide pour la facilitation de mes ateliers ecriture', offre: 'Vente de formations en ligne et marketing pour formateurs',
offre: 'Vente de formations en ligne, marketing direct', hashtags: [],
hashtags: ['vente', 'formation'],
created_at: '2026-05-08T10:02:00Z', created_at: '2026-05-08T10:02:00Z',
}, },
{ {
id: 4, id: 4, nom: 'Tom',
nom: 'Tom', besoin: 'Trouver des associes pour co-creer un tiers-lieu rural',
besoin: 'Trouver un associe pour un projet de tiers-lieu', offre: 'Architecture bioclimatique et eco-construction pour tiers-lieux',
offre: 'Architecture eco-responsable, conception bioclimatique', hashtags: [],
hashtags: ['tiers-lieu', 'archi'],
created_at: '2026-05-08T10:03:00Z', created_at: '2026-05-08T10:03:00Z',
}, },
{ {
id: 5, id: 5, nom: 'Maya',
nom: 'Maya', besoin: 'Creer et lancer mon offre de coaching en transition professionnelle',
besoin: 'Structurer mon offre de coaching freelance', offre: 'Accompagnement coaching de carriere et transitions professionnelles',
offre: 'Coaching de carriere, accompagnement transition pro', hashtags: [],
hashtags: ['coaching', 'carriere'],
created_at: '2026-05-08T10:04:00Z', created_at: '2026-05-08T10:04:00Z',
}, },
{ {
id: 6, id: 6, nom: 'Kenji',
nom: 'Kenji', besoin: 'Apprendre a vendre mes formations sans pression commerciale',
besoin: 'Apprendre a vendre mes formations sans me sentir vendeur', offre: 'Photographie professionnelle et direction artistique editoriale',
offre: 'Photographie, direction artistique de projets editoriaux', hashtags: [],
hashtags: ['formation', 'vente'],
created_at: '2026-05-08T10:05:00Z', created_at: '2026-05-08T10:05:00Z',
}, },
{ {
id: 7, id: 7, nom: 'Zoe',
nom: 'Zoe', besoin: 'Co-creer un tiers-lieu avec des porteurs de projet alignes',
besoin: 'Trouver des associes pour mon projet de tiers-lieu rural', offre: 'Facilitation de collectifs et animation en intelligence collective',
offre: 'Animation et facilitation de collectifs, intelligence collective', hashtags: [],
hashtags: ['tiers-lieu', 'facilitation'],
created_at: '2026-05-08T10:06:00Z', created_at: '2026-05-08T10:06:00Z',
}, },
{ {
id: 8, id: 8, nom: 'Nael',
nom: 'Nael', besoin: 'Creer un site web pour presenter et vendre ma formation',
besoin: 'Construire un site web pour ma formation', offre: 'Strategie marketing digital et lancement de produits en ligne',
offre: 'Strategie marketing, lancement de produits digitaux', hashtags: [],
hashtags: ['web', 'strategie'],
created_at: '2026-05-08T10:07:00Z', created_at: '2026-05-08T10:07:00Z',
}, },
{ {
id: 9, id: 9, nom: 'Eva',
nom: 'Eva', besoin: 'Lancer mon coaching avec une page de vente qui convertit',
besoin: 'Lancer mon offre de coaching avec une page de vente', offre: 'Ecriture longue forme, articles de fond et tribunes editoriales',
offre: 'Ecriture longue forme, articles essais et tribunes', hashtags: [],
hashtags: ['coaching', 'ecriture'],
created_at: '2026-05-08T10:08:00Z', created_at: '2026-05-08T10:08:00Z',
}, },
{ {
id: 10, id: 10, nom: 'Roman',
nom: 'Roman', besoin: 'Ecrire de meilleurs articles pour mon blog et ma newsletter',
besoin: 'Ameliorer mes articles de blog sur la renovation', offre: 'Architecture technique et plans pour renovation energetique',
offre: 'Architecture, plans techniques pour renovation energetique', hashtags: [],
hashtags: ['archi', 'reno'],
created_at: '2026-05-08T10:09:00Z', created_at: '2026-05-08T10:09:00Z',
}, },
] ]
@@ -186,7 +181,7 @@ function setMode(newMode: typeof mode.value) {
if (newMode === 'none') { if (newMode === 'none') {
matches.value = [] matches.value = []
} else { } else {
matches.value = computeMatches(fiches.value, newMode) matches.value = computeMatches(fiches.value, newMode, 0.12)
} }
} }
</script> </script>

View File

@@ -41,15 +41,21 @@ function score(textA: string, hashtagsA: string[], textB: string, hashtagsB: str
return jaccard(tokenize(textA), tokenize(textB)) 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[] = [] const matches: CodevMatch[] = []
for (const a of fiches) { for (const a of fiches) {
for (const b of fiches) { for (const b of fiches) {
if (a.id === b.id) continue if (a.id === b.id) continue
const s = score(a.besoin, a.hashtags, b.offre, b.hashtags) // Solution : on compare le TEXTE besoin de A avec le TEXTE offre de B
if (s >= THRESHOLD) { // 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' }) matches.push({ fromId: a.id, toId: b.id, score: s, mode: 'solution' })
} }
} }
@@ -57,13 +63,14 @@ export function matchSolution(fiches: CodevFiche[]): CodevMatch[] {
return matches return matches
} }
export function matchAlliance(fiches: CodevFiche[]): CodevMatch[] { export function matchAlliance(fiches: CodevFiche[], threshold = 0.25): CodevMatch[] {
const matches: CodevMatch[] = [] const matches: CodevMatch[] = []
for (let i = 0; i < fiches.length; i++) { for (let i = 0; i < fiches.length; i++) {
for (let j = i + 1; j < fiches.length; j++) { for (let j = i + 1; j < fiches.length; j++) {
const a = fiches[i], b = fiches[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) 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' }) matches.push({ fromId: a.id, toId: b.id, score: s, mode: 'alliance' })
} }
} }
@@ -71,13 +78,14 @@ export function matchAlliance(fiches: CodevFiche[]): CodevMatch[] {
return matches return matches
} }
export function matchSurprise(fiches: CodevFiche[]): CodevMatch[] { export function matchSurprise(fiches: CodevFiche[], threshold = 0.25): CodevMatch[] {
const matches: CodevMatch[] = [] const matches: CodevMatch[] = []
for (let i = 0; i < fiches.length; i++) { for (let i = 0; i < fiches.length; i++) {
for (let j = i + 1; j < fiches.length; j++) { for (let j = i + 1; j < fiches.length; j++) {
const a = fiches[i], b = fiches[j] const a = fiches[i], b = fiches[j]
// Surprise : offres similaires
const s = score(a.offre, a.hashtags, b.offre, b.hashtags) 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' }) 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( export function computeMatches(
fiches: CodevFiche[], fiches: CodevFiche[],
mode: 'solution' | 'alliance' | 'surprise', mode: 'solution' | 'alliance' | 'surprise',
threshold?: number,
): CodevMatch[] { ): CodevMatch[] {
switch (mode) { switch (mode) {
case 'solution': return matchSolution(fiches) case 'solution': return matchSolution(fiches, threshold)
case 'alliance': return matchAlliance(fiches) case 'alliance': return matchAlliance(fiches, threshold)
case 'surprise': return matchSurprise(fiches) case 'surprise': return matchSurprise(fiches, threshold)
} }
} }