Files
Spark b102a4a206 feat(core): moteur PromptKey — bibliothèque .md, variables, injection, raccourcis
- PromptStore/Frontmatter : un prompt = un fichier Markdown, en-tête au
  sous-ensemble strict du document d'architecture (§7), écriture UTF-8
- VariableEngine : variables typées + automatiques (selection, clipboard,
  date, app, cursor), résolution et profils de destination
- Injector/ClipboardGuard : focus capturé/rendu, presse-papiers sauvegardé
  puis restauré conditionnellement, aucun envoi automatique par défaut
- Search : classement flou insensible aux accents, pondéré par l'usage
- UsageStats (usage.json), HotkeyManager (conflits signalés, jamais muets)
- Settings : config.ini + profils par IA (claude, chatgpt, qwen, terminal…)
- PromptKey.ahk : tray, instance unique, capture du contexte, surveillance
  de la bibliothèque, installation du pack au premier lancement
2026-10-11 10:17:30 -04:00

130 lines
4.2 KiB
AutoHotkey

; Search.ahk — Recherche floue et classement (architecture §14 et Annexe B).
; Fonction pure : score déterministe, insensible aux accents, pondéré par l'usage.
class Search {
; usage : Map id -> Map(count, last_used) (peut être vide)
static Filter(prompts, query, usage) {
q := Util.Normalize(Trim(query))
scored := []
if (q = "") {
for p in prompts
scored.Push(Map("prompt", p, "score", Search.UsageWeight(p, usage)))
} else {
words := StrSplit(q, " ")
for p in prompts {
total := 0
ok := true
hayTitle := Util.Normalize(p.Title)
hayTags := Util.Normalize(Join(p.Tags, " "))
hayCat := Util.Normalize(p.Category)
hayBody := Util.Normalize(SubStr(p.Body, 1, 2000))
for w in words {
if (w = "")
continue
s := Search.WordScore(w, hayTitle, hayTags, hayCat, hayBody)
if (s = 0) {
ok := false
break
}
total += s
}
if ok
scored.Push(Map("prompt", p, "score", total * Search.UsageWeight(p, usage)))
}
}
; Tri décroissant par score, égalité : dernier usage puis titre (déterminisme, Annexe B).
SortScored(scored, usage)
out := []
for entry in scored
out.Push(entry["prompt"])
return out
}
static WordScore(w, title, tags, cat, body) {
best := 0
best := Max(best, Search.FieldScore(w, title) * 4)
best := Max(best, Search.FieldScore(w, tags) * 2)
best := Max(best, Search.FieldScore(w, cat) * 2)
best := Max(best, Search.FieldScore(w, body))
return best
}
; Score d'un champ : sous-séquence floue ; bonus début de mot et mot entier.
static FieldScore(w, field) {
if (field = "")
return 0
if InStr(field, w) {
score := 50
; Mot entier ou début de champ / de mot.
if (SubStr(field, 1, StrLen(w)) = w)
score += 50
if InStr(field, " " w)
score += 40
if InStr(field, w " ") || InStr(field, " " w " ")
score += 30
return score
}
; Sous-séquence : toutes les lettres de w, dans l'ordre.
pos := 1
loop parse w {
found := InStr(field, A_LoopField, , pos)
if (!found)
return 0
pos := found + 1
}
return 15
}
static UsageWeight(p, usage) {
wgt := 1.0
if (p.Pinned > 0)
wgt *= 1.5
if (p.Favorite)
wgt *= 1.3
if (usage is Map && usage.Has(p.Id)) {
u := usage[p.Id]
count := u.Has("count") ? Integer(u["count"]) : 0
if (count > 0)
wgt *= 1 + (Log(1 + count) / Log(10)) * 0.2
if u.Has("last_used") {
age := DateDiff(A_Now, u["last_used"], "Days")
if (age <= 7)
wgt *= 1.2
}
}
return wgt
}
}
; Tri simple par insertion (les listes sont petites ; la stabilité prime sur la vitesse).
SortScored(scored, usage) {
n := scored.Length
loop n - 1 {
i := A_Index
loop n - i {
j := A_Index
a := scored[j], b := scored[j + 1]
swap := false
if (b["score"] > a["score"])
swap := true
else if (b["score"] = a["score"]) {
la := LastUsedOf(a["prompt"], usage), lb := LastUsedOf(b["prompt"], usage)
if (lb > la)
swap := true
else if (lb = la && StrCompare(a["prompt"].Title, b["prompt"].Title) > 0)
swap := true
}
if swap {
scored[j] := b
scored[j + 1] := a
}
}
}
}
LastUsedOf(p, usage) {
if (usage is Map && usage.Has(p.Id) && usage[p.Id].Has("last_used"))
return usage[p.Id]["last_used"]
return ""
}