feat(editor): Notion AI-style block UX (hint, compact slash menu, inline AI composer)
FlowDeck CI / test (push) Failing after 14s
FlowDeck CI / docker (push) Skipped

- Empty paragraph placeholder: "Press space for AI or / for commands".
- Slash menu redesigned: compact items (icon + name + shortcut #/##/### on
  right), friendly group labels, 'Close menu (esc)' footer, 'Type to search'
  input at bottom, and hover preview card (e.g. 'Our Values' H2 sample).
- Inline AI composer (AIC): space on empty block opens 'Edit with AI' pill;
  Enter -> agent generate with animated 'Brewing...' (stop button) -> framed
  markdown result + thumbs/thumbdown + 'Insert below' -> applyAIBlocks (md2b).
- CSS: ai-pulse animation, .aici-result styles, .sm-* tweaks; app.css cache.
- VERSION + app.main -> 5.1.2; CHANGELOG updated.
- 271 tests pass (no regression); editor renders 200 (hint/menu/AIC present);
  JS validated via node --check.
This commit is contained in:
2026-09-07 10:52:23 -04:00
parent 4dc06eb203
commit d6bb424021
5 changed files with 181 additions and 21 deletions
+24 -1
View File
@@ -1,6 +1,29 @@
# Changelog — FlowDeck
## v5.1.1 (2026-09-07) — Rendu Markdown naturel (éditeur + panneau Agent)
## v5.1.2 (2026-09-07) — Éditeur style Notion AI (hint, menu slash, AI inline)
> 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
> (« Edit with AI » → « Brewing… » → résultat encadré → « Insert below »).
### Bloc vide & commandes (Images 1 & 3)
- **Hint de bloc vide** : sur un paragraphe vide, le placeholder devient
« Press 'space' for AI or '/' for commands ».
- **Menu slash refait** : compact (icône + nom + raccourci `#`/`##`/`###`… à droite),
groupes renommés (« Basic blocks », « Media », « Data »), footer **« Close menu (esc) »**,
champ **« Type to search »** en bas, et **aperçu au survol** d'un item (carte
« Our Values » / exemple de rendu de bloc), positionnée à côté du menu.
### Composer AI inline (Images 2, 4, 5, 6)
- **Espace sur un bloc vide** ouvre le composer AI inline (pill « **Edit with AI** »).
- **Entrée** → l'agent génère : indicateur « **Brewing…** » animé (bouton ■ pour
stopper), puis **résultat encadré** (markdown rendu : titres, listes, gras…)
avec barre d'actions (👍/👎) et bouton **« Insert below »**.
- **Insert below** insère le contenu comme vrais blocs (via `applyAIBlocks` → `md2b`).
### Tests / validation
- 271 tests pytest verts (aucune régression) ; éditeur rendu 200 (hint, menu, AIC présents) ;
JS validé (`node --check`).
> L'agent FlowDeck et l'insertion dans les pages affichent désormais le markdown
> rendu (titre, gras, tableaux, code…) au lieu des caractères bruts `##`, `**`…
+1 -1
View File
@@ -1 +1 @@
5.1.1
5.1.2
+1 -1
View File
@@ -60,7 +60,7 @@ async def lifespan(_app: FastAPI):
app = FastAPI(
title="FlowDeck",
version="5.1.1",
version="5.1.2",
docs_url="/docs" if settings.log_level == "DEBUG" else None,
redoc_url=None,
lifespan=lifespan,
+136 -18
View File
@@ -181,31 +181,73 @@
});
}
const SM={_o:false,_i:-1,_s:0,_q:'',_it:[],
// ── Block type shortcuts (shown on the right of each slash item) ──
const _SHORTCUT={text:'T',heading_1:'#',heading_2:'##',heading_3:'###',heading_4:'####',bulleted_list:'•',numbered_list:'1.',to_do:'[ ]',toggle:'▸',quote:'"',callout:'!',table_of_contents:'TOC',math:'∑',columns:'‖',divider:'———',code:'<>',table:'|',image:'[img]',database:'DB'};
const _GROUP_LABEL={BASIC:'Basic blocks',MEDIA:'Media',DATA:'Data'};
// ── Hover preview card (matches Notion's visual hint) ──
function _previewHtml(blockId){
const p=function(inner,cls){return '<div class="slash-prev" style="padding:0">'+inner+'</div>';};
switch(blockId){
case 'heading_1': return p('<div style="font-size:24px;font-weight:700">Big section heading</div>');
case 'heading_2': return p('<div style="font-size:19px;font-weight:700;margin-bottom:8px">Our Values</div><div style="font-size:13px;line-height:1.6">• Ownership<br>• Altruism<br>• Integrity</div><div style="font-size:11px;color:var(--text-dim);margin-top:8px">Medium section heading</div>');
case 'heading_3': return p('<div style="font-size:16px;font-weight:700;margin-bottom:6px">Small section heading</div><div style="font-size:13px;color:var(--text-dim)">Body text goes here…</div >');
case 'heading_4': return p('<div style="font-size:14px;font-weight:700">Tiny section heading</div>');
case 'bulleted_list': return p('<div style="font-size:13px;line-height:1.9">• List item<br>• Another item<br>• Third item</div>');
case 'numbered_list': return p('<div style="font-size:13px;line-height:1.9">1. First<br>2. Second<br>3. Third</div>');
case 'to_do': return p('<div style="font-size:13px;line-height:1.9">☐ Todo item<br>☑ Checked todo<br>☐ Another</div>');
case 'toggle': return p('<div style="font-size:13px">▸ Toggle content</div>');
case 'quote': return p('<div style="border-left:3px solid var(--accent);padding-left:10px;font-size:13px;color:var(--text-dim)">A blockquote to highlight a quotation.</div>');
case 'callout': return p('<div style="background:var(--blue-bg);padding:12px;border-radius:6px;font-size:13px">💡 A callout draws attention.</div>');
case 'table_of_contents': return p('<div style="font-size:13px;color:var(--text-dim)">On this page</div><div style="font-size:12px;line-height:1.8">· Heading 1<br>· Heading 2<br>· Heading 3</div>');
case 'math': return p('<div style="font-size:15px;text-align:center">E = mc²</div>');
case 'columns': return p('<div style="display:flex;gap:8px"><div style="flex:1;border:1px solid var(--border);border-radius:4px;padding:8px;font-size:12px">col 1</div><div style="flex:1;border:1px solid var(--border);border-radius:4px;padding:8px;font-size:12px">col 2</div></div>');
case 'divider': return p('<hr style="border-top:1px solid var(--border);margin:6px 0">');
case 'code': return p('<pre style="background:#111;padding:10px;border-radius:6px;font-size:12px"><code>const x = 1;</code></pre>');
case 'table': return p('<div style="font-size:12px"><table style="width:100%;border-collapse:collapse"><tr><td style="border:1px solid var(--border);padding:4px">A</td><td style="border:1px solid var(--border);padding:4px">B</td></tr><tr><td style="border:1px solid var(--border);padding:4px">1</td><td style="border:1px solid var(--border);padding:4px">2</td></tr></table></div>');
case 'image': return p('<div style="border:1px solid var(--border);border-radius:6px;padding:14px;text-align:center;color:var(--text-dim);font-size:12px">[Image]</div>');
case 'database': return p('<div style="border:1px solid var(--border);border-radius:6px;padding:10px;font-size:12px">📊 Inline database table</div>');
default: return p('<div style="font-size:13px;color:var(--text-dim)">Text block</div>');
}
}
const SM={_o:false,_i:-1,_s:0,_q:'',_it:[],_pv:null,
init(){const e=document.getElementById('_slashMenu');if(!e)return;e.innerHTML='';
const inp=document.createElement('input');inp.type='text';inp.placeholder='Filter...';
inp.style.cssText='width:100%;padding:6px 10px;background:var(--bg-secondary);border:1px solid var(--border);border-radius:6px;color:var(--text-primary);font-size:14px;outline:none;box-sizing:border-box';
inp.oninput=()=>SM.flt();inp.onkeydown=ev=>SM._kd(ev);
const hdr=document.createElement('div');hdr.style.cssText='padding:8px;border-bottom:1px solid var(--border)';hdr.appendChild(inp);
const list=document.createElement('div');list.style.cssText='overflow-y:auto;max-height:340px';
const list=document.createElement('div');list.className='sm-list';list.style.cssText='overflow-y:auto;flex:1;min-height:120px;max-height:360px;padding:6px';
list.addEventListener('click',ev=>{const it=ev.target.closest('.slash-item');if(it){ev.preventDefault();SM.sel(it.dataset.sid);}});
e.appendChild(hdr);e.appendChild(list);
SM._e=e;SM._in=inp;SM._li=list;
list.addEventListener('mouseover',ev=>{const it=ev.target.closest('.slash-item');if(it)SM._hover(it);});
const footer=document.createElement('div');footer.className='sm-footer';footer.style.cssText='display:flex;align-items:center;justify-content:space-between;padding:7px 12px;border-top:1px solid var(--border);font-size:12px;color:var(--text-dim);cursor:pointer';
footer.innerHTML='<span style="display:flex;align-items:center;gap:8px"><span style="opacity:.6">⌘</span> Close menu</span><span style="color:var(--text-dim);opacity:.7">esc</span>';
footer.onclick=()=>SM.close();
const inp=document.createElement('input');inp.type='text';inp.placeholder='Type to search';
inp.style.cssText='width:100%;padding:9px 12px;background:var(--bg-secondary);border:none;border-top:1px solid var(--border);color:var(--text-primary);font-size:14px;outline:none;box-sizing:border-box';
inp.oninput=()=>SM.flt();inp.onkeydown=ev=>SM._kd(ev);
e.appendChild(list);e.appendChild(footer);e.appendChild(inp);
SM._e=e;SM._in=inp;SM._li=list;SM._f=footer;
document.addEventListener('mousedown',ev=>{if(!e.contains(ev.target))SM.close();});
},
_hover(it){ if(!it) return; const id=it.dataset.sid; if(!id) return;
if(!SM._pv){ const d=document.createElement('div'); d.className='sm-preview'; d.style.cssText='position:fixed;z-index:1000;background:var(--bg-modal);border:1px solid var(--border-light);border-radius:8px;box-shadow:0 8px 24px rgba(0,0,0,.5);padding:12px;width:220px;pointer-events:none;'; document.body.appendChild(d); SM._pv=d; }
SM._pv.innerHTML=_previewHtml(id);
const r=SM._e.getBoundingClientRect(); const ir=it.getBoundingClientRect();
let left = r.right + 8; let top = ir.top - 12;
if(left+220 > window.innerWidth-8) left = r.left - 228;
top = Math.max(8, Math.min(top, window.innerHeight - SM._pv.offsetHeight - 8));
SM._pv.style.left=left+'px'; SM._pv.style.top=top+'px'; SM._pv.style.display='block';
},
open(i,el){if(!SM._e)SM.init();SM.close();SM._i=i;SM._s=0;SM._in.value='';SM.flt();
const r=el.getBoundingClientRect();SM._e.style.display='block';
SM._e.style.top=Math.max(4,(r.bottom+4>window.innerHeight-340)?r.top-344:r.bottom+4)+'px';
SM._e.style.left=Math.min(r.left,window.innerWidth-324)+'px';
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;},
close(){if(SM._e)SM._e.style.display='none';SM._o=false;SM._in&&(SM._in.value='');},
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;
h+=`<div style="padding:6px 12px 4px;font-size:11px;font-weight:600;color:var(--text-dim);text-transform:uppercase;letter-spacing:.5px">${g.name}</div>`;
h+=`<div class="sm-group" style="padding:6px 10px 3px;font-size:11px;font-weight:600;color:var(--text-dim);text-transform:uppercase;letter-spacing:.4px">${_GROUP_LABEL[g.name]||g.name}</div>`;
items.forEach(c=>{SM._it.push(c);
h+=`<div class="slash-item" data-sid="${c.id}" style="display:flex;align-items:center;gap:10px;padding:8px 12px;font-size:14px;color:var(--text-primary);cursor:pointer">
<span style="width:28px;height:28px;display:flex;align-items:center;justify-content:center;font-size:16px;background:var(--bg-secondary);border-radius:4px;flex-shrink:0">${c.icon}</span>
<span style="flex:1;font-weight:500">${c.name}</span></div>`;});}
h+=`<div class="slash-item" data-sid="${c.id}" style="display:flex;align-items:center;gap:10px;padding:7px 10px;font-size:14px;color:var(--text-primary);cursor:pointer;border-radius:5px">
<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;},
_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();}
@@ -215,6 +257,77 @@
sel(id){if(window.E)window.E.applySlash(id);SM.close();},
get isOpen(){return SM._o;}};
// ═══════════ Inline AI composer (Notion AI-style) ═══════════
// Space on an empty paragraph → "Edit with AI" pill → Enter → "Brewing…"
// → framed result → "Insert below" → applyAIBlocks.
const AIC={_open:false,_idx:-1,_stage:'',_html:'',
ROOT:null,
mk(){ if(this.ROOT) return this.ROOT;
const d=document.createElement('div'); d.id='_aiC'; d.style.cssText='position:fixed;z-index:1200;width:420px;max-width:92vw;display:none';
document.body.appendChild(d); this.ROOT=d; return d; },
open(idx){ const self=this; this._idx=idx; this._stage='input'; this.mk();
const r=this._anchorRect(idx); this.ROOT.style.left=Math.max(8,Math.min(r.left,window.innerWidth-440))+ 'px';
this.ROOT.style.top=Math.max(8,(r.bottom+6>window.innerHeight-260)?r.top-180:r.bottom+6)+'px';
this.paint(); this.ROOT.style.display='block'; this._open=true; this._focusIngest();
document.addEventListener('mousedown',function onDoc(ev){ if(!self.ROOT.contains(ev.target)){ self.close(); document.removeEventListener('mousedown',onDoc); } });
},
close(){ if(this.ROOT)this.ROOT.style.display='none'; this._open=false; this._html=''; if(window.E)window.E.dirty=true; this._stage=''; },
_anchorRect(idx){ const E=window.E; if(!E||!E.blocks)return{x:Math.max(24,window.innerWidth/2-210),bottom:window.innerHeight/2};
const b=E.blocks[idx]; const el=E.getEl(b?b.id:null); if(el){const r=el.getBoundingClientRect();return r;} return {left:24,top:120,bottom:180}; },
_focusIngest(){ const i=this.ROOT.querySelector('.aici-input'); if(i){i.focus();} },
_escTag(tag,s){return '<'+tag+'>'+s+'</'+tag+'>';},
_mdToHtml(md){ // compact block render (headings, lists, bold, paragraphs)
const lines=String(md||'').split(/\r?\n/); let out=[]; let i=0;
const inline=function(t){return esc(t).replace(/`([^`]+)`/g,'<code>$1</code>').replace(/\*\*([^*]+)\*\*/g,'<strong>$1</strong>').replace(/\*([^*]+)\*/g,'<em>$1</em>').replace(/\[([^\]]+)\]\((https?:[^)\s]+)\)/g,'<a href="$2" target="_blank">$1</a>');};
while(i<lines.length){ const t=lines[i].replace(/\r$/,''); const s=t.trim();
if(!s){i++;continue;}
if(/^#{1,4}\s/.test(s)){ const lvl=s.match(/^(#+)\s/)[1].length; out.push('<h'+lvl+'>'+inline(s.replace(/^#+\s+/,''))+'</h'+lvl+'>'); i++; continue; }
if(/^[-*+] /.test(s)){ const items=[]; while(i<lines.length&&/^[-*+] /.test(lines[i].trim())){items.push('<li>'+inline(lines[i].trim().replace(/^[-*+] /,''))+'</li>'); i++;} out.push('<ul>'+items.join('')+'</ul>'); continue; }
if(/^\d+\. /.test(s)){ const items=[]; while(i<lines.length&&/^\d+\. /.test(lines[i].trim())){items.push('<li>'+inline(lines[i].trim().replace(/^\d+\. /,''))+'</li>'); i++;} out.push('<ol>'+items.join('')+'</ol>'); continue; }
if(s.startsWith('> ')){ out.push('<blockquote>'+inline(s.replace(/^> /,''))+'</blockquote>'); i++; continue; }
out.push('<p>'+inline(s)+'</p>'); i++;
}
return out.join(''); },
paint(){ const self=this; const h=this.ROOT;
if(this._stage==='input'){
h.innerHTML='<div style="display:flex;align-items:center;gap:10px;background:var(--bg-modal);border:1px solid var(--border-light);border-radius:999px;padding:8px 14px;box-shadow:0 8px 30px rgba(0,0,0,.45)">'
+'<span style="font-size:18px;color:var(--text-primary)">✨</span>'
+'<input class="aici-input" type="text" placeholder="Edit with AI" autocomplete="off" spellcheck="false" style="flex:1;background:none;border:none;outline:none;color:var(--text);font-size:14px">'
+'<button class="aici-send" title="Generate" style="background:var(--bg-secondary);border:none;border-radius:50%;width:26px;height:26px;color:var(--text);cursor:pointer;font-size:14px">↑</button>'
+'</div>';
const inp=h.querySelector('.aici-input'); if(inp){inp.focus(); const go=function(){const v=inp.value.trim(); if(v){self._idx=self._idx>=0?self._idx:0; self._run(v);} }; inp.onkeydown=function(ev){ if(ev.key==='Enter'){ev.preventDefault();go();} else if(ev.key==='Escape'){ev.preventDefault();self.close();} }; h.querySelector('.aici-send').onclick=go; }
} else if(this._stage==='brewing'){
h.innerHTML='<div style="display:flex;align-items:center;gap:12px;background:var(--bg-modal);border:1px solid var(--border-light);border-radius:14px;padding:12px 16px;box-shadow:0 8px 30px rgba(0,0,0,.45)">'
+'<span style="font-size:20px">✨</span><span style="color:var(--text-dim);font-size:14px;animation:ai-pulse 1.2s ease-in-out infinite">Brewing…</span>'
+'<button class="aici-stop" title="Stop" style="margin-left:auto;background:var(--bg-secondary);border:none;border-radius:6px;width:26px;height:26px;color:var(--text);cursor:pointer">■</button></div>';
h.querySelector('.aici-stop').onclick=function(){ self.close(); if(window.fdAgent&&window.fdAgent.cancel)window.fdAgent.cancel(); };
} else if(this._stage==='result'){
h.innerHTML='<div style="background:var(--bg-modal);border:1px solid var(--border-light);border-radius:14px;box-shadow:0 10px 40px rgba(0,0,0,.5);overflow:hidden">'
+'<div class="aici-result" style="padding:14px 16px;font-size:14px;line-height:1.6;color:var(--text);max-height:300px;overflow-y:auto">'+this._html+'</div>'
+'<div style="display:flex;align-items:center;gap:6px;padding:8px 12px;border-top:1px solid var(--border)">'
+'<button class="aici-fb" data-fb="up" title="Good" style="background:none;border:none;color:var(--text-dim);cursor:pointer">👍</button>'
+'<button class="aici-fb" data-fb="down" title="Bad" style="background:none;border:none;color:var(--text-dim);cursor:pointer">👎</button>'
+'<span style="flex:1"></span>'
+'<button class="aici-insert" style="background:var(--accent,#2383E2);color:#fff;border:none;border-radius:8px;padding:6px 16px;font-size:13px;cursor:pointer">Insert below</button>'
+'</div></div>';
const self2=this; h.querySelector('.aici-insert').onclick=function(){ self2.insert(); }; h.querySelectorAll('.aici-fb').forEach(function(b){ b.onclick=function(){ self2.toast(b.dataset.fb==='up'?'👍 Merci':'👎 Enregistré'); }; });
}
},
_run(prompt){ const self=this; this._stage='brewing'; this.paint();
const E=window.E; const ctx=(typeof window.fdGetPageContext==='function')?String(window.fdGetPageContext()||''):'';
if(!(window.fdAgent&&window.fdAgent.generate)){ this._err('Agent IA non configuré'); return; }
window.fdAgent.generate(prompt, ctx).then(function(text){
self._html=self._mdToHtml(text||''); self._stage='result'; self.paint();
}).catch(function(err){ self._err((err&&err.message)?err.message:'Erreur'); });
},
_err(msg){ this._html='<p style="color:#e5484d">⚠ '+esc(msg)+'</p>'; this._stage='result'; this.paint(); },
insert(){ const E=window.E; if(!E||typeof E.applyAIBlocks!=='function')return; E.sync();
const result=this._resultText(); if(!result)return; E.applyAIBlocks(result); E.dirty=true; E.autoSave(); this.close(); },
_resultText(){ const el=this.ROOT&&this.ROOT.querySelector('.aici-result'); return el?el.textContent:''; },
toast(m){ if(window.showToast)window.showToast(m,'info'); }
};
window.AIC=AIC;
function renderBlock(b,idx){
if(b.type==='meeting')return renderMeetingBlock(b,idx);
if(b.type==='table')return renderTableBlock(b,idx);
@@ -232,7 +345,7 @@
return `<div class="block-wrapper"><div class="block-content block-embed" style="text-align:center;padding:60px 20px;"><div style="font-size:48px;margin-bottom:12px;">{{ fd_icon("paperclip",14) }}</div><div style="font-size:16px;font-weight:500;color:var(--text-primary);margin-bottom:4px;">${esc(b.file_name||'File')}</div><div style="font-size:12px;color:var(--text-tertiary);margin-bottom:16px;">${szStr} · ${esc(b.file_mime||'')}</div><a href="${b.src||''}" download style="display:inline-block;padding:8px 20px;background:var(--accent);color:#fff;text-decoration:none;border-radius:8px;font-size:13px;font-weight:500;">⬇ Download</a></div></div>`;
}
}
const ph=b.type==='heading_1'?'Heading 1':b.type==='heading_2'?'Heading 2':b.type==='heading_3'?'Heading 3':b.type==='heading_4'?'Heading 4':b.type==='bulleted_list'?'List':b.type==='numbered_list'?'List':b.type==='to_do'?'To-do':b.type==='toggle'?'Toggle list':b.type==='quote'?'Empty quote':b.type==='code'?'Type code...':b.type==='callout'?'Type something...':'Press \'/\' for commands...';
const ph=b.type==='heading_1'?'Heading 1':b.type==='heading_2'?'Heading 2':b.type==='heading_3'?'Heading 3':b.type==='heading_4'?'Heading 4':b.type==='bulleted_list'?'List':b.type==='numbered_list'?'List':b.type==='to_do'?'To-do':b.type==='toggle'?'Toggle list':b.type==='quote'?'Empty quote':b.type==='code'?'Type code...':b.type==='callout'?'Type something...':(b.type==='paragraph'&&!b.content?'Press \'space\' for AI or \'/\' for commands':'Press \'/\' for commands...');
let inner='';
if(b.type==='to_do')inner=`<input type="checkbox" class="todo-checkbox" ${b.checked?'checked':''} onchange="E._doToggle('${b.id}',this.checked)"><div data-bid="${b.id}" contenteditable="true" data-placeholder="${ph}" spellcheck="false">${mdEsc(b.content)}</div>`;
else if(b.type==='toggle')inner=`<div class="toggle-title-row"><button class="toggle-chevron ${b.expanded?'open':''}" onclick="E._doToggleExpand('${b.id}')">▶</button><div class="toggle-title" data-bid="${b.id}" contenteditable="true" data-placeholder="${ph}" spellcheck="false">${mdEsc(b.content)}</div></div><div class="toggle-children" ${(b.expanded===false)?'style="display:none"':''}>${(b.children||[]).map((ch)=>renderNestedBlock(ch)).join('')}</div>`;
@@ -980,7 +1093,12 @@ applyAIBlocks(text){
return;
}
if(e.key.length===1||e.key==='Backspace'||e.key==='Delete'){const i2=idx;setTimeout(()=>{const e2=this.getEl(this.blocks[i2]?.id);if(e2&&e2.textContent?.trim()==='/'&&!SM.isOpen)SM.open(i2,e2);},15);}
if(e.key===' '){const i2=idx;setTimeout(()=>this.checkMd(i2),15);}
if(e.key===' '){
if(block.type==='paragraph'&&!(block.content||'').trim()&&!(el.textContent||'').trim()&&!SM.isOpen&&window.AIC){
e.preventDefault(); window.AIC.open(idx); return;
}
const i2=idx;setTimeout(()=>this.checkMd(i2),15);
}
if(e.key==='Enter'){
if(e.shiftKey){this.dirty=true;this.autoSave();return;}
e.preventDefault();
+19
View File
@@ -4575,3 +4575,22 @@ button.breadcrumb-current { max-width: 260px; }
.ftable-editor .ftable-rowbar { opacity: 1; }
.ftable-editor .ftable-addcol { opacity: 1; }
}
/* ── v5.2.0: Notion AI-style block editor ── */
@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-hint { font-size: 12px; color: var(--text-dim); opacity: .7; }
.sm-preview { font-family: inherit; }
#_aiC { font-family: inherit; }
.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}
.aici-result ul,.aici-result ol { margin: 0 0 8px; padding-left: 22px; }
.aici-result li { margin: 2px 0; }
.aici-result code { background: rgba(127,127,127,.18); border-radius: 4px; padding: 1px 5px; font-size: .9em; font-family: ui-monospace,SFMono-Regular,Menlo,monospace; }
.aici-result blockquote { border-left: 3px solid var(--accent,#2383E2); margin: 8px 0; padding: 2px 12px; color: var(--text-dim); }
.aici-result a { color: var(--accent,#2383E2); }
.aici-fb:disabled { opacity:.4; cursor:default; }
@media (max-width:768px){ #_aiC { width: 92vw; } }