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
This commit is contained in:
+129
@@ -0,0 +1,129 @@
|
||||
; 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 ""
|
||||
}
|
||||
Reference in New Issue
Block a user