import type { CodevFiche, CodevMatch } from '~/types/codev' const STOP_WORDS_FR = new Set([ 'le', 'la', 'les', 'un', 'une', 'des', 'de', 'du', 'au', 'aux', 'et', 'ou', 'mais', 'donc', 'car', 'ni', 'or', 'a', 'en', 'pour', 'par', 'sur', 'avec', 'sans', 'dans', 'sous', 'je', 'tu', 'il', 'elle', 'on', 'nous', 'vous', 'ils', 'elles', 'mon', 'ma', 'mes', 'ton', 'ta', 'tes', 'son', 'sa', 'ses', 'notre', 'nos', 'votre', 'vos', 'leur', 'leurs', 'ce', 'cet', 'cette', 'ces', 'qui', 'que', 'quoi', 'dont', 'est', 'sont', 'etre', 'ai', 'as', 'avoir', 'pas', 'plus', 'moins', 'tres', 'aussi', 'bien', 'tout', 'tous', 'me', 'te', 'se', 'lui', 'leur', 'y', ]) function tokenize(text: string): Set { if (!text) return new Set() const tokens = text .toLowerCase() .replace(/[.,;:!?()'"\-/]/g, ' ') .split(/\s+/) .filter((t) => t.length >= 3 && !STOP_WORDS_FR.has(t)) return new Set(tokens) } function jaccard(a: Set, b: Set): number { if (a.size === 0 || b.size === 0) return 0 let inter = 0 for (const x of a) if (b.has(x)) inter++ const union = a.size + b.size - inter return union === 0 ? 0 : inter / union } function score(textA: string, hashtagsA: string[], textB: string, hashtagsB: string[]): number { const tagsA = new Set(hashtagsA.map((h) => h.toLowerCase())) const tagsB = new Set(hashtagsB.map((h) => h.toLowerCase())) if (tagsA.size > 0 && tagsB.size > 0) { return jaccard(tagsA, tagsB) } return jaccard(tokenize(textA), tokenize(textB)) } const THRESHOLD = 0.15 export function matchSolution(fiches: CodevFiche[]): 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) { matches.push({ fromId: a.id, toId: b.id, score: s, mode: 'solution' }) } } } return matches } export function matchAlliance(fiches: CodevFiche[]): 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] const s = score(a.besoin, a.hashtags, b.besoin, b.hashtags) if (s >= THRESHOLD) { matches.push({ fromId: a.id, toId: b.id, score: s, mode: 'alliance' }) } } } return matches } export function matchSurprise(fiches: CodevFiche[]): 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] const s = score(a.offre, a.hashtags, b.offre, b.hashtags) if (s >= THRESHOLD) { matches.push({ fromId: a.id, toId: b.id, score: s, mode: 'surprise' }) } } } return matches } export function computeMatches( fiches: CodevFiche[], mode: 'solution' | 'alliance' | 'surprise', ): CodevMatch[] { switch (mode) { case 'solution': return matchSolution(fiches) case 'alliance': return matchAlliance(fiches) case 'surprise': return matchSurprise(fiches) } }