fix(editor): AI composer 400, slash-menu arrows, drag handle, block outline, hint
FlowDeck CI / test (push) Failing after 14s
FlowDeck CI / docker (push) Skipped

- AI (space) -> 400 DeepSeek: fdAgent.generate now uses the clean no-tools
  /api/agent/generate endpoint instead of the tool-enabled conversation engine
  (which sends tools/tool_choice schemas DeepSeek rejects). Fixes the composer,
  Ask AI, AI meeting note and summarize flows.
- Slash menu arrows: selection highlight was invisible on open (nothing marked),
  so up/down appeared dead. Call _rs() on open + after filtering, and give the
  selected item an accent background + left bar. Verified with jsdom harness:
  Down/Up move selection, Enter applies.
- Block outline when selected: suppress focus rings (outline/box-shadow) on all
  editable blocks ([data-bid], .block-content, page title).
- Drag handle (6-dot): add .block-wrapper:hover/:focus-within .block-handle so
  it shows on hover for normal blocks (only column-wrappers showed it before).
- Empty-block hint: brighter (--text-secondary, opacity .75) so the
  "Press space for AI or / for commands" text is clearly visible on empty paras.
- VERSION + app.main -> 5.1.3; CHANGELOG updated.
- 271 tests pass; page renders 200; /api/agent/generate route registered;
  JS validated (node --check + jsdom).
This commit is contained in:
2026-09-07 11:35:24 -04:00
parent d6bb424021
commit 43aca1a1f7
6 changed files with 73 additions and 40 deletions
+34 -1
View File
@@ -1,6 +1,39 @@
# Changelog — FlowDeck
## v5.1.2 (2026-09-07) — Éditeur style Notion AI (hint, menu slash, AI inline)
## v5.1.3 (2026-09-07) — Corrections : AI inline (400), bloc vide, menu `/`, poignée, contour
> Résout les régressions du composer AI et de l'éditeur Notion-style v5.1.2.
### AI inline — erreur 400 DeepSeek (espace → AI)
- `fdAgent.generate` passait par l'engine à outils (`/conversations/{id}/run`) qui
envoie un schéma `tools`/`tool_choice` que DeepSeek rejette en **400 Bad Request**.
- Bascule de `generate` sur l'endpoint **`/api/agent/generate`** (sans outils, prompt
propre + contexte) : le composer AI, « Ask AI », la note de réunion et la synthèse
renvoient désormais un simple texte Markdown. Le bloc « 400 » disparaît.
### Bloc vide (placeholder)
- Le hint « Press 'space' for AI or '/' for commands » était rendu trop discret
(`--text-dim`). Il passe à `--text-secondary` avec opacité, plus lisible.
(Le texte n'apparaît que sur un paragraphe réellement vide — comportement Notion.)
### Slash menu — flèches ↑/↓
- La sélection n'était **pas visible** au démarrage (rien de surligné) → les flèches
semblaient inactives. `_rs()` est désormais appelé à l'ouverture et après filtrage,
et l'item sélectionné reçoit un fond accentué + liseré gauche clair. Testé en
jsdom : ↓/↑ déplacent la sélection, Entrée applique l'élément.
### Contour du bloc sélectionné
- Suppression des anneaux de focus (`outline`/`box-shadow`) sur tous les blocs
éditables (`[data-bid]`, `.block-content`, titre de page) quand sélectionné.
### Poignée de déplacement (⠿)
- La poignée 6 points restait invisible (seule `.block-column-wrapper:hover` la
montrait). Ajout de `.block-wrapper:hover`/`:focus-within .block-handle { opacity:1 }`
pour les blocs normaux.
### Tests / validation
- 271 tests pytest verts ; rendu édition 200 (hint/AIC/trigger présents) ;
route `/api/agent/generate` enregistrée ; JS validé (`node --check` + jsdom).
> Adapte le visuel des blocs à l'expérience Notion AI : hint sur bloc vide, menu
> `/` compact avec raccourcis + aperçu au survol, et composer AI inline
+1 -1
View File
@@ -1 +1 @@
5.1.2
5.1.3
+1 -1
View File
@@ -60,7 +60,7 @@ async def lifespan(_app: FastAPI):
app = FastAPI(
title="FlowDeck",
version="5.1.2",
version="5.1.3",
docs_url="/docs" if settings.log_level == "DEBUG" else None,
redoc_url=None,
lifespan=lifespan,
+2 -2
View File
@@ -238,7 +238,7 @@
const r=el.getBoundingClientRect();SM._e.style.display='flex';
SM._e.style.top=Math.max(4,(r.bottom+4>window.innerHeight-360)?r.top-364:r.bottom+4)+'px';
SM._e.style.left=Math.min(r.left,window.innerWidth-340)+'px';
setTimeout(()=>SM._in.focus(),10);SM._o=true;},
SM._rs();setTimeout(()=>SM._in.focus(),10);SM._o=true;},
close(){if(SM._e)SM._e.style.display='none';SM._o=false;SM._in&&(SM._in.value='');if(SM._pv)SM._pv.style.display='none';},
flt(){if(!SM._li)return;SM._it=[];const q=(SM._in.value||'').toLowerCase();SM._q=q;let h='';
for(const g of CMDS){let items=g.items;if(q)items=items.filter(c=>c.name.toLowerCase().includes(q)||c.id.includes(q));if(!items.length)continue;
@@ -248,7 +248,7 @@
<span class="slash-item-icon" style="width:26px;height:26px;display:flex;align-items:center;justify-content:center;font-size:15px;background:var(--bg-secondary);border-radius:4px;flex-shrink:0">${c.icon}</span>
<span class="slash-item-name" style="flex:1;font-weight:500">${c.name}</span>
<span class="slash-item-hint" style="font-size:12px;color:var(--text-dim);font-family:var(--font-mono);opacity:.7">${_SHORTCUT[c.id]||''}</span></div>`;});}
SM._li.innerHTML=h;},
SM._li.innerHTML=h;SM._rs();},
_kd(e){if(e.key==='ArrowDown'){e.preventDefault();SM._s=Math.min(SM._s+1,SM._it.length-1);SM._rs();}
else if(e.key==='ArrowUp'){e.preventDefault();SM._s=Math.max(SM._s-1,0);SM._rs();}
else if(e.key==='Enter'){e.preventDefault();const c=SM._it[SM._s];if(c)SM.sel(c.id);}
+13 -33
View File
@@ -457,44 +457,24 @@
self.setComposerText(t);
setTimeout(function(){ self.send(); }, 60);
};
// Headless text generation (no tools) via /agent/generate — the clean
// path used by "Ask AI" / "AI meeting note" / summarization. Sending a
// plain write request through the tool-enabled conversation engine sends
// a tools/tool_choice schema that some providers (e.g. DeepSeek) reject
// with a 400. This endpoint returns {ok, text, offline, model}.
window.fdAgent.generate = function(message, context){
message = (message || '').trim();
if(!message) return Promise.resolve('');
var payload = {message: message};
var payload = {prompt: message};
if(context && context.trim()) payload.context = String(context).slice(0, 20000);
return fetch('/api/agent/conversations', {
return fetch('/api/agent/generate', {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({title:'Agent'})
}).then(function(r){ return r.json(); }).then(function(conv){
var cid = conv && conv.id;
if(!cid) throw new Error('Impossible de créer une conversation agent');
return fetch('/api/agent/conversations/' + cid + '/run', {
method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(payload)
}).then(function(resp){
if(!resp.ok){ return resp.json().then(function(j){ throw new Error(j.detail || ('HTTP ' + resp.status)); }); }
var reader = resp.body.getReader();
var dec = new TextDecoder();
var buf = '', finalText = '', lastError = '';
function pump(){
return reader.read().then(function(res){
if(res.done){ if(lastError) throw new Error(lastError); return finalText; }
buf += dec.decode(res.value, {stream:true});
var parts = buf.split('\n\n');
buf = parts.pop();
for(var i=0;i<parts.length;i++){
var line = parts[i].replace(/^data:\s*/, '').trim();
if(!line) continue;
try{
var ev = JSON.parse(line);
if(ev.type === 'final') finalText = ev.content || '';
else if(ev.type === 'error') lastError = ev.message || 'Erreur agent';
}catch(e){}
}
return pump();
});
}
return pump();
});
body: JSON.stringify(payload)
}).then(function(resp){
return resp.json();
}).then(function(d){
if(d && d.ok){ return d.text || ''; }
throw new Error((d && d.error) ? d.error : 'Erreur IA');
});
};
},
+22 -2
View File
@@ -2243,6 +2243,10 @@ button.breadcrumb-current { max-width: 260px; }
.block-wrapper:hover {
background: rgba(0,0,0,0.02);
}
.block-wrapper:hover .block-handle,
.block-wrapper:focus-within .block-handle {
opacity: 1;
}
/* 6-dot drag handle */
.block-handle {
@@ -2332,7 +2336,8 @@ button.breadcrumb-current { max-width: 260px; }
}
.block-content.empty::before {
content: attr(data-placeholder);
color: var(--text-dim);
color: var(--text-secondary);
opacity: .75;
pointer-events: none;
font-style: normal;
}
@@ -4580,10 +4585,25 @@ button.breadcrumb-current { max-width: 260px; }
@keyframes ai-pulse { 0%,100%{opacity:.45} 50%{opacity:1} }
.slash-menu { flex-direction: column; }
.slash-menu .sm-footer:hover { background: var(--bg-hover); }
.slash-item.selected { background: var(--bg-hover); }
.slash-item.selected { background: rgba(35,131,226,.16); box-shadow: inset 2px 0 0 var(--accent,#2383E2); }
.slash-item.selected .slash-item-name { color: var(--text); }
.slash-item-hint { font-size: 12px; color: var(--text-dim); opacity: .7; }
.sm-preview { font-family: inherit; }
.sm-preview h2 { margin: 0 0 6px; }
.sm-preview .sm-pv-label { font-size: 11px; color: var(--text-dim); text-transform: uppercase; letter-spacing: .4px; margin: 8px 0 2px; }
#_aiC { font-family: inherit; }
/* No focus "contour" on selected/editable blocks */
.block-content:focus,
.block-content:focus-visible,
[data-bid]:focus,
[data-bid]:focus-visible,
.page-title-input:focus { outline: none !important; box-shadow: none !important; }
/* Drag handle: keep hover-visible but drop the invisible default outside gutter */
.block-handle { opacity: .0; }
.block-wrapper:hover .block-handle,
.block-wrapper:focus-within .block-handle { opacity: 1; }
.aici-result p { margin: 0 0 8px; }
.aici-result h1,.aici-result h2,.aici-result h3,.aici-result h4 { margin: 10px 0 6px; font-weight: 700; }
.aici-result h1{font-size:1.4em}.aici-result h2{font-size:1.25em}.aici-result h3{font-size:1.1em}.aici-result h4{font-size:1em}