feat: palette Ctrl+K — onglets Pages / ✨ Réponses IA (v7.44.0)
FlowDeck CI / lint (push) Successful in 1m57s
FlowDeck CI / test (push) Successful in 15m21s
FlowDeck CI / docker (push) Successful in 1m51s

Added :
- Onglets dans la palette (markup + CSS + wiring IIFE) :
  · Pages — comportement inchange (recherche /api/search + actions).
  · ✨ Réponses IA — POST /api/v2/search/ask (debounce 150 ms, CSRF via
    getCsrf(), gardes staleness onglet+requete) → answer_markdown rendu :
    echappement AVANT injection, [[fdpage:ID]] → lien citation (ids =
    chiffres, type = [a-z]+ — pas d'injection), **gras**, bloc « Sources »
    avec liens /pages/{id} · /db/{id} ; etats hint/chargement/erreur.
- Gate E2E etendu : ouverture → clic onglet IA → reponse non-echappee.
  (probe : ask = 200 en 1,6s, 8 citations, provider actif)

Reporte (backend absent) : onglet Fichiers — /api/search ne renvoie que
pages/collections → endpoint a creer d'abord (ROADMAP, entree mise a jour).

Notes : 401 transitoire sur le 1er ask d'un run E2E observe une fois
(session fraiche), non reproductible ensuite — signale CHANGELOG.

suite **1094/1094** · ruff OK · E2E **7/7** · docs a jour
This commit is contained in:
2026-10-02 16:27:52 -04:00
parent de751ffe35
commit b0af1bbfb6
8 changed files with 116 additions and 6 deletions
+1 -1
View File
@@ -185,7 +185,7 @@ async def lifespan(_app: FastAPI):
app = FastAPI(
title="FlowDeck",
version="7.43.0",
version="7.44.0",
docs_url="/docs",
redoc_url="/redoc",
lifespan=lifespan,
+69 -1
View File
@@ -2234,6 +2234,14 @@
.cmd-palette-overlay.open .cmd-palette{transform:translateY(0)}
.cmd-palette-input{width:100%;box-sizing:border-box;padding:16px 18px;background:transparent;border:none;outline:none;color:var(--text,#fff);font-size:16px}
.cmd-palette-input::placeholder{color:var(--text-dim,rgba(255,255,255,.4))}
.cmd-palette-tabs{display:flex;gap:6px;padding:10px 16px 0}
.cp-tab{background:none;border:1px solid var(--border,rgba(255,255,255,.12));color:var(--text-dim,rgba(255,255,255,.55));border-radius:999px;padding:4px 12px;font-size:12px;cursor:pointer}
.cp-tab.active{background:var(--bg-hover,#2a2a2a);color:var(--text,#eee);border-color:var(--accent,#2383e2)}
.cmd-palette-ai{padding:14px 18px;font-size:13.5px;line-height:1.55;white-space:pre-wrap;color:var(--text,#eee)}
.cmd-palette-ai .cp-cites{margin-top:10px;display:flex;flex-direction:column;gap:4px;font-size:12.5px;white-space:normal}
a.cp-cite{color:var(--accent,#529ffa);text-decoration:none}
a.cp-cite:hover{text-decoration:underline}
.cp-loading{color:var(--text-dim,rgba(255,255,255,.5))}
.cmd-palette-list{overflow-y:auto;border-top:1px solid var(--border,rgba(255,255,255,.06))}
.cmd-palette-group{display:flex;align-items:center;gap:8px;padding:10px 18px 4px;font-size:11px;font-weight:600;letter-spacing:.04em;text-transform:uppercase;color:var(--text-dim,rgba(255,255,255,.4))}
.cmd-palette-item{display:flex;align-items:center;gap:10px;padding:9px 18px;cursor:pointer;font-size:14px;color:var(--text,#fff)}
@@ -2255,6 +2263,10 @@
<input id="fd-cp-input" class="cmd-palette-input" type="text"
placeholder="Search pages, databases, or type a command…"
autocomplete="off" spellcheck="false">
<div class="cmd-palette-tabs" id="fd-cp-tabs" role="tablist">
<button type="button" class="cp-tab active" data-tab="pages">Pages</button>
<button type="button" class="cp-tab" data-tab="ai">✨ Réponses IA</button>
</div>
<div id="fd-cp-list" class="cmd-palette-list"></div>
<div class="cmd-palette-footer">
<span class="cpf-kbd"><b>↑</b> / <b>↓</b> navigate</span>
@@ -2275,7 +2287,7 @@
];
var overlay, input, list;
var state = { open:false, items:[], index:0, query:'', timer:null, actions:false };
var state = { open:false, items:[], index:0, query:'', timer:null, actions:false, tab:'pages', ai:null };
function esc(s){ return String(s==null?'':s).replace(/[&<>"']/g, function(c){ return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]; }); }
function highlight(text, q){
@@ -2299,6 +2311,7 @@
}
function render(){
if(state.tab==='ai'){ renderAi(); return; }
if(!state.open) return;
if(!state.items.length){
list.innerHTML = '<div class="cmd-palette-empty">No results for “'+esc(state.query)+'”</div>';
@@ -2335,6 +2348,7 @@
}
function runSearch(){
if(state.tab==='ai'){ runAi(); return; }
var q = input.value.trim();
state.query = q;
state.index = 0;
@@ -2359,6 +2373,50 @@
.catch(function(){ state.items=[]; render(); });
}
function runAi(){
var q = input.value.trim();
state.ai = null;
if(!q){ state.ai = {hint:1}; renderAi(); return; }
state.ai = {loading:1}; renderAi();
fetch('/api/v2/search/ask', {
method:'POST',
headers: {'Content-Type':'application/json', 'X-CSRF-Token': getCsrf()},
body: JSON.stringify({question: q})
}).then(function(r){ if(!r.ok) throw 0; return r.json(); })
.then(function(d){
if(state.tab!=='ai' || input.value.trim()!==q) return;
state.ai = d; renderAi();
})
.catch(function(){
if(state.tab!=='ai') return;
state.ai = {error:1}; renderAi();
});
}
function renderAi(){
var a = state.ai;
if(!a){ list.innerHTML=''; return; }
if(a.loading){ list.innerHTML='<div class="cmd-palette-ai"><span class="cp-loading">Recherche de la réponse…</span></div>'; return; }
if(a.error){ list.innerHTML='<div class="cmd-palette-ai">Demande impossible — réessayez.</div>'; return; }
if(a.hint){ list.innerHTML='<div class="cmd-palette-ai">Posez une question sur votre espace : la réponse cite vos pages.</div>'; return; }
var cits = a.citations || [], byId = {};
cits.forEach(function(c){ byId[String(c.id)] = c; });
// échappement AVANT injection des liens (ids = chiffres, type = [a-z]+)
var md = esc(a.answer_markdown || '')
.replace(/\[\[([a-z]+):(\d+)\]\]/g, function(m, type, id){
var c = byId[id];
var label = esc((c && (c.title || c.name)) || (type + ' #' + id));
var href = type === 'collection' ? '/db/' + id : '/pages/' + id;
return '<a class="cp-cite" href="' + href + '">' + label + '</a>';
})
.replace(/\*\*([^*]+)\*\*/g, '<b>$1</b>');
var cites = cits.map(function(c){
var href = c.type === 'collection' ? '/db/' + c.id : '/pages/' + c.id;
return '<a class="cp-cite" href="' + href + '">' + esc(c.title || c.name || 'page #' + c.id) + '</a>';
}).join('');
list.innerHTML = '<div class="cmd-palette-ai">' + md +
(cites ? '<div class="cp-cites"><b>Sources</b> ' + cites + '</div>' : '') + '</div>';
}
function open(){
if(state.open) return;
state.open=true; overlay.classList.add('open');
@@ -2413,6 +2471,16 @@
list = document.getElementById('fd-cp-list');
if(!overlay) return;
input.addEventListener('input', function(){ clearTimeout(state.timer); state.timer=setTimeout(runSearch, 150); });
var tabs = document.getElementById('fd-cp-tabs');
if(tabs) tabs.addEventListener('click', function(e){
var b = e.target && e.target.closest ? e.target.closest('.cp-tab') : null;
if(!b) return;
state.tab = b.getAttribute('data-tab');
var all = tabs.querySelectorAll('.cp-tab');
for(var i=0;i<all.length;i++) all[i].classList.toggle('active', all[i]===b);
state.index = 0;
runSearch();
});
document.addEventListener('keydown', onKey);
});
})();