feat(codev): M4 - matching 3 modes + boutons UI + animation force
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
97
utils/codev/matching.ts
Normal file
97
utils/codev/matching.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
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<string> {
|
||||
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<string>, b: Set<string>): 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user