fix(editor): AI composer 400, slash-menu arrows, drag handle, block outline, hint
- 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:
+34
-1
@@ -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
@@ -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,
|
||||
|
||||
@@ -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);}
|
||||
|
||||
@@ -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
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user