diff --git a/CHANGELOG.md b/CHANGELOG.md index a9fd693..2c3822f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 `##`, `**`… diff --git a/VERSION b/VERSION index 3bff059..1b47e8f 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -5.1.1 \ No newline at end of file +5.1.2 \ No newline at end of file diff --git a/app/main.py b/app/main.py index 02c56a4..030b285 100644 --- a/app/main.py +++ b/app/main.py @@ -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, diff --git a/app/templates/_page_editor_scripts.html b/app/templates/_page_editor_scripts.html index 99eb179..27819e9 100644 --- a/app/templates/_page_editor_scripts.html +++ b/app/templates/_page_editor_scripts.html @@ -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 '
'+inner+'
';}; + switch(blockId){ + case 'heading_1': return p('
Big section heading
'); + case 'heading_2': return p('
Our Values
• Ownership
• Altruism
• Integrity
Medium section heading
'); + case 'heading_3': return p('
Small section heading
Body text goes here…
'); + case 'heading_4': return p('
Tiny section heading
'); + case 'bulleted_list': return p('
• List item
• Another item
• Third item
'); + case 'numbered_list': return p('
1. First
2. Second
3. Third
'); + case 'to_do': return p('
☐ Todo item
☑ Checked todo
☐ Another
'); + case 'toggle': return p('
▸ Toggle content
'); + case 'quote': return p('
A blockquote to highlight a quotation.
'); + case 'callout': return p('
💡 A callout draws attention.
'); + case 'table_of_contents': return p('
On this page
· Heading 1
· Heading 2
· Heading 3
'); + case 'math': return p('
E = mc²
'); + case 'columns': return p('
col 1
col 2
'); + case 'divider': return p('
'); + case 'code': return p('
const x = 1;
'); + case 'table': return p('
AB
12
'); + case 'image': return p('
[Image]
'); + case 'database': return p('
📊 Inline database table
'); + default: return p('
Text block
'); + } + } + + 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='⌘ Close menuesc'; + 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+=`
${g.name}
`; + h+=`
${_GROUP_LABEL[g.name]||g.name}
`; items.forEach(c=>{SM._it.push(c); - h+=`
- ${c.icon} - ${c.name}
`;});} + h+=`
+ ${c.icon} + ${c.name} + ${_SHORTCUT[c.id]||''}
`;});} 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+'';}, + _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,'$1').replace(/\*\*([^*]+)\*\*/g,'$1').replace(/\*([^*]+)\*/g,'$1').replace(/\[([^\]]+)\]\((https?:[^)\s]+)\)/g,'$1');}; + while(i'+inline(s.replace(/^#+\s+/,''))+''); i++; continue; } + if(/^[-*+] /.test(s)){ const items=[]; while(i'+inline(lines[i].trim().replace(/^[-*+] /,''))+''); i++;} out.push(''); continue; } + if(/^\d+\. /.test(s)){ const items=[]; while(i'+inline(lines[i].trim().replace(/^\d+\. /,''))+''); i++;} out.push('
    '+items.join('')+'
'); continue; } + if(s.startsWith('> ')){ out.push('
'+inline(s.replace(/^> /,''))+'
'); i++; continue; } + out.push('

'+inline(s)+'

'); i++; + } + return out.join(''); }, + paint(){ const self=this; const h=this.ROOT; + if(this._stage==='input'){ + h.innerHTML='
' + +'✨' + +'' + +'' + +'
'; + 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='
' + +'✨Brewing…' + +'
'; + h.querySelector('.aici-stop').onclick=function(){ self.close(); if(window.fdAgent&&window.fdAgent.cancel)window.fdAgent.cancel(); }; + } else if(this._stage==='result'){ + h.innerHTML='
' + +'
'+this._html+'
' + +'
' + +'' + +'' + +'' + +'' + +'
'; + 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='

⚠ '+esc(msg)+'

'; 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 `
{{ fd_icon("paperclip",14) }}
${esc(b.file_name||'File')}
${szStr} · ${esc(b.file_mime||'')}
⬇ Download
`; } } - 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=`
${mdEsc(b.content)}
`; else if(b.type==='toggle')inner=`
${mdEsc(b.content)}
${(b.children||[]).map((ch)=>renderNestedBlock(ch)).join('')}
`; @@ -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(); diff --git a/static/css/app.css b/static/css/app.css index 094dc3c..3757073 100644 --- a/static/css/app.css +++ b/static/css/app.css @@ -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; } }