Editeur: - sync() lisait el.textContent qui supprime les <br> : ajout de gt() (innerText) utilise par sync(), tableaux (toggle/columns) et _resultText. - Scission sur Entree + limites haut/bas de bloc via splitCaret() (marqueur temporaire au curseur), plus de perte de fin de bloc multi-lignes. Agent: - fetch_provider_models() valide la liste nvidia : filtre des modeles non-chat (nom) puis probe reelle /chat/completions (6 paralleles, 6s), seuls les modeles 2xx restent. 429 conserve (route, donc utilisable). - Repli sur la liste filtree si toutes les validations echouent. - tests/test_llm_config.py : 5 cas (catalogue mock, 404 chat, autres providers non valides, chute de securite, offline). 276 tests passent.
1364 lines
107 KiB
HTML
1364 lines
107 KiB
HTML
<script type="application/json" id="page-data">
|
||
{{ page_data | tojson }}
|
||
</script>
|
||
<script>
|
||
/* eslint-disable */
|
||
const CMDS=[{name:'BASIC',items:[
|
||
{id:'paragraph',name:'Text',icon:'¶'},{id:'heading_1',name:'Heading 1',icon:'H1'},{id:'heading_2',name:'Heading 2',icon:'H2'},
|
||
{id:'heading_3',name:'Heading 3',icon:'H3'},{id:'heading_4',name:'Heading 4',icon:'H4'},{id:'bulleted_list',name:'Bulleted list',icon:'•'},{id:'numbered_list',name:'Numbered list',icon:'1.'},
|
||
{id:'to_do',name:'To-do',icon:'☐'},{id:'toggle',name:'Toggle',icon:'▶'},{id:'quote',name:'Quote',icon:'❝'},
|
||
{id:'callout',name:'Callout',icon:'💡'},{id:'table_of_contents',name:'Table of contents',icon:'☰'},{id:'math',name:'Math equation',icon:'∑'},
|
||
{id:'columns',name:'Column list',icon:'▮'},{id:'divider',name:'Divider',icon:'—'},{id:'code',name:'Code block',icon:'<>'},
|
||
{id:'table',name:'Table',icon:'⊞'},
|
||
]},{name:'MEDIA',items:[{id:'image',name:'Image',icon:'🖼️'}]},
|
||
{name:'DATA',items:[{id:'database',name:'Database / Inline database',icon:'📊'}]}];
|
||
|
||
let _bid=0;function genId(){return 'b'+(++_bid)+'_'+Date.now().toString(36);}
|
||
function cp(e){const s=window.getSelection();if(!s.rangeCount)return 0;const r=s.getRangeAt(0).cloneRange();r.selectNodeContents(e);r.setEnd(s.getRangeAt(0).endContainer,s.getRangeAt(0).endOffset);return r.toString().length;}
|
||
function gt(e){return (e&&e.innerText!==undefined)?e.innerText:(e?e.textContent:'');}
|
||
function splitCaret(e){
|
||
const s=window.getSelection(); if(!s.rangeCount||!s.rangeCount)return null; const sel=s.getRangeAt(0);
|
||
const MK='\uF000FD'; const mark=document.createElement('span'); mark.textContent=MK;
|
||
sel.insertNode(mark);
|
||
const full=gt(e)||'';
|
||
const parts=full.split(MK);
|
||
mark.remove(); e.normalize();
|
||
return {before:parts[0]||'',after:(parts.length>1?parts[1]:'')||'',content:full.split(MK).join('')};
|
||
}
|
||
function ce(e){const r=document.createRange();r.selectNodeContents(e);r.collapse(false);window.getSelection().removeAllRanges();window.getSelection().addRange(r);}
|
||
function cs(e){const r=document.createRange();r.selectNodeContents(e);r.collapse(true);window.getSelection().removeAllRanges();window.getSelection().addRange(r);}
|
||
function esc(s){return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');}
|
||
// Escape then render inline markdown (bold/italic/code/links/strikethrough)
|
||
// so agent-generated content displays without raw `**`/backtick characters.
|
||
function mdEsc(s){
|
||
if(!s) return '';
|
||
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>')
|
||
.replace(/`([^`]+)`/g,'<code>$1</code>')
|
||
.replace(/\*\*([^*]+)\*\*/g,'<strong>$1</strong>')
|
||
.replace(/\*([^*]+)\*/g,'<em>$1</em>')
|
||
.replace(/~~([^~]+)~~/g,'<s>$1</s>')
|
||
.replace(/\[([^\]]+)\]\((https?:[^)\s]+)\)/g,'<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>')
|
||
.replace(/\n/g,'<br>');
|
||
}
|
||
|
||
const CALLOUT_ICONS=['💡','💡','📌','⚠️','ℹ️','✅','❌','🚀','🔔','📣','⭐','❤️','🔥','💬','👀','🧠','🎯','🔑','📅','💰','💡','📚','🧪','🌍'];
|
||
|
||
/* ═══════════ Simple Table block (v4.8.0) ═══════════ */
|
||
function _tblSplitLine(s){s=(s||'').trim();if(s.charAt(0)==='|')s=s.slice(1);if(s.charAt(s.length-1)==='|')s=s.slice(0,-1);return s.split('|').map(function(x){return x.trim();});}
|
||
function _tblIsSep(s){s=(s||'').trim();if(!s||s.charAt(0)!=='|')return false;var cells=_tblSplitLine(s);if(!cells.length)return false;return cells.every(function(c){return /^:?-{1,}:?$/.test(c);});}
|
||
function _tblIsData(s){s=(s||'').trim();return /^\|/.test(s)&&s.indexOf('|',1)>=0;}
|
||
function _tblNew(rows,cols){var out=[];for(var i=0;i<rows;i++){var r=new Array(cols);for(var j=0;j<cols;j++)r[j]='';out.push(r);}return out;}
|
||
function _tblClone(b){b.rows=(b.rows||[]).map(function(r){return (r||[]).slice();});return b;}
|
||
function _tblAddCol(b,index){_tblClone(b);var w=b.rows.length?b.rows[0].length:1;if(index<0)index=0;if(index>w)index=w;b.rows.forEach(function(r){r.splice(index,0,'');});}
|
||
function _tblAddRow(b,index){_tblClone(b);var w=b.rows.length&&b.rows[0]?b.rows[0].length:1;if(index<0)index=0;if(index>b.rows.length)index=b.rows.length;b.rows.splice(index,0,_tblNew(1,w)[0]);}
|
||
function _tblDupCol(b,index){_tblClone(b);b.rows.forEach(function(r){var v=(index<r.length)?r[index]:'';var at=index+1>r.length?r.length:index+1;r.splice(at,0,v);});}
|
||
function _tblDupRow(b,index){_tblClone(b);if(index<0||index>b.rows.length)return;b.rows.splice(index+1,0,(b.rows[index]||[]).slice());}
|
||
function _tblClearCol(b,index){_tblClone(b);b.rows.forEach(function(r){if(r&&index<r.length)r[index]='';});}
|
||
function _tblClearRow(b,index){_tblClone(b);var w=b.rows[0]?b.rows[0].length:1;if(index>=0&&index<b.rows.length)b.rows[index]=_tblNew(1,w)[0];}
|
||
function _tblDelCol(b,index){_tblClone(b);if(!b.rows.length)return;if(b.rows[0].length<=1)return;b.rows.forEach(function(r){if(index>=0&&index<r.length)r.splice(index,1);});}
|
||
function _tblDelRow(b,index){_tblClone(b);if(b.rows.length<=1)return;if(index>=0&&index<b.rows.length)b.rows.splice(index,1);}
|
||
function _tblGrip(){return '<svg viewBox="0 0 16 16" width="12" height="12" style="display:block"><circle cx="4" cy="3" r="1.4"/><circle cx="11" cy="3" r="1.4"/><circle cx="4" cy="8" r="1.4"/><circle cx="11" cy="8" r="1.4"/><circle cx="4" cy="13" r="1.4"/><circle cx="11" cy="13" r="1.4"/></svg>';}
|
||
function _tblDefault(){return {rows:_tblNew(3,3)};}
|
||
function _tblFocus(bid,r,c){var sel=document.querySelector('.ftable-editor[data-bid="'+bid+'"]');if(!sel)return;var q='[data-c="'+c+'"]';var cell=sel.querySelector(q);if(!cell){var first=sel.querySelector('.ftable-cell[data-r]');if(first){first.focus();ce(first);}return;}cell.focus();if(r===undefined)cs(cell);else ce(cell);}
|
||
function renderTableBlock(b,idx){
|
||
var rows=(b&&Array.isArray(b.rows)&&b.rows.length)?b.rows:_tblNew(2,2);
|
||
var w=0;rows.forEach(function(r){if(r&&r.length>w)w=r.length;});if(w<1)w=2;
|
||
var widths=(b&&Array.isArray(b.colsW))?b.colsW:[];
|
||
var bid=b.id;var h='';
|
||
h+='<div class="block-wrapper"><div class="block-content ftable-editor" data-bid="'+bid+'">';
|
||
h+='<div class="ftable-flex"><div class="ftable-flex-main">';
|
||
h+='<div class="ftable-colbar">';
|
||
for(var c=0;c<w;c++){
|
||
h+='<div class="ftable-colcell"><button type="button" class="ftable-colgrip" data-tbl="colmenu" data-bid="'+bid+'" data-col="'+c+'" title="Column options">'+_tblGrip()+'</button></div>';
|
||
}
|
||
h+='</div>';
|
||
h+='<table class="ftable ftable-edit"><tbody>';
|
||
for(var ri=0;ri<rows.length;ri++){
|
||
var isHead=(ri===0);var tag=isHead?'th':'td';
|
||
h+='<tr class="ftable-row">';
|
||
for(var ci=0;ci<w;ci++){
|
||
var v=rows[ri]&&ci<rows[ri].length?rows[ri][ci]:'';
|
||
var wpx=(widths[ci]&&widths[ci]>0)?(' style="width:'+widths[ci]+'px"'):'';
|
||
h+='<'+tag+' class="ftable-cell'+(isHead?' head':'')+'" contenteditable="true" data-r="'+ri+'" data-c="'+ci+'"'+wpx+' spellcheck="false">'+esc(v);
|
||
if(isHead)h+='<span class="ftable-resize" data-col="'+ci+'" title="Drag to resize column"></span>';
|
||
h+='</'+tag+'>';
|
||
}
|
||
h+='</tr>';
|
||
}
|
||
h+='</tbody></table>';
|
||
h+='<div class="ftable-rowbar"><button type="button" class="ftable-addrow" data-tbl="rowadd" data-bid="'+bid+'" title="Click to add a row">+ Row</button></div>';
|
||
h+='</div>';
|
||
h+='<div class="ftable-addcolwrap"><button type="button" class="ftable-addcol" data-tbl="coladdright" data-bid="'+bid+'" title="Add column on the right">+</button></div>';
|
||
h+='</div>';
|
||
h+='</div></div>';
|
||
return h;
|
||
}
|
||
function _tblBlockById(bid){var e=window.E;if(!e||!e.blocks)return null;for(var i=0;i<e.blocks.length;i++){if(e.blocks[i].id===bid)return e.blocks[i];}return null;}
|
||
function _tblIdxByBid(bid){var e=window.E;if(!e||!e.blocks)return -1;for(var i=0;i<e.blocks.length;i++){if(e.blocks[i].id===bid)return i;}return -1;}
|
||
function _tblMenuClose(){var m=document.getElementById('_tblMenu');if(m){m.parentNode&&m.parentNode.removeChild(m);}if(window._tblCur)window._tblCur=null;}
|
||
function _tblMenuOpen(x,y,bid,kind,index){
|
||
_tblMenuClose();
|
||
var items=[];
|
||
if(kind==='col'){
|
||
items=[['col-insert-left','Insert left'],['col-insert-right','Insert right'],['col-dup','Duplicate'],null,['col-clear','Clear contents'],['col-del','Delete column',1],null,['tbl-del','Delete table',1]];
|
||
}else{
|
||
items=[['row-insert-above','Insert above'],['row-insert-below','Insert below'],['row-dup','Duplicate'],null,['row-clear','Clear row'],['row-del','Delete row',1],null,['tbl-del','Delete table',1]];
|
||
}
|
||
var d=document.createElement('div');d.className='ftable-menu';d.id='_tblMenu';d.style.cssText='position:fixed;z-index:3000;';
|
||
var html='';
|
||
items.forEach(function(it){
|
||
if(!it){html+='<div class="ftable-menu-sep"></div>';return;}
|
||
html+='<div class="ftable-menu-item'+(it[2]?' danger':'')+'" data-tbl-act="'+it[0]+'">'+it[1]+'</div>';
|
||
});
|
||
d.innerHTML=html;
|
||
document.body.appendChild(d);
|
||
var r=d.getBoundingClientRect();
|
||
x=Math.min(Math.max(8,x),window.innerWidth-r.width-8);
|
||
y=Math.min(Math.max(8,y),window.innerHeight-r.height-8);
|
||
d.style.left=x+'px';d.style.top=y+'px';
|
||
window._tblCur={bid:bid,kind:kind,index:index};
|
||
}
|
||
function _tblDo(action,cur){
|
||
if(!cur)return;
|
||
var bid=cur.bid,index=cur.index,b=_tblBlockById(bid),e=window.E;
|
||
if(!b||!e)return;
|
||
e.sync();
|
||
if(!(b.rows&&b.rows.length))b.rows=_tblNew(2,2);
|
||
var focusR=1,focusC=0;
|
||
if(action==='col-insert-left'){_tblAddCol(b,index);focusC=index;}
|
||
else if(action==='col-insert-right'){_tblAddCol(b,index+1);focusC=index+1;}
|
||
else if(action==='col-dup'){_tblDupCol(b,index);focusC=index+1;}
|
||
else if(action==='col-clear'){_tblClearCol(b,index);focusC=index;}
|
||
else if(action==='col-del'){_tblDelCol(b,index);focusC=Math.min(index,b.rows[0].length-1);}
|
||
else if(action==='row-insert-above'){_tblAddRow(b,index);focusR=index;}
|
||
else if(action==='row-insert-below'){_tblAddRow(b,index+1);focusR=index+1;}
|
||
else if(action==='row-dup'){_tblDupRow(b,index);focusR=index+1;}
|
||
else if(action==='row-clear'){_tblClearRow(b,index);focusR=index;}
|
||
else if(action==='row-del'){_tblDelRow(b,index);focusR=Math.min(index,b.rows.length-1);if(focusR<1)focusR=1;}
|
||
else if(action==='tbl-del'){_tblMenuClose();var bi=_tblIdxByBid(bid);if(bi>=0)e.removeBlock(bi);return;}
|
||
e.render();e.dirty=true;e.autoSave();
|
||
_tblFocus(bid,focusR,focusC);
|
||
}
|
||
function _tblResizeStart(ev){
|
||
var sp=ev.target&&ev.target.closest?ev.target.closest('.ftable-resize'):null;if(!sp)return;
|
||
var th=sp.parentElement;if(!th)return;
|
||
var wrap=th.closest('.ftable-editor');if(!wrap)return;
|
||
var bid=wrap.getAttribute('data-bid');var col=parseInt(sp.getAttribute('data-col')||'0',10);
|
||
var b=_tblBlockById(bid);if(!b)return;
|
||
ev.preventDefault();ev.stopPropagation();
|
||
var E=window.E;if(E&&E.sync)E.sync();
|
||
var widths=(b.colsW&&Array.isArray(b.colsW))?b.colsW.slice():[];
|
||
while(widths.length<=col)widths.push(null);
|
||
var startX=ev.clientX;var base=widths[col]||th.getBoundingClientRect().width;
|
||
document.body.style.cursor='col-resize';document.body.style.userSelect='none';
|
||
function move(me){var dx=me.clientX-startX;var nw=Math.max(48,Math.min(760,base+dx));th.style.width=Math.round(nw)+'px';widths[col]=Math.round(nw);}
|
||
function up(me){
|
||
document.removeEventListener('mousemove',move);document.removeEventListener('mouseup',up);
|
||
document.body.style.cursor='';document.body.style.userSelect='';
|
||
move(me);
|
||
b.colsW=widths;
|
||
if(E){E.dirty=true;E.autoSave();}
|
||
}
|
||
document.addEventListener('mousemove',move);
|
||
document.addEventListener('mouseup',up);
|
||
}
|
||
function _tblInitDelegate(){
|
||
if(window._tblDelegate)return;window._tblDelegate=true;
|
||
document.addEventListener('click',function(ev){
|
||
if(ev.target.closest&&ev.target.closest('#_tblMenu')){var it=ev.target.closest('.ftable-menu-item');if(it){var act=it.getAttribute('data-tbl-act');_tblDo(act,window._tblCur);}return;}
|
||
var g=ev.target.closest?ev.target.closest('[data-tbl]'):null;
|
||
if(g){var act=g.getAttribute('data-tbl'),bid=g.getAttribute('data-bid'),col=parseInt(g.getAttribute('data-col')||'0',10);
|
||
var gr=g.getBoundingClientRect?g.getBoundingClientRect():null;
|
||
if(act==='colmenu'){_tblMenuOpen(gr?gr.left:ev.clientX,(gr?gr.bottom:ev.clientY)+4,bid,'col',col);ev.preventDefault();}
|
||
else if(act==='colplus'){var b=_tblBlockById(bid);if(b){window.E.sync();_tblAddCol(b,col+1);window.E.render();window.E.dirty=true;window.E.autoSave();_tblFocus(bid,1,col+1);}}
|
||
else if(act==='rowadd'){var b2=_tblBlockById(bid);if(b2){window.E.sync();var n=b2.rows.length;_tblAddRow(b2,n);window.E.render();window.E.dirty=true;window.E.autoSave();_tblFocus(bid,n,0);}}
|
||
else if(act==='coladdright'){var b4=_tblBlockById(bid);if(b4){window.E.sync();var cw=(b4.rows&&b4.rows[0])?b4.rows[0].length:1;_tblAddCol(b4,cw);window.E.render();window.E.dirty=true;window.E.autoSave();_tblFocus(bid,1,cw);}}
|
||
return;
|
||
}
|
||
if(document.getElementById('_tblMenu')&&!(ev.target.closest&&ev.target.closest('[data-tbl]')))_tblMenuClose();
|
||
});
|
||
document.addEventListener('contextmenu',function(ev){
|
||
var cell=ev.target.closest?ev.target.closest('.ftable-cell'):null;
|
||
if(!cell)return;
|
||
var w=cell.closest('.ftable-editor');if(!w)return;
|
||
var bid=w.getAttribute('data-bid'),r=parseInt(cell.getAttribute('data-r')||'0',10),c=parseInt(cell.getAttribute('data-c')||'0',10);
|
||
ev.preventDefault();
|
||
if(r===0)_tblMenuOpen(ev.clientX,ev.clientY,bid,'col',c);
|
||
else _tblMenuOpen(ev.clientX,ev.clientY,bid,'row',r);
|
||
});
|
||
document.addEventListener('mousedown',function(ev){
|
||
if(ev.target&&ev.target.closest&&ev.target.closest('.ftable-resize')){_tblResizeStart(ev);return;}
|
||
if(document.getElementById('_tblMenu')&&!(ev.target.closest&&ev.target.closest('#_tblMenu')))_tblMenuClose();
|
||
});
|
||
}
|
||
|
||
// ── 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 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);}});
|
||
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();
|
||
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();});
|
||
if(!SM._doc){
|
||
SM._doc=true;
|
||
document.addEventListener('keydown',function(ev){
|
||
if(!SM._o)return;
|
||
if(ev.metaKey||ev.ctrlKey||ev.altKey)return;
|
||
if(ev.key==='ArrowDown'||ev.key==='ArrowUp'||ev.key==='Enter'||ev.key==='Escape'){
|
||
ev.preventDefault();ev.stopPropagation();SM._docKd(ev);
|
||
}
|
||
});
|
||
}
|
||
SM._docKd=function(ev){
|
||
if(SM._o&&(ev.key==='ArrowDown'||ev.key==='ArrowUp'||ev.key==='Enter'||ev.key==='Escape'))SM._kd(ev);
|
||
};
|
||
},
|
||
_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='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';
|
||
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;
|
||
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: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;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);}
|
||
else if(e.key==='Escape'){e.preventDefault();SM.close();}},
|
||
_rs(){const its=SM._li.querySelectorAll('.slash-item');its.forEach((el,i)=>{el.classList.toggle('selected',i===SM._s);});const cur=its[SM._s];if(cur&&cur.scrollIntoView)cur.scrollIntoView({block:'nearest'});},
|
||
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);
|
||
const w=Math.max(r.width||0,280); this.ROOT.style.width=Math.min(w,1200)+'px';
|
||
this.ROOT.style.left=Math.max(8,Math.min(r.left,window.innerWidth-r.width-16))+ '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{left:Math.max(24,window.innerWidth/2-210),top:window.innerHeight/2,bottom:window.innerHeight/2,width:420};
|
||
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,width:420}; },
|
||
_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'); if(!el)return ''; const r=document.createRange();r.selectNodeContents(el);return r.toString(); },
|
||
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);
|
||
if(b.type==='divider')return `<div class="block-wrapper"><hr class="block-divider"></div>`;
|
||
if(b.type==='image')return `<div class="block-wrapper"><div class="block-content block-image" style="text-align:center;padding:12px 0;"><img src="${b.src||''}" alt="${esc(b.alt||'')}" style="max-width:100%;max-height:70vh;border-radius:8px;display:block;margin:0 auto;" onerror="this.style.display='none';this.parentElement.innerHTML='<div style=\\'padding:40px;color:var(--text-tertiary)\\'>Image not found</div>'"></div></div>`;
|
||
if(b.type==='table_of_contents')return renderTOC();
|
||
if(b.type==='math')return `<div class="block-wrapper"><div class="block-content block-math">${renderMathBlock(b)}</div></div>`;
|
||
if(b.type==='columns')return renderColumnsBlock(b);
|
||
if(b.type==='embed'){
|
||
if(b.embed_type==='inline_dbs')return `<div class="block-wrapper"><div class="block-content block-inline-dbs"><div style="padding:20px;text-align:center;color:var(--text-secondary);">📊 Inline Databases</div><div class="inline-dbs-grid">${(b.dbs||[]).map(db=>`<a href="/db/${db.id}/view/${db.view_type||'table'}" class="inline-db-card" target="_blank" style="display:block;padding:12px 16px;background:var(--bg-secondary);border-radius:8px;margin:8px 0;text-decoration:none;color:var(--text-primary);border:1px solid var(--border);"><span style="font-weight:500">📋 ${db.name}</span><span style="float:right;font-size:12px;color:var(--text-tertiary);">${db.view_type==='form'?'📝 Form':'📊 Table'} →</span></a>`).join('')}</div></div></div>`;
|
||
if(b.embed_type==='collection')return `<div class="block-wrapper"><div class="block-content block-embed-collection" id="inline-db-${b.collection_id}" style="padding:0;min-height:200px"><div style="padding:40px;text-align:center;color:var(--text-dim)">Loading database...</div></div><`+`script>setTimeout(function(){var el=document.getElementById('inline-db-${b.collection_id}');if(el&&window.FlowDeckDB)window.FlowDeckDB.renderInto(el,${b.collection_id},null);},50);<`+`/script></div>`;
|
||
if(b.embed_type==='pdf')return `<div class="block-wrapper"><div class="block-content block-embed" style="padding:0;"><iframe src="${b.src||''}" style="width:100%;height:80vh;border:none;border-radius:8px;" onerror="this.style.display='none';this.nextElementSibling.style.display='block';"></iframe><div style="display:none;text-align:center;padding:32px;color:var(--text-secondary);"><svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg><p style="margin-top:8px;">Failed to load PDF</p><a href="${b.src||''}" target="_blank" style="color:var(--accent);">Open in new tab →</a></div></div></div>`;
|
||
if(b.embed_type==='download'){
|
||
var sz=b.file_size||0;var szStr=sz<1024?sz+' B':sz<1048576?(sz/1024).toFixed(1)+' KB':(sz/1048576).toFixed(1)+' MB';
|
||
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...':(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>`;
|
||
else if(b.type==='code')inner=`<div class="code-lang-label">${b.language||'Plain Text'}</div><pre><code data-bid="${b.id}" contenteditable="true" spellcheck="false">${mdEsc(b.content)}</code></pre>`;
|
||
else if(b.type==='callout')inner=`<span class="callout-icon" onclick="E._toggleCalloutIcon('${b.id}')" title="Change icon">${b.icon||'💡'}</span><div data-bid="${b.id}" contenteditable="true" data-placeholder="${ph}" spellcheck="false">${mdEsc(b.content)}</div><div class="callout-icon-picker" id="callout-picker-${b.id}" style="display:none">${CALLOUT_ICONS.map(function(i){return '<button type="button" class="callout-icon-opt" onclick="E._setCalloutIcon(\''+b.id+'\',\''+i+'\')">'+i+'</button>';}).join('')}</div>`;
|
||
else inner=`<div data-bid="${b.id}" contenteditable="true" data-placeholder="${ph}" spellcheck="false">${mdEsc(b.content)}</div>`;
|
||
const tag=b.type.startsWith('heading_')?'h'+b.type.slice(-1):b.type==='quote'?'blockquote':'div';
|
||
const cls=tag==='div'?`block-content ${b.type==='bulleted_list'?'block-bullet':b.type==='numbered_list'?'block-numbered':b.type==='to_do'?'block-todo':b.type==='toggle'?'block-toggle':b.type==='code'?'block-code':b.type==='callout'?'block-callout':''}`:`block-content block-${tag}`;
|
||
const style=b.type==='callout'?` style="background:${(b.style&&b.style.bgColor)||'var(--blue-bg)'}"`:'';
|
||
const bidAttr=b.type.startsWith('heading_')?` id="h-${(b.id||'').replace(/[^a-zA-Z0-9]/g,'')}"`:'';
|
||
const actions=idx>=0?`<div class="block-actions"><button class="block-add-btn" onclick="E.addAfter(${idx})">+</button></div>`:'';
|
||
return `<div class="block-wrapper">
|
||
<div class="block-handle"><svg viewBox="0 0 16 16" width="14" height="14"><circle cx="4" cy="3" r="1.3"/><circle cx="11" cy="3" r="1.3"/><circle cx="4" cy="8" r="1.3"/><circle cx="11" cy="8" r="1.3"/><circle cx="4" cy="13" r="1.3"/><circle cx="11" cy="13" r="1.3"/></svg></div>
|
||
${actions}
|
||
<${tag}${style}${bidAttr} class="${cls}">${inner}</${tag}>
|
||
</div>`;
|
||
}
|
||
|
||
function renderTOC(){
|
||
var out=[];
|
||
var blocks=window.E?window.E.blocks:[];
|
||
var headings=blocks.filter(function(b){return /^heading_[1-4]$/.test(b.type)&&(b.content||'').trim();});
|
||
if(headings.length===0){
|
||
out.push(`<div class="block-content block-toc block-toc-empty"><span style="color:var(--text-dim);font-size:13px;">Add headings to generate a table of contents</span></div>`);
|
||
}else{
|
||
out.push(`<div class="block-content block-toc"><div class="block-toc-title">On this page</div>`);
|
||
headings.forEach(function(h){
|
||
var lvl=parseInt(h.type.slice(-1),10);
|
||
var id='h-'+ (h.id||'').replace(/[^a-zA-Z0-9]/g,'');
|
||
out.push(`<div class="block-toc-item" style="margin-left:${(lvl-1)*12}px" onclick="var el=document.getElementById('${id}');if(el){el.scrollIntoView({behavior:'smooth',block:'start'});}">${esc(h.content)}</div>`);
|
||
});
|
||
out.push(`</div>`);
|
||
}
|
||
return `<div class="block-wrapper">${out.join('')}</div>`;
|
||
}
|
||
|
||
function renderMathBlock(b){
|
||
var html=`<div class="math-display" data-bid="${b.id}"></div>`;
|
||
html+=`<textarea class="math-editor" data-math-bid="${b.id}" data-placeholder="Type a LaTeX math expression (e.g. E = mc^2)..." spellcheck="false">${esc(b.content)}</textarea>`;
|
||
return html;
|
||
}
|
||
|
||
function renderColumnsBlock(b){
|
||
var cols=(b.children&&b.children.length)?b.children:[{id:genId(),type:'paragraph',content:''}];
|
||
var cells=cols.map(function(c){
|
||
return `<div class="column-cell">${renderChildColumn(c,b.id)}</div>`;
|
||
}).join('');
|
||
return `<div class="block-wrapper"><div class="block-content block-columns"><div class="columns-add-bar"><button class="columns-add-btn" onclick="E._addColumn('${b.id}')">+ Add column</button></div><div class="columns-row">${cells}</div></div></div>`;
|
||
}
|
||
|
||
function renderChildColumn(c,parentId){
|
||
return `<div class="block-column-wrapper" data-parent="${parentId}">
|
||
<div class="block-actions"><button class="block-add-btn" onclick="E._removeColumn('${parentId}','${c.id}')" title="Remove column">×</button></div>
|
||
<div class="block-content block-column" data-bid="${c.id}" contenteditable="true" data-placeholder="${c.type==='heading_1'?'Heading 1':c.type==='heading_2'?'Heading 2':c.type==='heading_3'?'Heading 3':'Type...'}" spellcheck="false">${esc(c.content)}</div>
|
||
</div>`;
|
||
}
|
||
|
||
function renderNestedBlock(c){
|
||
var ph=c.type==='heading_1'?'Heading 1':c.type==='heading_2'?'Heading 2':c.type==='heading_3'?'Heading 3':c.type==='heading_4'?'Heading 4':c.type==='bulleted_list'?'List':c.type==='numbered_list'?'List':c.type==='to_do'?'To-do':'Type...';
|
||
return `<div class="block-wrapper nested-block"><div class="block-content block-nested" data-bid="${c.id}" contenteditable="true" data-placeholder="${ph}" spellcheck="false">${esc(c.content)}</div></div>`;
|
||
}
|
||
|
||
// ═══════════ AI Meeting Note — widget de réunion (v4.12.0) ═══════════
|
||
// Bloc interactif « Notion-like » : consentement, transcription live (Web
|
||
// Speech API quand disponible), transcript horodaté + notes, puis résumé
|
||
// généré par le FlowDeck Agent.
|
||
function renderMeetingBlock(b, idx){
|
||
var actions=idx>=0?`<div class="block-actions"><button class="block-add-btn" onclick="E.addAfter(${idx})">+</button></div>`:'';
|
||
return `<div class="block-wrapper">
|
||
<div class="block-handle"><svg viewBox="0 0 16 16" width="14" height="14"><circle cx="4" cy="3" r="1.3"/><circle cx="11" cy="3" r="1.3"/><circle cx="4" cy="8" r="1.3"/><circle cx="11" cy="8" r="1.3"/><circle cx="4" cy="13" r="1.3"/><circle cx="11" cy="13" r="1.3"/></svg></div>
|
||
${actions}
|
||
<div class="block-content block-meeting" data-meeting-bid="${b.id}"><div class="fm-loading">Préparation de la réunion…</div></div>
|
||
</div>`;
|
||
}
|
||
|
||
function _fmFind(bid){var e=window.E;if(e&&e.blocks){for(var i=0;i<e.blocks.length;i++){if(e.blocks[i].id===bid)return e.blocks[i];}}return null;}
|
||
function _fmDef(){return {title:'Meeting',date:new Date().toISOString().slice(0,10),consent:'self',started:false,status:'idle',lines:[],interim:'',notes:'',manualTranscript:'',summary:'',summaryError:'',summaryLoading:false,lastTab:'transcript'};}
|
||
function _fmEnsure(bid){var b=_fmFind(bid);if(!b)return null;if(!b.meeting)b.meeting=_fmDef();if(!b.meeting.lines)b.meeting.lines=[];if(b.meeting.summaryLoading===undefined)b.meeting.summaryLoading=false;if(b.meeting.manualTranscript===undefined)b.meeting.manualTranscript='';return b;}
|
||
function _fmTime(){var d=new Date();var p=function(n){return (n<10?'0':'')+n;};return p(d.getHours())+':'+p(d.getMinutes())+':'+p(d.getSeconds());}
|
||
function _fmEsc(v){return esc(v==null?'':String(v));}
|
||
function _fmAttr(v){return _fmEsc(v).replace(/"/g,'"');}
|
||
function _fmSave(){var e=window.E;if(e){e.dirty=true;if(e.autoSave)e.autoSave();}}
|
||
function _fmTranscriptText(b){var out=[];(b.meeting.lines||[]).forEach(function(l){out.push('['+l.t+'] '+(l.who||'Vous')+' : '+l.text);});if((b.meeting.manualTranscript||'').trim())out.push('(transcription saisie/collée) '+(b.meeting.manualTranscript||'').trim());if((b.meeting.notes||'').trim())out.push('(notes) '+(b.meeting.notes||'').trim());return out.join('\n');}
|
||
|
||
var FDMeeting = {
|
||
_rt:{},
|
||
_audio(){ return !!(window.SpeechRecognition||window.webkitSpeechRecognition); },
|
||
mount(bid,root){
|
||
var s=this._rt[bid]||(this._rt[bid]={});s.root=root;
|
||
if(!s.wired){
|
||
s.wired=true;
|
||
var self=this;
|
||
root.addEventListener('click',function(ev){var t=ev.target&&ev.target.closest?ev.target.closest('[data-act]'):null;if(t)self.act(bid,t.getAttribute('data-act'),t);});
|
||
root.addEventListener('input',function(ev){self.onInput(bid,ev.target);});
|
||
root.addEventListener('change',function(ev){self.onInput(bid,ev.target);});
|
||
}
|
||
this.paint(bid);
|
||
},
|
||
focus(bid){},
|
||
paint(bid){
|
||
var s=this._rt[bid];var b=_fmEnsure(bid);if(!b||!s||!s.root)return;
|
||
s.root.innerHTML=this.html(bid);
|
||
},
|
||
_statusLabel(m){
|
||
if(m.status==='running')return '● Enregistrement';
|
||
if(m.status==='paused')return '⏸ En pause';
|
||
if(m.status==='done')return '✓ Terminée';
|
||
if(m.status==='manual')return '📝 Sans audio';
|
||
return 'Prête';
|
||
},
|
||
html(bid){
|
||
var b=_fmEnsure(bid);if(!b)return '';
|
||
var m=b.meeting;var s=this._rt[bid]||{};
|
||
var audioOk=this._audio();
|
||
var st=this._statusLabel(m);
|
||
var pill=m.status==='running'||m.status==='paused'||m.status==='done'||m.status==='manual'
|
||
? `<span class="fm-pill ${m.status==='done'?'fm-pill-ok':''}">${_fmEsc(st)}</span>`:'';
|
||
var head=`<div class="fm-head">
|
||
<span class="fm-ico">🎙️</span>
|
||
<input class="fm-title" data-fm="title" value="${_fmAttr(m.title)}" placeholder="Titre de la réunion" spellcheck="false">
|
||
<span class="fm-date">📅 ${_fmEsc(m.date)}</span>
|
||
${pill}
|
||
</div>`;
|
||
|
||
var body='';
|
||
if(!m.started){
|
||
body+=`<div class="fm-step">
|
||
<div class="fm-step-title">1 · Consentement des participants</div>
|
||
<label class="fm-radio"><input type="radio" name="fm-consent-${bid}" data-fm="consent" value="self" ${m.consent==='auto'?'':'checked'}> <span>🔘 Obtenir le consentement moi-même</span></label>
|
||
<label class="fm-radio"><input type="radio" name="fm-consent-${bid}" data-fm="consent" value="auto" ${m.consent==='auto'?'checked':''}> <span>🔘 Lire automatiquement un message audio</span></label>
|
||
<div class="fm-step-desc">${audioOk?'Notion AI est prêt : la réunion sera transcrite en direct. Autorisez le micro lorsque le navigateur le demande.':'Ce navigateur ne supporte pas la transcription audio : vous pourrez saisir ou coller la transcription manuellement.'}</div>
|
||
<button class="fm-btn fm-btn-primary" data-act="start" type="button">${audioOk?'🎙 Start transcribing':'Ouvrir la boîte de réunion (sans audio)'}</button>
|
||
</div>`;
|
||
}else{
|
||
var tabs=`<div class="fm-tabs">
|
||
<button class="fm-tab ${m.lastTab==='notes'?'active':''}" data-act="tab" data-tab="notes" type="button">Notes</button>
|
||
<button class="fm-tab ${m.lastTab==='transcript'?'active':''}" data-act="tab" data-tab="transcript" type="button">Transcript</button>
|
||
<button class="fm-tab ${m.lastTab==='summary'?'active':''}" data-act="tab" data-tab="summary" type="button">Summary</button>
|
||
</div>`;
|
||
var ctrl='';
|
||
if(m.status==='running'){
|
||
ctrl=`<div class="fm-wave">•••|ı|ı|ı|ı•••</div>
|
||
<div class="fm-actions">
|
||
<button class="fm-btn" data-act="pause" type="button">⏸ Pause</button>
|
||
<button class="fm-btn fm-btn-primary" data-act="stop" type="button">⏹ Stop</button>
|
||
</div>`;
|
||
}else if(m.status==='paused'){
|
||
ctrl=`<div class="fm-actions">
|
||
<button class="fm-btn fm-btn-primary" data-act="start" type="button">▶ Reprendre</button>
|
||
<button class="fm-btn" data-act="stop" type="button">⏹ Stop</button>
|
||
</div>`;
|
||
}else if(m.status==='manual'){
|
||
ctrl=`<div class="fm-actions"><button class="fm-btn" data-act="stop" type="button">⏹ Terminer la réunion</button></div>`;
|
||
}else if(m.status==='done'){
|
||
ctrl=`<div class="fm-actions"><button class="fm-btn" data-act="new" type="button">🔄 Nouvelle transcription</button></div>`;
|
||
}
|
||
var panel='';
|
||
if(m.lastTab==='notes'){
|
||
panel=`<textarea class="fm-area" data-fm="notes" rows="6" placeholder="Prenez des notes manuelles pendant la réunion…">${_fmEsc(m.notes)}</textarea>`;
|
||
}else if(m.lastTab==='transcript'){
|
||
var lines=(m.lines||[]).map(function(l){return `<div class="fm-line"><span class="fm-ts">${_fmEsc(l.t||'')}</span><span class="fm-who">${_fmEsc(l.who||'Vous')}</span><span class="fm-txt">${_fmEsc(l.text)}</span></div>`;}).join('');
|
||
if(m.status==='running'){lines+=`<div class="fm-line fm-interim"><span class="fm-ts">·</span><span class="fm-who">Vous</span><span class="fm-txt">${_fmEsc(m.interim||'')}${(m.interim||'')?'':'…'}</span></div>`;}
|
||
var manual=m.status!=='running'?`<div class="fm-manual"><div class="fm-manual-label">Transcript saisi ou collé (si aucune capture audio)</div><textarea class="fm-area" data-fm="manual" rows="4" placeholder="Collez ici la transcription de la réunion (optionnel)…">${_fmEsc(m.manualTranscript)}</textarea></div>`:'';
|
||
panel=`<div class="fm-transcript"><div data-fm-transcript-body>${lines||'<div class="fm-empty">La transcription apparaîtra ici en direct. Enregistrez la réunion pour générer un transcript.</div>'}</div>${manual}</div>`;
|
||
}else{
|
||
var summaryHtml='';
|
||
if(m.summaryLoading){
|
||
summaryHtml=`<div class="fm-loading">Génération du résumé…</div>`;
|
||
}else if(m.summary){
|
||
summaryHtml=`<textarea class="fm-area" data-fm="summary" rows="14">${_fmEsc(m.summary)}</textarea>
|
||
<div class="fm-actions"><button class="fm-btn" data-act="copy" type="button">📋 Copier le résumé</button></div>`;
|
||
}else if(m.summaryError){
|
||
summaryHtml=`<div class="fm-error">${_fmEsc(m.summaryError)}</div>
|
||
<button class="fm-btn fm-btn-primary" data-act="generate" type="button">Réessayer</button>`;
|
||
}else{
|
||
var has=!!(_fmTranscriptText(b).trim());
|
||
if(m.status==='done'&&has){
|
||
summaryHtml=`<button class="fm-btn fm-btn-primary" data-act="generate" type="button">✨ Générer le résumé</button>
|
||
<div class="fm-step-desc">L'agent analyse le transcript et produit un aperçu structuré (sujets, décisions, actions).</div>`;
|
||
}else{
|
||
summaryHtml=`<div class="fm-empty">Arrêtez l'enregistrement (ou ajoutez un transcript) puis générez le résumé ici.</div>`;
|
||
}
|
||
}
|
||
panel=summaryHtml;
|
||
}
|
||
body=`<div class="fm-step">${ctrl}${tabs}${panel}</div>`;
|
||
}
|
||
return `<div class="fm">${head}${body}</div>`;
|
||
},
|
||
onInput(bid,el){
|
||
var b=_fmEnsure(bid);if(!b||!el)return;var m=b.meeting;var k=el.getAttribute&&el.getAttribute('data-fm');
|
||
if(!k)return;
|
||
if(k==='title'){m.title=el.value||'';var tEl=document.getElementById('_titleEl');if(tEl&&!((tEl.textContent||'').trim())){tEl.textContent=m.title;if(window.E)window.E.pageTitle=m.title;}}
|
||
else if(k==='notes'){m.notes=el.value;}
|
||
else if(k==='manual'){m.manualTranscript=el.value;}
|
||
else if(k==='summary'){m.summary=el.value;}
|
||
else if(k==='consent'){m.consent=el.value;}
|
||
else return;
|
||
_fmSave();
|
||
},
|
||
act(bid,act,el){
|
||
var b=_fmEnsure(bid);if(!b)return;var m=b.meeting;var self=this;
|
||
if(act==='start'){
|
||
if(m.status==='done'||m.status==='idle'||m.status==='paused'){
|
||
if(m.status==='done'){m.lines=[];m.summary='';m.summaryError='';m.manualTranscript='';}
|
||
m.started=true;m.lastTab='transcript';
|
||
if(!self._audio()){m.status='manual';m.interim='';_fmSave();self.paint(bid);return;}
|
||
if(m.consent==='auto'&&window.speechSynthesis){
|
||
var u=new SpeechSynthesisUtterance('Cette réunion est enregistrée et transcrite par FlowDeck. Merci de votre attention.');
|
||
try{window.speechSynthesis.speak(u);}catch(e){}
|
||
}
|
||
setTimeout(function(){self._startRec(bid);},m.consent==='auto'?1300:0);
|
||
}
|
||
}else if(act==='pause'){
|
||
this._stopRec(bid,false);m.status='paused';m.interim='';_fmSave();this.paint(bid);
|
||
}else if(act==='stop'){
|
||
this._stopRec(bid,true);m.status='done';m.lastTab='transcript';m.interim='';_fmSave();this.paint(bid);
|
||
}else if(act==='new'){
|
||
m.lines=[];m.summary='';m.summaryError='';m.status='idle';m.started=false;m.lastTab='transcript';_fmSave();this.paint(bid);
|
||
}else if(act==='tab'){
|
||
m.lastTab=(el&&el.getAttribute&&el.getAttribute('data-tab'))||'transcript';_fmSave();this.paint(bid);
|
||
}else if(act==='generate'){
|
||
this._generate(bid);
|
||
}else if(act==='copy'){
|
||
var txt=(m.summary||'').trim();
|
||
if(!txt)return;
|
||
if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(txt).catch(function(){});}
|
||
}
|
||
},
|
||
_startRec(bid){
|
||
var b=_fmEnsure(bid);if(!b)return;var m=b.meeting;var s=this._rt[bid]||(this._rt[bid]={});
|
||
var SR=window.SpeechRecognition||window.webkitSpeechRecognition;
|
||
if(!SR){m.status='manual';_fmSave();this.paint(bid);return;}
|
||
if(s.rec){try{s.rec.stop();}catch(e){}}
|
||
var rec=new SR();s.rec=rec;s.offset=0;s.stopped=false;
|
||
rec.lang='fr-FR';rec.continuous=true;rec.interimResults=true;rec.maxAlternatives=1;
|
||
var self=this;
|
||
rec.onresult=function(ev){
|
||
var final='',interim='';
|
||
for(var i=(s.offset||0);i<ev.results.length;i++){
|
||
var r=ev.results[i];
|
||
if(r.isFinal)final+=(r[0]&&r[0].transcript||'');
|
||
else interim+=(r[0]&&r[0].transcript||'');
|
||
}
|
||
s.offset=ev.results.length;
|
||
if(final&&final.trim()){
|
||
m.lines.push({t:_fmTime(),who:'Vous',text:final.trim()});
|
||
_fmSave();
|
||
self._updateTranscriptOnly(bid);
|
||
}
|
||
m.interim=interim.trim();
|
||
self._updateInterim(bid);
|
||
};
|
||
rec.onend=function(){
|
||
if(!s.stopped&&m.status==='running'&&self._audio()){try{rec.start();}catch(e){}}
|
||
};
|
||
rec.onerror=function(ev){
|
||
if(ev&&ev.error==='not-allowed'){m.summaryError='Microphone refusé : autorisez le micro ou utilisez la saisie manuelle du transcript.';m.status='manual';s.stopped=true;_fmSave();self.paint(bid);}
|
||
};
|
||
m.status='running';m.interim='';_fmSave();
|
||
this.paint(bid);
|
||
try{rec.start();}catch(e){m.status='manual';s.stopped=true;_fmSave();this.paint(bid);}
|
||
},
|
||
_stopRec(bid,stop){
|
||
var b=_fmEnsure(bid);if(!b)return;var m=b.meeting;var s=this._rt[bid];
|
||
if(s&&s.rec){s.stopped=true;try{s.rec.stop();}catch(e){}}
|
||
if(stop&&m.interim&&m.interim.trim()){m.lines.push({t:_fmTime(),who:'Vous',text:m.interim.trim()});}
|
||
m.interim='';
|
||
},
|
||
_updateTranscriptOnly(bid){var s=this._rt[bid];var b=_fmEnsure(bid);if(!b||!s||!s.root)return;var el=s.root.querySelector('[data-fm-transcript-body]');if(el)el.innerHTML=this._transcriptLines(b);},
|
||
_updateInterim(bid){var s=this._rt[bid];var b=_fmEnsure(bid);if(!b||!s||!s.root)return;var el=s.root.querySelector('.fm-interim .fm-txt');if(el&&el.textContent!==(b.meeting.interim||'')){el.textContent=(b.meeting.interim||'')+'…';}},
|
||
_transcriptLines(b){var m=b.meeting;var lines=(m.lines||[]).map(function(l){return `<div class="fm-line"><span class="fm-ts">${_fmEsc(l.t||'')}</span><span class="fm-who">${_fmEsc(l.who||'Vous')}</span><span class="fm-txt">${_fmEsc(l.text)}</span></div>`;}).join('');return lines||'<div class="fm-empty">La transcription apparaîtra ici en direct…</div>';},
|
||
_generate(bid){
|
||
var b=_fmEnsure(bid);if(!b)return;var m=b.meeting;
|
||
var content=_fmTranscriptText(b);
|
||
if(!content.trim()){m.summaryError='Aucun contenu de réunion à résumer.';_fmSave();this.paint(bid);return;}
|
||
m.summaryLoading=true;m.summaryError='';_fmSave();this.paint(bid);
|
||
var self=this;
|
||
var prompt='Rédige le résumé structuré de cette réunion (format AI Meeting Note) : une ligne d\'en-tête avec la date du '+_fmEsc(m.date)+', puis les sections « Résumé » (Overview), « Décisions » et « Action items » avec des puces concrètes. Si le contenu est trop court ou fragmentaire, indique « No meaningful content could be extracted ». Ne précède pas la réponse de commentaires.';
|
||
if(!(window.fdAgent&&window.fdAgent.generate)){
|
||
m.summaryLoading=false;m.summaryError='Agent IA indisponible — configurez-le dans Paramètres → Agent & IA.';_fmSave();this.paint(bid);return;
|
||
}
|
||
window.fdAgent.generate(prompt, content).then(function(text){
|
||
m.summary=(text||'').trim();m.summaryLoading=false;m.summaryError='';_fmSave();self.paint(bid);
|
||
}).catch(function(err){
|
||
m.summaryLoading=false;m.summaryError=(err&&err.message)?err.message:'Échec de génération du résumé';_fmSave();self.paint(bid);
|
||
});
|
||
}
|
||
};
|
||
window.FDMeeting=FDMeeting;
|
||
|
||
window.E=null;
|
||
function editorState(){
|
||
return {
|
||
pid:null,pageTitle:'',blocks:[],dirty:false,saving:false,lastSaved:'',st:null,
|
||
updatedAt:'{{ page.get("updated_at", "") }}',
|
||
fmt:{open:false,top:0,left:0,idx:-1},
|
||
shareOpen:false,moreOpen:false,gsMoreOpen:false,accessMenuOpen:false,exportOpen:false,
|
||
aiBusy:false,
|
||
moveOpen:false,moveWorkspaces:[],moveLoading:false,
|
||
activityOpen:false,pageCreated:'{{ page.get("created_at", "") }}',
|
||
shareTab:'share',favorited:false,
|
||
pagePublished:{{ page_published | tojson }},
|
||
generalAccess:'{{ page_share_mode }}',publicPerm:'viewer',
|
||
pubAllowEdit:false,pubAllowComments:true,
|
||
pageUrl:window.location.href,publishedUrl:'',
|
||
inviteEmail:'',invitePermission:'editor',accessList:[],
|
||
toastVisible:false,toastMsg:'',
|
||
// ── v4.9.0: Collaboration — comments & mentions ──
|
||
commentsOpen:false,commentDraft:'',comments:[],commentCount:0,
|
||
currentUserId:{{ user.get('id', 0) if user else 0 }},
|
||
_commentSel:{range:null,bid:null,start:0,end:0},
|
||
_notifiedMentions:{},
|
||
get timeAgo() {
|
||
if (!this.updatedAt) return 'just now';
|
||
var diff = Math.floor((Date.now() - new Date(this.updatedAt + 'Z').getTime()) / 1000);
|
||
if (diff < 60) return 'just now';
|
||
if (diff < 3600) return Math.floor(diff / 60) + 'm ago';
|
||
if (diff < 86400) return Math.floor(diff / 3600) + 'h ago';
|
||
return Math.floor(diff / 86400) + 'd ago';
|
||
},
|
||
formatDate(d) {
|
||
if (!d) return '';
|
||
var dt = new Date(d + 'Z');
|
||
return dt.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) +
|
||
' at ' + dt.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' });
|
||
},
|
||
init(){
|
||
_bid=Math.floor(Math.random()*10000);
|
||
const dataEl=document.getElementById('page-data');
|
||
if(dataEl){try{const data=JSON.parse(dataEl.textContent);this.pid=data.id;this.pageTitle=data.title||'';this.favorited=data.favorited||false;const fmt=data.content_format||'blocks';const raw=data.content||'';
|
||
if(fmt==='file'){this.fileData=data;this.loadFileContent(data);}
|
||
else if(fmt==='blocks'&&raw){try{this.blocks=JSON.parse(raw);this.blocks.forEach(b=>{if(!b.id)b.id=genId()});}catch(e){this.blocks=[];}}
|
||
else if(raw&&raw.trim())this.blocks=this.md2b(raw);}catch(e){}}
|
||
if(!this.blocks.length)this.blocks=[this.mkB('paragraph','')];
|
||
this.loadInlineDBs();
|
||
window.E=this;
|
||
this.insertTable=function(){this.sync();var nb={id:genId(),type:'table',content:'',rows:_tblNew(3,3)};this.blocks.push(nb);this.dirty=true;this.autoSave();this.render();setTimeout(function(){_tblFocus(nb.id,0,0);},60);};
|
||
SM.init();_tblInitDelegate();this.render();
|
||
const tEl=document.getElementById('_titleEl');
|
||
if(tEl){const syncT=()=>{tEl.classList.toggle('empty',!(tEl.textContent||'').trim());};syncT();tEl.addEventListener('input',syncT);tEl.addEventListener('blur',syncT);}
|
||
setTimeout(()=>{this.focusBlock();const ct=document.getElementById('_blocksCt');if(!ct)return;
|
||
ct.addEventListener('keydown',e=>this.onKd(e));
|
||
ct.addEventListener('paste',e=>{e.preventDefault();document.execCommand('insertText',false,(e.clipboardData||window.clipboardData).getData('text/plain'));this.dirty=true;this.autoSave();});
|
||
if(typeof Sortable!=='undefined')Sortable.create(ct,{draggable:'.block-wrapper:not(.nested-block):not(.block-column-wrapper)',handle:'.block-handle',animation:150,ghostClass:'sortable-ghost',dragClass:'sortable-drag',onEnd:evt=>{if(evt.oldIndex===evt.newIndex)return;const[item]=this.blocks.splice(evt.oldIndex,1);this.blocks.splice(evt.newIndex,0,item);this.dirty=true;this.autoSave();this.render();}});
|
||
},100);
|
||
},
|
||
fileData:null,
|
||
async loadFileContent(data){
|
||
const url=data.file_url||'';const mime=data.file_mime||'';const name=data.file_name||'';const self=this;
|
||
if(!url){this.blocks=[this.mkB('paragraph','File not available')];this.render();return;}
|
||
const ext=name.toLowerCase().split('.').pop();
|
||
if(mime.startsWith('image/')||['png','jpg','jpeg','gif','webp','svg','bmp','ico'].includes(ext)){
|
||
this.blocks=[this.mkB('image','',{src:url,alt:name})];this.render();return;
|
||
}
|
||
if(mime==='application/pdf'||ext==='pdf'){
|
||
this.blocks=[this.mkB('embed','',{src:url,embed_type:'pdf'})];this.render();return;
|
||
}
|
||
const codeExts=['py','js','ts','jsx','tsx','html','htm','css','json','xml','yaml','yml','md','markdown','sql','sh','bash','ps1','bat','cmd','rb','go','rs','java','c','cpp','h','hpp','php','swift','kt','scala','r','toml','ini','cfg','conf','env','txt','log'];
|
||
const langMap={'py':'python','js':'javascript','ts':'typescript','jsx':'javascript','tsx':'typescript','html':'html','htm':'html','css':'css','json':'json','xml':'xml','yaml':'yaml','yml':'yaml','md':'markdown','markdown':'markdown','sql':'sql','sh':'bash','bash':'bash','ps1':'powershell','bat':'plaintext','cmd':'plaintext','rb':'ruby','go':'go','rs':'rust','java':'java','c':'c','cpp':'cpp','h':'c','hpp':'cpp','php':'php','swift':'swift','kt':'kotlin','scala':'scala','r':'r','toml':'toml','ini':'ini','cfg':'ini','conf':'ini','env':'ini','txt':'plaintext','log':'plaintext'};
|
||
if(mime.startsWith('text/')||codeExts.includes(ext)){
|
||
try{const r=await fetch(url);const text=await r.text();
|
||
if(ext==='md'||ext==='markdown'){this.blocks=this.md2b(text);}
|
||
else{this.blocks=[this.mkB('code',text,{language:langMap[ext]||'Plain Text'})];}
|
||
this.render();
|
||
}catch(e){this.blocks=[this.mkB('paragraph','Error loading file: '+e.message)];this.render();}
|
||
return;
|
||
}
|
||
this.blocks=[this.mkB('embed','',{src:url,embed_type:'download',file_name:name,file_size:data.file_size||0,file_mime:mime})];this.render();
|
||
},
|
||
mkB(type,content,extras){const b={id:genId(),type,content:content||'',...(extras||{})};if(type==='toggle')b.expanded=true;if(type==='to_do')b.checked=false;if(type==='columns')b.children=[{id:genId(),type:'paragraph',content:''}];if(type==='toggle')b.children=[];return b;},
|
||
getEl(bid){const ct=document.getElementById('_blocksCt');return ct?ct.querySelector(`[data-bid="${bid}"]`):null;},
|
||
getIdx(bid){return this.blocks.findIndex(b=>b.id===bid);},
|
||
getActiveBlock(){const el=document.activeElement;if(!el||!el.hasAttribute('data-bid'))return null;return{el,bid:el.dataset.bid,idx:this.getIdx(el.dataset.bid)};},
|
||
focusBlock(){const b=this.blocks[0];if(!b)return;const el=this.getEl(b.id);if(el){el.focus();ce(el);}},
|
||
onCtClick(e){if(e.target===document.getElementById('_blocksCt')||e.target?.classList?.contains('blocks-empty')){if(!this.blocks.length)this.addAt(0);else{const b=this.blocks[this.blocks.length-1];if(b&&(b.type==='table'||b.type==='divider'||b.type==='image'||b.type==='embed')){this.addAt(this.blocks.length,'paragraph');}else{const el=b&&this.getEl(b.id);if(el){el.focus();ce(el);}}}}},
|
||
sync(){const ct=document.getElementById('_blocksCt');if(!ct)return;for(const b of this.blocks){if(b.type==='table'){const wrap=ct.querySelector(`[data-bid="${b.id}"].ftable-editor`);if(wrap){const rows=[];wrap.querySelectorAll('tr.ftable-row').forEach(function(tr){const cells=tr.querySelectorAll('th.ftable-cell,td.ftable-cell');if(!cells.length)return;const rowarr=[];cells.forEach(function(cell){rowarr.push(cell.textContent||'');});rows.push(rowarr);});if(rows.length)b.rows=rows;}continue;}
|
||
if(b.type==='divider'||b.type==='image'||b.type==='embed'||b.type==='table_of_contents')continue;
|
||
if(b.type==='math'){const te=ct.querySelector(`[data-math-bid="${b.id}"]`);if(te)b.content=te.value||'';continue;}
|
||
const el=ct.querySelector(`[data-bid="${b.id}"]`);if(el&&!el.classList.contains('toggle-title'))b.content=gt(el)||'';else if(el)b.content=gt(el)||'';
|
||
if(b.type==='toggle'&&b.children)b.children.forEach(function(ch){const che=ct.querySelector(`[data-bid="${ch.id}"]`);if(che)ch.content=gt(che)||'';});
|
||
if(b.type==='columns'&&b.children)b.children.forEach(function(ch){const che=ct.querySelector(`[data-bid="${ch.id}"]`);if(che)ch.content=gt(che)||'';});
|
||
}},
|
||
render(){const ct=document.getElementById('_blocksCt');if(!ct)return;let html='';for(let i=0;i<this.blocks.length;i++)html+=renderBlock(this.blocks[i],i);ct.innerHTML=html;
|
||
// Render math blocks with KaTeX
|
||
var self=this;
|
||
ct.querySelectorAll('.math-display').forEach(function(el){var bid=el.dataset.bid;var b=self.blocks.find(function(x){return x.id===bid;});self._renderKatex(el,b?b.content:'');});
|
||
ct.querySelectorAll('.math-editor').forEach(function(ta){var disp=ct.querySelector(`.math-display[data-bid="${ta.dataset.mathBid}"]`);ta.addEventListener('input',function(){self._renderKatex(disp,ta.value);self.dirty=true;self.autoSave();});});
|
||
// Initialize inline database blocks (scripts in innerHTML don't execute)
|
||
setTimeout(()=>{ct.querySelectorAll('.block-embed-collection').forEach(el=>{const cid=parseInt(el.id.replace('inline-db-',''));if(cid&&window.FlowDeckDB)window.FlowDeckDB.renderInto(el,cid,null);});},50);
|
||
// Initialize AI Meeting Note widgets (block type 'meeting')
|
||
setTimeout(()=>{ct.querySelectorAll('[data-meeting-bid]').forEach(function(el){if(window.FDMeeting)window.FDMeeting.mount(el.getAttribute('data-meeting-bid'),el);});},50);
|
||
ct.querySelectorAll('[data-bid]').forEach(el=>{el.addEventListener('input',()=>{self.dirty=true;self.autoSave();if(el.hasAttribute&&el.hasAttribute('data-placeholder'))el.classList.toggle('empty',!(el.textContent||'').trim());})});
|
||
ct.querySelectorAll('[data-placeholder]').forEach(el=>{el.innerHTML=el.innerHTML.replace(/<br[^>]*>/gi,'').trim()!==''?el.innerHTML:'';el.classList.toggle('empty',!el.textContent.trim());});
|
||
},
|
||
_renderKatex(el,tex){
|
||
if(!el)return;if(!tex||!tex.trim()){el.innerHTML='<span style="color:var(--text-dim);font-style:italic;">Empty math block</span>';return;}
|
||
if(window.katex){try{el.innerHTML=window.katex.renderToString(tex,{displayMode:true,throwOnError:false});}catch(e){el.innerHTML='<span style="color:#e5484d;">Invalid LaTeX</span>';}}
|
||
else{el.innerHTML='<span>'+esc(tex)+'</span>';}
|
||
},
|
||
replaceBlock(idx,type){const b=this.blocks[idx];if(!b)return;this.sync();b.type=type;b.content='';if(type==='toggle'){b.expanded=true;b.children=[];}if(type==='to_do')b.checked=false;if(type==='columns')b.children=[{id:genId(),type:'paragraph',content:''}];if(type==='math'){}this.render();this.dirty=true;this.autoSave();setTimeout(()=>{const el=this.getEl(b.id);if(el)el.focus();},60);},
|
||
addAt(idx,type,content){this.sync();const b=this.mkB(type||'paragraph',content||'');this.blocks.splice(idx,0,b);this.dirty=true;this.autoSave();this.render();setTimeout(()=>{const el=this.getEl(b.id);if(el&&type!=='divider'){el.focus();content&&ce(el);}},60);return b;},
|
||
addAfter(idx,type){return this.addAt(idx+1,type);},
|
||
removeBlock(idx){this.sync();if(this.blocks.length<=1){this.blocks[0]=this.mkB('paragraph','');}else{this.blocks.splice(idx,1);}const fi=Math.min(idx,this.blocks.length-1);this.render();this.dirty=true;this.autoSave();setTimeout(()=>{const el=this.getEl(this.blocks[fi]?.id);if(el){el.focus();ce(el);}},60);},
|
||
|
||
// ── v4.11.1: Inline AI — "Ask AI" & "AI meeting note" (Get started) ──
|
||
// Wire the contextual get-started actions into the FlowDeck Agent
|
||
// (Notion-style inline generation). The reply is streamed in the agent
|
||
// panel and inserted into this page via window.__fdInsertAI.
|
||
askAI(){
|
||
const self=this;
|
||
const title=(this.pageTitle||'Sans titre').trim();
|
||
if(!(window.fdAgent && window.fdAgent.generate)){ self.showToast('Agent IA indisponible — configurez-le dans Paramètres → Agent & IA'); return; }
|
||
if(self.aiBusy) return;
|
||
self.aiBusy=true;
|
||
self.showToast('FlowDeck Agent : génération en cours…');
|
||
let ctx='';
|
||
try{ if(window.fdGetPageContext) ctx=String(window.fdGetPageContext()||''); }catch(e){}
|
||
const prompt='Rédige le contenu de la page FlowDeck intitulée « '+title+' ». Fournis un texte structuré et directement utilisable : paragraphes et, si utile, des listes à puces. Sépare chaque paragraphe par une ligne vide. Ne précède pas le texte de commentaires.';
|
||
window.fdAgent.generate(prompt, ctx).then(function(text){
|
||
self.aiBusy=false;
|
||
if(text && text.trim()){ self.applyAIBlocks(text); }
|
||
else { self.showToast('Aucun retour généré par l\u2019agent'); }
|
||
}).catch(function(err){
|
||
self.aiBusy=false;
|
||
self.showToast((err && err.message) ? err.message : 'Erreur de génération');
|
||
});
|
||
},
|
||
meetingNote(){
|
||
// v4.12.0: crée une vraie « boîte de réunion » (bloc 'meeting') au lieu
|
||
// d'insérer du texte. La page est renommée « Meeting @date » si elle
|
||
// est vide, puis le widget est ajouté (ou remplace le bloc vide initial).
|
||
var self=this;
|
||
this.gsMoreOpen=false;
|
||
var today=new Date();
|
||
var iso=today.toISOString().slice(0,10);
|
||
var title='Meeting @'+iso;
|
||
var tEl=document.getElementById('_titleEl');
|
||
var curTitle=tEl?(tEl.textContent||'').trim():'';
|
||
var blank=!curTitle||curTitle==='Untitled'||curTitle==='New page';
|
||
if(blank){
|
||
this.pageTitle=title;
|
||
if(tEl)tEl.textContent=title;
|
||
}
|
||
var mb=this.mkB('meeting','');
|
||
mb.meeting={
|
||
title:title,
|
||
date:iso,
|
||
consent:'self',
|
||
started:false,
|
||
status:'idle',
|
||
lines:[],
|
||
interim:'',
|
||
notes:'',
|
||
summary:'',
|
||
summaryError:'',
|
||
lastTab:'transcript'
|
||
};
|
||
this.sync();
|
||
var emptyFirst=this.blocks.length===1 && this.blocks[0].type==='paragraph' && !(this.blocks[0].content||'').trim();
|
||
if(emptyFirst){ this.blocks=[mb]; } else { this.blocks.push(mb); }
|
||
this.dirty=true; this.autoSave(); this.render();
|
||
this.showToast('AI Meeting Note : bloc de réunion ajouté. Cliquez sur « Start transcribing ».');
|
||
var bid=mb.id;
|
||
setTimeout(function(){ var el=document.querySelector('[data-meeting-bid="'+bid+'"]'); if(el&&el.scrollIntoView)el.scrollIntoView({behavior:'smooth',block:'center'}); if(el&&window.FDMeeting)window.FDMeeting.focus(bid); },120);
|
||
},
|
||
aiRun(prompt){
|
||
const self=this;
|
||
if(self.aiBusy) return;
|
||
if(!(window.fdAgent && window.fdAgent.ask)){ self.showToast('Agent IA indisponible — configurez-le dans Paramètres → Agent & IA'); return; }
|
||
self.aiBusy=true;
|
||
window.__fdInsertAI = function(content){ window.__fdInsertAI=null; self.aiBusy=false; self.applyAIBlocks(content); };
|
||
self.showToast('FlowDeck Agent : génération en cours…');
|
||
window.fdAgent.ask(prompt);
|
||
},
|
||
applyAIBlocks(text){
|
||
// Convert agent markdown → real blocks (headings, lists, tables, code…)
|
||
// so it renders naturally (Notion-style) instead of showing raw `## …`.
|
||
const nb=this.md2b(text||'');
|
||
if(!nb.length) return;
|
||
this.sync();
|
||
const startEmpty=this.blocks.length===1 && this.blocks[0].type==='paragraph' && !(this.blocks[0].content||'').trim();
|
||
this.blocks = startEmpty ? nb : this.blocks.concat(nb);
|
||
this.dirty=true; this.autoSave(); this.render();
|
||
},
|
||
|
||
toggleFavorite(){
|
||
const csrf=document.cookie.match(/csrf_token=([^;]+)/);
|
||
const m=this.favorited?'DELETE':'POST';
|
||
fetch(`/board/api/favorites/${this.pid}`,{method:m,headers:{'X-CSRF-Token':csrf?csrf[1]:''}})
|
||
.then(r=>r.json()).then(()=>{this.favorited=!this.favorited;this.showToast(this.favorited?'Added to favorites':'Removed from favorites');
|
||
if(window.appState&&window.appState.refreshFavorites)window.appState.refreshFavorites();
|
||
}).catch(()=>{this.showToast('Failed to toggle favorite');});
|
||
},
|
||
toggleActivityOpen(){
|
||
if(!this.activityOpen){this.activityOpen=true;this._justOpened=true;setTimeout(()=>this._justOpened=false,200);}
|
||
else{this.activityOpen=false;}
|
||
},
|
||
toggleShareOpen(){
|
||
if(!this.shareOpen){this.shareOpen=true;this._justOpened=true;setTimeout(()=>this._justOpened=false,200);}
|
||
else{this.shareOpen=false;}
|
||
},
|
||
toggleMoreOpen(){
|
||
if(!this.moreOpen){this.moreOpen=true;this._justOpened=true;setTimeout(()=>this._justOpened=false,200);}
|
||
else{this.moreOpen=false;}
|
||
},
|
||
_justOpened:false,
|
||
togglePublish(){this.pagePublished=!this.pagePublished;this.saveShare();},
|
||
saveShare(){
|
||
const csrf=document.cookie.match(/csrf_token=([^;]+)/);
|
||
fetch(`/board/api/share/${this.pid}`,{method:'POST',headers:{'Content-Type':'application/json','X-CSRF-Token':csrf?csrf[1]:''},body:JSON.stringify({mode:this.generalAccess,published:this.pagePublished})}).catch(()=>{});
|
||
},
|
||
async copyPageLink(){
|
||
console.log('copyPageLink invoked');
|
||
const url = this.pageUrl;
|
||
const fallback = () => {
|
||
const ta = document.createElement('textarea');
|
||
ta.value = url; ta.style.position='fixed'; ta.style.opacity='0';
|
||
document.body.appendChild(ta); ta.select();
|
||
document.execCommand('copy'); document.body.removeChild(ta);
|
||
};
|
||
try {
|
||
await navigator.clipboard.writeText(url);
|
||
this.showToast('Link copied to clipboard');
|
||
} catch(e) { fallback(); this.showToast('Link copied to clipboard'); }
|
||
},
|
||
showToast(msg){
|
||
this.toastMsg = msg; this.toastVisible = true;
|
||
clearTimeout(this._toastTimer);
|
||
this._toastTimer = setTimeout(() => { this.toastVisible = false; }, 2500);
|
||
},
|
||
publishPage() {
|
||
var self = this;
|
||
var csrf = document.cookie.match(/csrf_token=([^;]+)/);
|
||
fetch('/api/pages/' + this.pid + '/publish', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf ? csrf[1] : '' }
|
||
}).then(function(r){ return r.json(); }).then(function(d){
|
||
if (d.is_published) {
|
||
self.pagePublished = true;
|
||
self.publishedUrl = window.location.origin + '/p/' + d.publish_slug;
|
||
self.showToast('Published to web — ' + self.publishedUrl);
|
||
} else {
|
||
self.showToast(d.detail || 'Publish failed');
|
||
}
|
||
}).catch(function(){ self.showToast('Publish failed'); });
|
||
},
|
||
unpublishPage() {
|
||
var self = this;
|
||
var csrf = document.cookie.match(/csrf_token=([^;]+)/);
|
||
fetch('/api/pages/' + this.pid + '/publish', {
|
||
method: 'DELETE',
|
||
headers: { 'X-CSRF-Token': csrf ? csrf[1] : '' }
|
||
}).then(function(r){ return r.json(); }).then(function(d){
|
||
if (!d.is_published) {
|
||
self.pagePublished = false;
|
||
self.publishedUrl = '';
|
||
self.showToast('Unpublished');
|
||
} else {
|
||
self.showToast(d.detail || 'Unpublish failed');
|
||
}
|
||
}).catch(function(){ self.showToast('Unpublish failed'); });
|
||
},
|
||
shareInvite() {
|
||
var self = this;
|
||
if (!this.inviteEmail.trim()) return;
|
||
var csrf = document.cookie.match(/csrf_token=([^;]+)/);
|
||
fetch('/api/pages/' + this.pid + '/share', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf ? csrf[1] : '' },
|
||
body: JSON.stringify({ email: this.inviteEmail.trim(), permission: this.invitePermission })
|
||
}).then(function(r){ return r.json(); }).then(function(d){
|
||
if (d.status === 'shared') { self.inviteEmail = ''; self.showToast('Invitation sent'); self.loadShares(); }
|
||
else { self.showToast(d.detail || 'Share failed'); }
|
||
}).catch(function(){ self.showToast('Share failed'); });
|
||
},
|
||
loadShares() {
|
||
var self = this;
|
||
fetch('/api/pages/' + this.pid + '/shares').then(function(r){ return r.json(); }).then(function(d){
|
||
self.accessList = d.shares || [];
|
||
}).catch(function(){ self.accessList = []; });
|
||
},
|
||
removeShare(sid) {
|
||
var self = this;
|
||
var csrf = document.cookie.match(/csrf_token=([^;]+)/);
|
||
fetch('/api/pages/' + this.pid + '/share/' + sid, {
|
||
method: 'DELETE',
|
||
headers: { 'X-CSRF-Token': csrf ? csrf[1] : '' }
|
||
}).then(function(r){ return r.json(); }).then(function(d){
|
||
if (d.status === 'removed') { self.showToast('Share removed'); self.loadShares(); }
|
||
else { self.showToast(d.detail || 'Remove failed'); }
|
||
}).catch(function(){ self.showToast('Remove failed'); });
|
||
},
|
||
invitePerson(){if(!this.inviteEmail.trim())return;this.accessList.push({email:this.inviteEmail,permission:this.invitePermission,permOpen:false});this.inviteEmail='';},
|
||
removeAccess(email){this.accessList=this.accessList.filter(a=>a.email!==email);},
|
||
exportPage(fmt){
|
||
this.moreOpen=false;
|
||
this.exportOpen=false;
|
||
// Ensure latest blocks are persisted before exporting any format
|
||
var csrf = document.cookie.match(/csrf_token=([^;]+)/);
|
||
var self = this;
|
||
var doDownload = function(){
|
||
var title = encodeURIComponent((self.pageTitle || 'Untitled').trim() || 'Untitled');
|
||
var a = document.createElement('a');
|
||
a.href = '/api/export/' + (fmt==='markdown'?'markdown':fmt==='html'?'html':fmt==='pdf'?'pdf':'site') + '/' + self.pid;
|
||
a.download = '';
|
||
document.body.appendChild(a);
|
||
a.click();
|
||
document.body.removeChild(a);
|
||
var label = {markdown:'Markdown',html:'HTML',pdf:'PDF',site:'Site'}[fmt]||'';
|
||
self.showToast('Exported as ' + label);
|
||
};
|
||
if (this.dirty) {
|
||
this.save(function(){ doDownload(); });
|
||
} else {
|
||
doDownload();
|
||
}
|
||
},
|
||
duplicatePage(){this.moreOpen=false;const csrf=document.cookie.match(/csrf_token=([^;]+)/);const t=(this.pageTitle||'').trim()||'New Page';fetch(`/board/api/pages?title=${encodeURIComponent(t+' copy')}§ion=Private&project=${encodeURIComponent('{{ workspace_key }}')}`,{method:'POST',headers:{'X-CSRF-Token':csrf?csrf[1]:''}}).then(r=>r.json()).then(d=>{window.location.href=`/pages/${d.id}`;}).catch(()=>{window.showToast('Duplicate failed','error');});},
|
||
movePage(){
|
||
this.moreOpen=false;
|
||
this.moveOpen = true;
|
||
this.moveLoading = true;
|
||
var self = this;
|
||
fetch('/api/workspaces')
|
||
.then(function(r){ return r.json(); })
|
||
.then(function(d){
|
||
self.moveWorkspaces = d.workspaces || [];
|
||
self.moveLoading = false;
|
||
})
|
||
.catch(function(){ self.moveLoading = false; });
|
||
},
|
||
doMove(wsId) {
|
||
this.moveOpen = false;
|
||
var csrf = document.cookie.match(/csrf_token=([^;]+)/);
|
||
var self = this;
|
||
fetch('/api/pages/' + this.pid + '/move', {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf ? csrf[1] : '' },
|
||
body: JSON.stringify({ workspace_id: wsId })
|
||
}).then(function(r){ return r.json(); })
|
||
.then(function(d){
|
||
if (d.status === 'ok') {
|
||
self.showToast('Moved to workspace');
|
||
setTimeout(function(){ window.location.href = '/local-workspace'; }, 800);
|
||
} else {
|
||
self.showToast(d.detail || 'Move failed');
|
||
}
|
||
})
|
||
.catch(function(){ self.showToast('Move failed'); });
|
||
},
|
||
deletePage(){this.moreOpen=false;if(!confirm('Move to Trash?'))return;const csrf=document.cookie.match(/csrf_token=([^;]+)/);fetch(`/board/api/pages/${this.pid}/trash`,{method:'POST',headers:{'X-CSRF-Token':csrf?csrf[1]:''}}).then(()=>{window.location.href='/';}).catch(()=>{window.showToast('Failed','error');});},
|
||
|
||
// ── v4.6.0 / v5.3.0: Get Started actions — Database (template picker) ──
|
||
dbTplOpen:false, dbTplLoading:false, dbTemplates:[], dbTplTargetIdx:-1,
|
||
async createDatabase(){
|
||
this.openDbTemplatePicker(-1);
|
||
},
|
||
async openDbTemplatePicker(targetIdx){
|
||
this.gsMoreOpen=false;
|
||
this.dbTplOpen=true; this.dbTplTargetIdx=targetIdx; this.dbTplLoading=true;
|
||
var self=this;
|
||
try{
|
||
const r=await fetch('/workspace/templates/database');
|
||
const d=await r.json();
|
||
self.dbTemplates=d.templates||[];
|
||
}catch(e){ self.dbTemplates=[]; }
|
||
self.dbTplLoading=false;
|
||
self.$nextTick(()=>{ const f=document.getElementById('fd-dbtpl'); if(f) setTimeout(()=>{f.style.opacity='1';},10); });
|
||
},
|
||
closeDbTemplatePicker(){ this.dbTplOpen=false; },
|
||
async createDbFromTemplate(tplName){
|
||
const self=this;
|
||
var name = tplName ? (tplName+' — '+new Date().toLocaleDateString('fr-FR')) : 'Database';
|
||
const csrf=document.cookie.match(/csrf_token=([^;]+)/);
|
||
try{
|
||
const body={name:name, parent_page_id:this.pid};
|
||
if(tplName) body.template=tplName;
|
||
const r=await fetch('/db/inline/api',{
|
||
method:'POST',headers:{'Content-Type':'application/json','X-CSRF-Token':csrf?csrf[1]:''},
|
||
body:JSON.stringify(body)
|
||
});
|
||
const d=await r.json();
|
||
if(d.id){
|
||
const nb=this.mkB('embed','',{embed_type:'collection',collection_id:d.id});
|
||
this.sync();
|
||
const idx=this.dbTplTargetIdx>=0?this.dbTplTargetIdx:(this.blocks.length);
|
||
this.blocks.splice(idx,0,nb);
|
||
this.dbTplOpen=false;
|
||
this.dirty=true; this.autoSave(); this.render();
|
||
this.showToast(tplName?('Database created from template — '+tplName):'Database created');
|
||
}else{
|
||
this.showToast(d.detail||'Failed to create database');
|
||
}
|
||
}catch(e){ this.showToast('Error: '+e.message); }
|
||
},
|
||
async createForm(){
|
||
const name=prompt('Form name:','New Form');
|
||
if(!name||!name.trim())return;
|
||
const csrf=document.cookie.match(/csrf_token=([^;]+)/);
|
||
try{
|
||
const r=await fetch('/db/inline/api',{
|
||
method:'POST',headers:{'Content-Type':'application/json','X-CSRF-Token':csrf?csrf[1]:''},
|
||
body:JSON.stringify({name:name.trim(),parent_page_id:this.pid})
|
||
});
|
||
const d=await r.json();
|
||
if(d.id){
|
||
this.inlineDBs.push({id:d.id,name:d.name,view_type:'form'});
|
||
this.showToast('Form created — '+d.name);
|
||
this.saveDBsToPage();
|
||
this.renderInlineDBs();
|
||
}else{
|
||
this.showToast('Failed to create form');
|
||
}
|
||
}catch(e){this.showToast('Error: '+e.message);}
|
||
},
|
||
saveDBsToPage(){
|
||
// Store inline DB refs in the page's blocks as a special embed block
|
||
if(this.inlineDBs.length===0)return;
|
||
const existing=this.blocks.filter(b=>b.type==='embed'&&b.embed_type==='inline_dbs');
|
||
if(existing.length===0){
|
||
this.blocks.push(this.mkB('embed','',{embed_type:'inline_dbs',dbs:this.inlineDBs}));
|
||
}else{
|
||
existing.forEach(b=>{b.dbs=this.inlineDBs;});
|
||
}
|
||
this.render();
|
||
this.dirty=true;this.autoSave();
|
||
},
|
||
renderInlineDBs(){
|
||
// Re-render with inline DB blocks
|
||
this.render();
|
||
},
|
||
loadInlineDBs(){
|
||
const existing=this.blocks.filter(b=>b.type==='embed'&&b.embed_type==='inline_dbs');
|
||
if(existing.length>0){
|
||
this.inlineDBs=existing[0].dbs||[];
|
||
}
|
||
},
|
||
|
||
onKd(e){
|
||
const ab=this.getActiveBlock();if(!ab)return;
|
||
const{el,bid,idx}=ab;const block=this.blocks[idx];if(!block)return;
|
||
if(SM.isOpen){
|
||
if(e.key==='Escape'){e.preventDefault();SM.close();return;}
|
||
if(e.key==='ArrowDown'||e.key==='ArrowUp'||e.key==='Enter'){e.preventDefault();return;}
|
||
if(e.key==='Backspace'&&!SM._q&&(el.textContent||'').trim()==='/'){e.preventDefault();SM.close();return;}
|
||
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===' '){
|
||
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();
|
||
const sp=splitCaret(el);
|
||
const before=sp?sp.before:gt(el)||'',after=sp?sp.after:'';
|
||
if(!before.trim()&&!after.trim()){
|
||
const nt=(block.type==='bulleted_list'||block.type==='numbered_list'||block.type==='to_do')?block.type:'paragraph';
|
||
this.sync();this.blocks.splice(idx+1,0,this.mkB(nt,''));this.render();
|
||
setTimeout(()=>{const ne=this.getEl(this.blocks[idx+1]?.id);if(ne){ne.focus();ce(ne);}},60);
|
||
this.dirty=true;this.autoSave();return;
|
||
}
|
||
const nt=(block.type==='bulleted_list'||block.type==='numbered_list'||block.type==='to_do')?block.type:'paragraph';
|
||
this.sync();block.content=before;this.blocks.splice(idx+1,0,this.mkB(nt,after));this.render();
|
||
setTimeout(()=>{const ne=this.getEl(this.blocks[idx+1]?.id);if(ne){ne.focus();cs(ne);}},60);
|
||
this.dirty=true;this.autoSave();return;
|
||
}
|
||
if(e.key==='ArrowUp'){const _sp=splitCaret(el);if(_sp&&!_sp.before.trim()&&idx>0){e.preventDefault();const prev=this.getEl(this.blocks[idx-1]?.id);if(prev){prev.focus();ce(prev);}}return;}
|
||
if(e.key==='ArrowDown'){const _sp=splitCaret(el);if(_sp&&!_sp.after.trim()&&idx<this.blocks.length-1){e.preventDefault();const next=this.getEl(this.blocks[idx+1]?.id);if(next){next.focus();cs(next);}}return;}
|
||
if(e.key==='Backspace'){if(!(el.textContent||'').trim()){this.sync();if(block.type!=='paragraph'){e.preventDefault();block.type='paragraph';block.content='';this.render();setTimeout(()=>{const ne=this.getEl(block.id);if(ne)ne.focus();},60);}else if(this.blocks.length>1){e.preventDefault();this.removeBlock(idx);}}return;}
|
||
if(e.key===' '){const b2=bid;setTimeout(()=>this.checkMd(b2),15);}
|
||
if((e.ctrlKey||e.metaKey)&&!e.altKey){const m={b:'bold',i:'italic',u:'underline'};if(m[e.key]){e.preventDefault();document.execCommand(m[e.key]);this.dirty=true;this.autoSave();}}
|
||
if(e.key==='Tab'&&block.type==='code'){e.preventDefault();document.execCommand('insertText',false,' ');}
|
||
this.dirty=true;this.autoSave();
|
||
},
|
||
checkMd(idx){
|
||
if(idx<0||idx>=this.blocks.length)return;const block=this.blocks[idx];const el=this.getEl(block.id);if(!el)return;const text=el.textContent||'';
|
||
const sc={'# ':'heading_1','## ':'heading_2','### ':'heading_3','#### ':'heading_4','- ':'bulleted_list','* ':'bulleted_list','+ ':'bulleted_list','1. ':'numbered_list','[] ':'to_do','[ ] ':'to_do','> ':'quote'};
|
||
this.sync();
|
||
for(const[pfx,type] of Object.entries(sc)){if(text===pfx||text.startsWith(pfx)){block.type=type;block.content='';if(type==='toggle')block.expanded=true;if(type==='to_do')block.checked=false;this.render();setTimeout(()=>{const ne=this.getEl(block.id);if(ne&&type!=='divider')ne.focus();},60);return;}}
|
||
if(text==='---'){block.type='divider';block.content='';this.addAfter(idx,'paragraph');}
|
||
if(text==='```'){block.type='code';block.content='';block.language='Plain Text';this.render();setTimeout(()=>{const ne=this.getEl(block.id);if(ne)ne.focus();},60);}
|
||
},
|
||
applySlash(id){
|
||
const idx=SM._i;if(idx<0||idx>=this.blocks.length)return;
|
||
if(id==='database'){ this.openDbTemplatePicker(idx); return; }
|
||
const block=this.blocks[idx];this.sync();block.content='';
|
||
if(id==='divider'){block.type='divider';this.addAfter(idx,'paragraph');}
|
||
else{block.type=id;if(id==='toggle'){block.expanded=true;block.children=[];}if(id==='to_do')block.checked=false;if(id==='columns')block.children=[{id:genId(),type:'paragraph',content:''}];if(id==='code')block.language='Plain Text';if(id==='table'){block.rows=_tblNew(3,3);block.content='';}}
|
||
this.render();this.dirty=true;this.autoSave();
|
||
if(id==='table'){setTimeout(()=>_tblFocus(block.id,1,0),60);}
|
||
else if(id==='math'){setTimeout(()=>{const ne=document.querySelector(`[data-math-bid="${block.id}"]`);if(ne)ne.focus();},60);}
|
||
else if(id==='columns'){setTimeout(()=>{const ne=this.getEl(block.children[0].id);if(ne)ne.focus();},60);}
|
||
else{setTimeout(()=>{const ne=this.getEl(block.id);if(ne)ne.focus();},60);}
|
||
},
|
||
_doToggle(bid,v){const idx=this.getIdx(bid);if(idx>=0){this.blocks[idx].checked=v;this.dirty=true;this.autoSave();}},
|
||
_doToggleExpand(bid){const idx=this.getIdx(bid);if(idx>=0){this.blocks[idx].expanded=!this.blocks[idx].expanded;this.render();this.dirty=true;this.autoSave();}},
|
||
_addColumn(parentId){const idx=this.getIdx(parentId);if(idx<0)return;const b=this.blocks[idx];b.children.push({id:genId(),type:'paragraph',content:''});this.render();this.dirty=true;this.autoSave();setTimeout(()=>{const ne=this.getEl(b.children[b.children.length-1].id);if(ne)ne.focus();},60);},
|
||
_removeColumn(parentId,childId){const idx=this.getIdx(parentId);if(idx<0)return;const b=this.blocks[idx];b.children=b.children.filter(function(c){return c.id!==childId;});if(b.children.length===0)b.children=[{id:genId(),type:'paragraph',content:''}];this.render();this.dirty=true;this.autoSave();},
|
||
_toggleCalloutIcon(bid){const picker=document.getElementById('callout-picker-'+bid);if(picker)picker.style.display=picker.style.display==='none'?'flex':'none';},
|
||
_setCalloutIcon(bid,icon){const idx=this.getIdx(bid);if(idx>=0){this.blocks[idx].icon=icon;this.dirty=true;this.autoSave();this.render();}},
|
||
showFmt(idx){const s=window.getSelection();if(!s.rangeCount||s.isCollapsed){this.fmt.open=false;return;}const r=s.getRangeAt(0).getBoundingClientRect();this.fmt.open=true;this.fmt.idx=idx;this.fmt.top=Math.max(r.top-44,0);this.fmt.left=Math.min(r.left+r.width/2-120,window.innerWidth-260);this._sel={range:s.getRangeAt(0).cloneRange(),idx};},
|
||
fmtApply(f){this.fmt.open=false;if(!this._sel)return;const s=window.getSelection();s.removeAllRanges();s.addRange(this._sel.range);document.execCommand(f==='strikeThrough'?'strikeThrough':f);this._sel=null;this.dirty=true;this.autoSave();},
|
||
fmtLink(){this.fmt.open=false;const u=prompt('URL:');if(u){document.execCommand('createLink',false,u);this.dirty=true;this.autoSave();}},
|
||
// ── v4.9.0: Collaboration — comments & mentions ──
|
||
toggleComments(){this.commentsOpen=!this.commentsOpen;if(this.commentsOpen)this.loadComments();},
|
||
async loadComments(){try{const r=await fetch('/api/pages/'+this.pid+'/comments',{credentials:'same-origin'});const d=await r.json();this.comments=d.comments||[];this.commentCount=this.comments.length;}catch(e){this.comments=[];}},
|
||
fmtTime(s){if(!s)return '';const t=new Date((String(s).includes('T')||String(s).includes('Z'))?s:(s+'Z'));if(isNaN(t.getTime()))t=new Date(s);const diff=Math.floor((Date.now()-t.getTime())/1000);if(diff<60)return 'just now';if(diff<3600)return Math.floor(diff/60)+'m ago';if(diff<86400)return Math.floor(diff/3600)+'h ago';return Math.floor(diff/86400)+'d ago';},
|
||
csrfTok(){return (document.cookie.match(/csrf_token=([^;]+)/)||[])[1]||'';},
|
||
async addPageComment(){
|
||
const text=(this.commentDraft||'').trim();if(!text)return;
|
||
const self=this;const sel=this._commentSel;
|
||
try{
|
||
const r=await fetch('/api/pages/'+this.pid+'/comments',{method:'POST',credentials:'same-origin',
|
||
headers:{'Content-Type':'application/json','X-CSRF-Token':this.csrfTok()},
|
||
body:JSON.stringify({body:text,anchor_block_id:sel.bid||null,anchor_start:sel.bid?sel.start:null,anchor_end:sel.bid?sel.end:null})});
|
||
const d=await r.json();
|
||
if(d.status==='created'){this.commentDraft='';this._commentSel={range:null,bid:null,start:0,end:0};this.hideCommentBtn();await this.loadComments();this.showToast('Comment added');}
|
||
else{this.showToast(d.detail||'Failed to add comment');}
|
||
}catch(e){this.showToast('Failed to add comment');}
|
||
},
|
||
async resolveComment(id){try{await fetch('/api/comments/'+id,{method:'PUT',credentials:'same-origin',headers:{'Content-Type':'application/json','X-CSRF-Token':this.csrfTok()},body:JSON.stringify({resolved:true})});await this.loadComments();}catch(e){}},
|
||
async deleteComment(id){if(!confirm('Delete this comment?'))return;try{await fetch('/api/comments/'+id,{method:'DELETE',credentials:'same-origin',headers:{'X-CSRF-Token':this.csrfTok()}});await this.loadComments();}catch(e){}},
|
||
commentOnSelection(){
|
||
const sel=this._commentSel;if(!sel.range)return;
|
||
const bid=sel.bid||'';this.commentsOpen=true;this.commentDraft='';
|
||
const btn=document.getElementById('_commentSelBtn');if(btn)btn.style.display='none';
|
||
this.$nextTick(()=>{const ta=document.querySelector('.comments-drawer textarea');if(ta)ta.focus();});
|
||
},
|
||
showCommentBtn(){
|
||
const s=window.getSelection();if(!s||s.isCollapsed)return;
|
||
const ct=document.getElementById('_blocksCt');if(!ct||!ct.contains(s.anchorNode))return;
|
||
const bel=s.anchorNode.parentElement? s.anchorNode.parentElement.closest('[data-bid]'):null;
|
||
if(!bel)return;
|
||
const bid=bel.dataset.bid;
|
||
const range=s.getRangeAt(0).cloneRange();range.selectNodeContents(bel);range.setEnd(s.getRangeAt(0).startContainer,s.getRangeAt(0).startOffset);
|
||
const start=range.toString().length;
|
||
const end=start+s.toString().length;
|
||
if(end<=start)return;
|
||
this._commentSel={range:s.getRangeAt(0).cloneRange(),bid:bid,start:start,end:end};
|
||
const r=s.getRangeAt(0).getBoundingClientRect();
|
||
const btn=document.getElementById('_commentSelBtn');if(!btn)return;
|
||
btn.style.display='block';
|
||
const bw=btn.offsetWidth||110,bh=btn.offsetHeight||34;
|
||
let x=r.left+r.width/2-bw/2,y=r.top-bh-8;
|
||
x=Math.min(Math.max(8,x),window.innerWidth-bw-8);if(y<8)y=r.bottom+8;
|
||
btn.style.left=x+'px';btn.style.top=y+'px';
|
||
},
|
||
hideCommentBtn(){const btn=document.getElementById('_commentSelBtn');if(btn)btn.style.display='none';},
|
||
// ── @mention autocomplete ──
|
||
mentionOpen:false,mentionQuery:'',mentionResults:[],mentionEl:null,
|
||
openMention(el){
|
||
const s=window.getSelection();if(!s.rangeCount)return;
|
||
const text=el.textContent||'';const pos=s.getRangeAt(0).startOffset;
|
||
const before=text.substring(0,pos);
|
||
const m=before.match(/@([\w\-\.]*)$/);
|
||
if(!m)return;
|
||
this.mentionQuery=m[1];this.mentionEl=el;
|
||
const r=s.getRangeAt(0).getBoundingClientRect();
|
||
const menu=document.getElementById('_mentionMenu');
|
||
menu.style.display='block';
|
||
menu.style.top=Math.max(4,(r.bottom+4>window.innerHeight-280)?r.top-284:r.bottom+4)+'px';
|
||
menu.style.left=Math.min(r.left,window.innerWidth-260)+'px';
|
||
this.mentionOpen=true;this.loadMentionUsers();
|
||
},
|
||
async loadMentionUsers(){
|
||
const q=this.mentionQuery||'';
|
||
try{const r=await fetch('/api/notifications/users/search?q='+encodeURIComponent(q),{credentials:'same-origin'});const d=await r.json();this.mentionResults=d.users||[];this.renderMentionMenu();}catch(e){this.mentionResults=[];}
|
||
},
|
||
renderMentionMenu(){
|
||
const menu=document.getElementById('_mentionMenu');if(!menu)return;
|
||
let h='';const self=this;
|
||
(this.mentionResults||[]).forEach(function(u,idx){
|
||
h+='<div class="mention-item" data-login="'+u.login+'" style="display:flex;align-items:center;gap:8px;padding:7px 10px;font-size:13px;color:var(--text);cursor:pointer;border-radius:6px;">'
|
||
+'<div style="width:24px;height:24px;border-radius:50%;background:'+(u.avatar_color||'#3A3A3A')+';color:#fff;display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:700;">'
|
||
+((u.avatar_url)?'<img src="'+u.avatar_url+'" style="width:24px;height:24px;border-radius:50%;object-fit:cover;">':((u.full_name||u.login||'?').charAt(0).toUpperCase()))
|
||
+'</div><div><div style="font-weight:500">'+u.login+'</div><div style="font-size:11px;color:var(--text-dim,#999)">'+(u.full_name||'')+'</div></div></div>';
|
||
});
|
||
if(!h)h='<div style="padding:10px;color:var(--text-dim,#999);font-size:13px;">No users found</div>';
|
||
menu.innerHTML=h;
|
||
menu.querySelectorAll('.mention-item').forEach(function(it){
|
||
it.addEventListener('click',function(){self.insertMention(it.getAttribute('data-login'));});
|
||
});
|
||
},
|
||
insertMention(login){
|
||
const el=this.mentionEl;if(!el)return;
|
||
const s=window.getSelection();if(s.rangeCount){s.deleteFromDocument();}
|
||
const text=el.textContent||'';const pos=s.rangeCount? (()=>{const r=s.getRangeAt(0);return r.startOffset;} )():text.length;
|
||
const before=text.substring(0,pos);
|
||
const at=before.lastIndexOf('@');
|
||
const full=before.substring(0,at)+'@'+login+' ';
|
||
const after=text.substring(pos);
|
||
el.textContent=full+after;
|
||
const ns=window.getSelection();const nr=document.createRange();nr.selectNodeContents(el);nr.collapse(false);ns.removeAllRanges();ns.addRange(nr);
|
||
this.closeMention();this.dirty=true;this.autoSave();
|
||
this.notifyNewMentions(full);
|
||
},
|
||
closeMention(){const menu=document.getElementById('_mentionMenu');if(menu)menu.style.display='none';this.mentionOpen=false;this.mentionEl=null;},
|
||
notifyNewMentions(text){
|
||
const self=this;
|
||
const handles=[];const re=/@([\w\-\.]+)/g;let mm;
|
||
while((mm=re.exec(text)))handles.push(mm[1].toLowerCase());
|
||
const fresh=handles.filter(function(h){return !self._notifiedMentions[h];});
|
||
if(!fresh.length)return;
|
||
fetch('/api/pages/'+this.pid+'/mentions',{method:'POST',credentials:'same-origin',
|
||
headers:{'Content-Type':'application/json','X-CSRF-Token':this.csrfTok()},
|
||
body:JSON.stringify({text:'@'+fresh.join(' @')})}).catch(function(){});
|
||
},
|
||
pastePlain(e){e.preventDefault();document.execCommand('insertText',false,(e.clipboardData||window.clipboardData).getData('text/plain'));this.dirty=true;this.autoSave();},
|
||
autoSave(){if(this.fileData)return;clearTimeout(this.st);this.st=setTimeout(()=>this.save(),1500);},
|
||
save(cb){
|
||
if(this.fileData)return;
|
||
if(this.saving){ if(cb) cb(); return; }
|
||
this.sync();
|
||
const t=document.getElementById('_titleEl');if(t)this.pageTitle=t.textContent?.trim()||'';
|
||
this.saving=true;
|
||
const csrf=document.cookie.match(/csrf_token=([^;]+)/);
|
||
fetch(`/board/api/pages/${this.pid}/blocks`,{method:'POST',headers:{'Content-Type':'application/json','X-CSRF-Token':csrf?csrf[1]:''},body:JSON.stringify({title:this.pageTitle,blocks:this.blocks.map(b=>{const c={id:b.id,type:b.type,content:b.content};['checked','expanded','language','icon','src','alt','style','embed_type','collection_id','dbs','file_name','file_size','file_mime','children','rows','align','has_header','colsW','meeting'].forEach(k=>{if(b[k]!=null)c[k]=b[k];});return c;})})})
|
||
.then(r=>r.json()).then(()=>{this.saving=false;this.dirty=false;this.lastSaved=new Date().toLocaleTimeString();
|
||
document.querySelectorAll(`.sidebar-item[data-page-id="page/${this.pid}"]`).forEach(item=>{const n=item.querySelector('.page-name');if(n)n.textContent=this.pageTitle||'New page';});
|
||
if(cb)cb();
|
||
}).catch(()=>{this.saving=false;});
|
||
},
|
||
md2b(md){const bl=[];const lines=String(md).split('\n');let i=0;for(;i<lines.length;i++){const raw=lines[i];const t=raw.trim();if(!t)continue;
|
||
if(t.startsWith('```')){const lang=t.substring(3).trim()||'Plain Text';let code='';i++;for(;i<lines.length;i++){if(lines[i].trim()==='```')break;code+=(code?'\n':'')+lines[i];}bl.push(this.mkB('code',code,{language:lang}));continue;}
|
||
if(_tblIsData(t)&&i+1<lines.length&&_tblIsSep(lines[i+1])){const header=_tblSplitLine(t);const rows=[header];let j=i+2;while(j<lines.length&&_tblIsData(lines[j])){rows.push(_tblSplitLine(lines[j]));j++;}let w=0;rows.forEach(function(r){if(r.length>w)w=r.length;});if(w<1)w=1;const rows2=rows.map(function(r){const a=r.slice();while(a.length<w)a.push('');return a;});bl.push(this.mkB('table','',{rows:rows2}));i=j-1;continue;}
|
||
if(t.startsWith('# ')&&!t.startsWith('## '))bl.push(this.mkB('heading_1',t.substring(2)));
|
||
else if(t.startsWith('## ')&&!t.startsWith('### '))bl.push(this.mkB('heading_2',t.substring(3)));
|
||
else if(t.startsWith('### ')&&!t.startsWith('#### '))bl.push(this.mkB('heading_3',t.substring(4)));
|
||
else if(t.startsWith('#### '))bl.push(this.mkB('heading_4',t.substring(5)));
|
||
else if(t==='---'||t==='***')bl.push(this.mkB('divider',''));
|
||
else if(/^[-*+] /.test(t))bl.push(this.mkB('bulleted_list',t.substring(2)));
|
||
else if(/^\d+\. /.test(t))bl.push(this.mkB('numbered_list',t.replace(/^\d+\. /,'')));
|
||
else if(t.startsWith('- [ ] ')||t.startsWith('* [ ] '))bl.push(this.mkB('to_do',t.substring(6)));
|
||
else if(t.startsWith('- [x] ')||t.startsWith('* [x] '))bl.push(this.mkB('to_do',t.substring(6),{checked:true}));
|
||
else if(t.startsWith('> '))bl.push(this.mkB('quote',t.substring(2)));
|
||
else bl.push(this.mkB('paragraph',t));
|
||
}return bl;},
|
||
};
|
||
}
|
||
document.addEventListener('mouseup',()=>{setTimeout(()=>{const s=window.getSelection();if(!s||s.isCollapsed)return;const ed=document.querySelector('.page-editor-wrapper');if(!ed?.__x)return;const d=ed.__x.$data;const ct=document.getElementById('_blocksCt');if(!ct?.contains(s.anchorNode))return;const bel=s.anchorNode.parentElement?.closest('[data-bid]');if(bel){const idx=d.getIdx(bel.dataset.bid);if(idx>=0)d.showFmt(idx);d.showCommentBtn();}else{d.hideCommentBtn();}},80);});
|
||
document.addEventListener('mousedown',()=>{setTimeout(()=>{const ed=document.querySelector('.page-editor-wrapper');if(ed?.__x)ed.__x.$data.hideCommentBtn();},10);});
|
||
document.addEventListener('keyup',(e)=>{
|
||
const ed=document.querySelector('.page-editor-wrapper');if(!ed?.__x)return;
|
||
const d=ed.__x.$data;if(!d.mentionOpen){if(e.key!=='@')return;}
|
||
const el=document.activeElement;if(!el||!el.hasAttribute('data-bid'))return;
|
||
const ct=document.getElementById('_blocksCt');if(!ct||!ct.contains(el))return;
|
||
if(d.mentionOpen){
|
||
if(e.key==='Escape'){d.closeMention();return;}
|
||
if(e.key==='ArrowDown'||e.key==='ArrowUp'){e.preventDefault();const items=document.querySelectorAll('#_mentionMenu .mention-item');if(!items.length)return;let idx=[].indexOf.call(items,document.querySelector('#_mentionMenu .mention-item.hover'));idx+= (e.key==='ArrowDown'?1:-1);if(idx<0)idx=items.length-1;if(idx>=items.length)idx=0;items.forEach(function(x){x.classList.remove('hover');});items[idx].classList.add('hover');return;}
|
||
if(e.key==='Enter'){e.preventDefault();const hover=document.querySelector('#_mentionMenu .mention-item.hover')||document.querySelector('#_mentionMenu .mention-item');if(hover)hover.click();return;}
|
||
}
|
||
d.openMention(el);
|
||
},true);
|
||
document.addEventListener('click',(e)=>{
|
||
const ed=document.querySelector('.page-editor-wrapper');if(!ed?.__x)return;
|
||
const d=ed.__x.$data;if(!d.mentionOpen)return;
|
||
const menu=document.getElementById('_mentionMenu');
|
||
if(menu&&!menu.contains(e.target))d.closeMention();
|
||
});
|
||
var fdStyle=document.createElement('style');
|
||
fdStyle.textContent='#_mentionMenu .mention-item:hover,#_mentionMenu .mention-item.hover{background:rgba(35,131,226,.15);}';
|
||
document.head.appendChild(fdStyle);
|
||
function blocksToMarkdown(b) {
|
||
var c = b.content || '';
|
||
switch(b.type) {
|
||
case 'heading_1': return '# ' + c;
|
||
case 'heading_2': return '## ' + c;
|
||
case 'heading_3': return '### ' + c;
|
||
case 'heading_4': return '#### ' + c;
|
||
case 'bulleted_list': return '- ' + c;
|
||
case 'numbered_list': return '1. ' + c;
|
||
case 'to_do': return (b.checked ? '- [x] ' : '- [ ] ') + c;
|
||
case 'quote': return '> ' + c;
|
||
case 'divider': return '---';
|
||
case 'code': return '```' + (b.language || '') + '\n' + c + '\n```';
|
||
case 'toggle': return (b.children && b.children.length ? '## ' : '## ') + c + (b.children && b.children.length ? '\n\n' + b.children.map(blocksToMarkdown).join('\n\n') : '');
|
||
case 'math': return '$$\n' + c + '\n$$';
|
||
case 'table_of_contents': return '[TOC]';
|
||
case 'columns': return (b.children || []).map(blocksToMarkdown).join('\n\n');
|
||
case 'table': { var rows=b.rows||[]; if(!rows.length)return ''; var w=0; rows.forEach(function(r){if(r&&r.length>w)w=r.length;}); var rl=function(r){var cells=[];for(var k=0;k<w;k++)cells.push((r&&k<r.length)?String(r[k]==null?'':r[k]):'');return '| '+cells.join(' | ')+' |';}; var out=rl(rows[0]); var seps=[];for(var k=0;k<w;k++)seps.push('---');out+='\n| '+seps.join(' | ')+' |';for(var ri=1;ri<rows.length;ri++)out+='\n'+rl(rows[ri]);return out; }
|
||
default: return c;
|
||
}
|
||
}
|
||
</script>
|