feat: palette Ctrl+K — onglets Pages / ✨ Réponses IA (v7.44.0)
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:
@@ -1,5 +1,28 @@
|
|||||||
# Changelog - FlowDeck
|
# Changelog - FlowDeck
|
||||||
|
|
||||||
|
## v7.44.0 (2026-10-01) — Palette Ctrl+K : onglets Pages / ✨ Réponses IA
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **Palette `Ctrl+K` = 2 onglets** (markup + CSS + wiring dans l'IIFE) :
|
||||||
|
· **Pages** — comportement inchangé (recherche `/api/search` + actions).
|
||||||
|
· **✨ Réponses IA** — `POST /api/v2/search/ask` (debounce 150 ms,
|
||||||
|
CSRF via `getCsrf()`, gardes staleness onglet+requête) → rendu de
|
||||||
|
`answer_markdown` : **échappement AVANT injection**, `[[fdpage:ID]]` →
|
||||||
|
lien citation (ids = chiffres, type = `[a-z]+` — pas d'injection),
|
||||||
|
`**gras**`, bloc « Sources » avec liens `/pages/{id}` · `/db/{id}`.
|
||||||
|
États : hint / chargement / erreur.
|
||||||
|
- **Gate E2E étendu** (`gate A20 palette`) : ouverture → clic onglet IA →
|
||||||
|
réponse non-échaffée affichée (le backend tourne en extractif ou LLM —
|
||||||
|
probe : 200 en 1,6 s, 8 citations).
|
||||||
|
- **Reporté (backend absent)** : onglet `Fichiers` — `/api/search` ne
|
||||||
|
renvoie que `pages`/`collections` → endpoint à créer d'abord (ROADMAP).
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
|
||||||
|
- Un 401 transitoire sur le 1er ask d'un run E2E a été observé (session
|
||||||
|
fraîche) — non reproductible ensuite ; à surveiller si ça revient.
|
||||||
|
|
||||||
## v7.43.0 (2026-10-01) — 🎉 A20 TERMINÉ : Alpine en build CSP, `unsafe-eval` retiré
|
## v7.43.0 (2026-10-01) — 🎉 A20 TERMINÉ : Alpine en build CSP, `unsafe-eval` retiré
|
||||||
|
|
||||||
### Changed (la bascule A20 phase 3)
|
### Changed (la bascule A20 phase 3)
|
||||||
|
|||||||
+1
-1
@@ -987,7 +987,7 @@ Détails livrés :
|
|||||||
- [x] **Migrations 25** — `semantic_embeddings`, `semantic_index_state`, colonne `pages.search_excluded`
|
- [x] **Migrations 25** — `semantic_embeddings`, `semantic_index_state`, colonne `pages.search_excluded`
|
||||||
- [x] **Tests** — `tests/test_v69_search_ask.py` (**24 tests** : migration, chunk/overlap, déterminisme/norme, cosinus, index idempotent, exclusion, purge, rappel vectoriel partiel, hybride keyword/auth/400/pagination/isolation ACL/exclusion/collections, ask citations/auth/400/cache/ACL/rate-limit, index-status)
|
- [x] **Tests** — `tests/test_v69_search_ask.py` (**24 tests** : migration, chunk/overlap, déterminisme/norme, cosinus, index idempotent, exclusion, purge, rappel vectoriel partiel, hybride keyword/auth/400/pagination/isolation ACL/exclusion/collections, ask citations/auth/400/cache/ACL/rate-limit, index-status)
|
||||||
- [x] **Version** — 6.9.0 (`VERSION` + `app/main.py`) · `ruff check` OK
|
- [x] **Version** — 6.9.0 (`VERSION` + `app/main.py`) · `ruff check` OK
|
||||||
- [ ] **UI** — palette `Ctrl+K` onglets `Pages / Fichiers / ✨ Réponses IA` (reporté : backend livré, frontend en follow-up)
|
- [x] **UI** — palette `Ctrl+K` : **onglets `Pages / ✨ Réponses IA`** livrés (v7.44.0) — onglet IA = `POST /api/v2/search/ask` (CSRF via `getCsrf()`, debounce 150 ms, échappement AVANT injection des liens `[[fdpage:ID]]` → citations cliquables + bloc « Sources ») ; gate E2E étendu (onglet cliqué → réponse non-échappée). **Reporté** : onglet `Fichiers` — aucun backend (`/api/search` ne renvoie que `pages`/`collections`) → à créer d'abord.
|
||||||
|
|
||||||
### v7.0.0 — Automations v2 + Workers ✅ (2026-09-28)
|
### v7.0.0 — Automations v2 + Workers ✅ (2026-09-28)
|
||||||
> **Objectif** : automatiser au-delà du if-this-then-that + custom code sandboxé.
|
> **Objectif** : automatiser au-delà du if-this-then-that + custom code sandboxé.
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
# WORKLOAD — FlowDeck Notion Clone
|
# WORKLOAD — FlowDeck Notion Clone
|
||||||
|
|
||||||
> **Début**: 2026-07-08 | **Version**: v7.43.0 (**A20 TERMINÉ** : bascule Alpine CSP + unsafe-eval retiré de la CSP) | **Statut**: EN COURS 🔄
|
> **Début**: 2026-07-08 | **Version**: v7.44.0 (UI palette : onglets Pages / ✨ Réponses IA — POST /api/v2/search/ask rendu + sources citées) | **Statut**: EN COURS 🔄
|
||||||
> **Cible**: parité Notion + intégration forge · **Follow-ups v7.3 livrés**: sidebar teamspaces, notif `page.updated`, charts `number` + dashboards multi-DB, unfurl forge, UI Settings → Audit — voir `ROADMAP.md § v7.3.0`
|
> **Cible**: parité Notion + intégration forge · **Follow-ups v7.3 livrés**: sidebar teamspaces, notif `page.updated`, charts `number` + dashboards multi-DB, unfurl forge, UI Settings → Audit — voir `ROADMAP.md § v7.3.0`
|
||||||
|
|
||||||
## Avancement Global
|
## Avancement Global
|
||||||
|
|||||||
+1
-1
@@ -185,7 +185,7 @@ async def lifespan(_app: FastAPI):
|
|||||||
|
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title="FlowDeck",
|
title="FlowDeck",
|
||||||
version="7.43.0",
|
version="7.44.0",
|
||||||
docs_url="/docs",
|
docs_url="/docs",
|
||||||
redoc_url="/redoc",
|
redoc_url="/redoc",
|
||||||
lifespan=lifespan,
|
lifespan=lifespan,
|
||||||
|
|||||||
+69
-1
@@ -2234,6 +2234,14 @@
|
|||||||
.cmd-palette-overlay.open .cmd-palette{transform:translateY(0)}
|
.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{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-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-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-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)}
|
.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"
|
<input id="fd-cp-input" class="cmd-palette-input" type="text"
|
||||||
placeholder="Search pages, databases, or type a command…"
|
placeholder="Search pages, databases, or type a command…"
|
||||||
autocomplete="off" spellcheck="false">
|
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 id="fd-cp-list" class="cmd-palette-list"></div>
|
||||||
<div class="cmd-palette-footer">
|
<div class="cmd-palette-footer">
|
||||||
<span class="cpf-kbd"><b>↑</b> / <b>↓</b> navigate</span>
|
<span class="cpf-kbd"><b>↑</b> / <b>↓</b> navigate</span>
|
||||||
@@ -2275,7 +2287,7 @@
|
|||||||
];
|
];
|
||||||
|
|
||||||
var overlay, input, list;
|
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 {'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]; }); }
|
function esc(s){ return String(s==null?'':s).replace(/[&<>"']/g, function(c){ return {'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]; }); }
|
||||||
function highlight(text, q){
|
function highlight(text, q){
|
||||||
@@ -2299,6 +2311,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function render(){
|
function render(){
|
||||||
|
if(state.tab==='ai'){ renderAi(); return; }
|
||||||
if(!state.open) return;
|
if(!state.open) return;
|
||||||
if(!state.items.length){
|
if(!state.items.length){
|
||||||
list.innerHTML = '<div class="cmd-palette-empty">No results for “'+esc(state.query)+'”</div>';
|
list.innerHTML = '<div class="cmd-palette-empty">No results for “'+esc(state.query)+'”</div>';
|
||||||
@@ -2335,6 +2348,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function runSearch(){
|
function runSearch(){
|
||||||
|
if(state.tab==='ai'){ runAi(); return; }
|
||||||
var q = input.value.trim();
|
var q = input.value.trim();
|
||||||
state.query = q;
|
state.query = q;
|
||||||
state.index = 0;
|
state.index = 0;
|
||||||
@@ -2359,6 +2373,50 @@
|
|||||||
.catch(function(){ state.items=[]; render(); });
|
.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(){
|
function open(){
|
||||||
if(state.open) return;
|
if(state.open) return;
|
||||||
state.open=true; overlay.classList.add('open');
|
state.open=true; overlay.classList.add('open');
|
||||||
@@ -2413,6 +2471,16 @@
|
|||||||
list = document.getElementById('fd-cp-list');
|
list = document.getElementById('fd-cp-list');
|
||||||
if(!overlay) return;
|
if(!overlay) return;
|
||||||
input.addEventListener('input', function(){ clearTimeout(state.timer); state.timer=setTimeout(runSearch, 150); });
|
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);
|
document.addEventListener('keydown', onKey);
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"openapi": "3.1.0",
|
"openapi": "3.1.0",
|
||||||
"info": {
|
"info": {
|
||||||
"title": "FlowDeck",
|
"title": "FlowDeck",
|
||||||
"version": "7.43.0"
|
"version": "7.44.0"
|
||||||
},
|
},
|
||||||
"paths": {
|
"paths": {
|
||||||
"/auth/register": {
|
"/auth/register": {
|
||||||
|
|||||||
@@ -164,4 +164,23 @@ test('gate A20 : palette Ctrl+K (Alpine + recherche GET)', async ({ page }) => {
|
|||||||
return !ov || !ov.classList.contains('open');
|
return !ov || !ov.classList.contains('open');
|
||||||
});
|
});
|
||||||
expect(closed).toBe(true);
|
expect(closed).toBe(true);
|
||||||
|
|
||||||
|
// onglet ✨ Réponses IA : POST /api/v2/search/ask (extractif hors-LM)
|
||||||
|
await page.keyboard.press('Control+k');
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
await page.click('.cp-tab[data-tab="ai"]');
|
||||||
|
await page.waitForSelector('.cmd-palette-ai', { timeout: 10000 });
|
||||||
|
await page.waitForFunction(
|
||||||
|
() => {
|
||||||
|
const el = document.querySelector('.cmd-palette-ai');
|
||||||
|
return el && !el.querySelector('.cp-loading');
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
{ timeout: 15000 }
|
||||||
|
);
|
||||||
|
const ai = await page.evaluate(() => {
|
||||||
|
const el = document.querySelector('.cmd-palette-ai');
|
||||||
|
return { text: ((el && el.textContent) || '').trim() };
|
||||||
|
});
|
||||||
|
expect(ai.text.length).toBeGreaterThan(5);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user