feat(v5.9.0): AI Writing Assist - slash /ai, autocompletion, AI properties
- Service app/services/ai_writing.py: 6 actions sans outils (write, summarize, translate, continue, autocomplete, properties) + replis deterministes offline - Endpoints POST /api/agent/writing et /api/agent/writing/properties - Editeur: groupe slash AI (Write/Summarize/Translate/Continue) via E.aiSlash - Autocompletion inline AIAC (suggestion ~900ms, Tab accepte, Escape rejette) - Database: bouton AI fill (suggestions de proprietes Status/Priority/Resume) - Tests tests/test_ai_writing.py (+29) ; version 5.9.0 (VERSION, main.py) - CHANGELOG + ROADMAP v5.9.0 completes
This commit is contained in:
@@ -84,7 +84,7 @@
|
||||
h += '</div><div class="db-col-resize-handle" data-prop-id="'+prop.id+'"></div></th>';
|
||||
}
|
||||
h += '<th class="db-th db-th-add-prop"><button class="db-add-prop-btn db-show-prop-modal-btn">+ Add property</button></th>';
|
||||
h += '<th class="db-th" style="width:40px;min-width:40px"></th>';
|
||||
h += '<th class="db-th" style="width:72px;min-width:72px"></th>';
|
||||
h += '</tr></thead>';
|
||||
|
||||
// Body
|
||||
@@ -104,7 +104,7 @@
|
||||
}
|
||||
h += '</div></td>';
|
||||
}
|
||||
h += '<td class="db-td db-td-open db-hover-only" style="visibility:hidden"><button class="db-open-btn db-open-page-btn" data-page-id="'+cp.id+'" title="Open">↗</button></td>';
|
||||
h += '<td class="db-td db-td-open db-hover-only" style="visibility:hidden;white-space:nowrap"><button class="db-ai-btn db-ai-fill-btn" data-page-id="'+cp.id+'" title="Remplir les propriétés avec l\'IA" style="background:none;border:none;cursor:pointer;font-size:14px;padding:2px 4px;color:var(--text-dim)">✨</button><button class="db-open-btn db-open-page-btn" data-page-id="'+cp.id+'" title="Open">↗</button></td>';
|
||||
h += '</tr>';
|
||||
}
|
||||
// Empty rows + New Page at bottom
|
||||
@@ -141,6 +141,13 @@
|
||||
openPageInSidePeek(parseInt(this.dataset.pageId));
|
||||
};
|
||||
});
|
||||
// AI fill properties
|
||||
containerEl.querySelectorAll('.db-ai-fill-btn').forEach(btn => {
|
||||
btn.onclick = function(e) {
|
||||
e.stopPropagation();
|
||||
aiFillRow(parseInt(this.dataset.pageId));
|
||||
};
|
||||
});
|
||||
// Hover
|
||||
containerEl.querySelectorAll('.db-row').forEach(row => {
|
||||
row.onmouseenter = function() { this.querySelectorAll('.db-hover-only').forEach(el => el.style.visibility = 'visible'); };
|
||||
@@ -249,6 +256,53 @@
|
||||
cellEl.onclick = function(e) { if(!state.editingCell && !e.target.closest('.db-open-btn')) startEditCell(pageId,propId,cellEl); };
|
||||
}
|
||||
|
||||
// ── v5.9.0: AI property suggestions ──
|
||||
async function aiFillRow(pageId) {
|
||||
const cp = state.pages.find(p => p.id === pageId);
|
||||
if (!cp) return;
|
||||
const props = state.properties.filter(p => p.prop_type !== 'title');
|
||||
if (!props.length) {
|
||||
if (typeof window.showToast === 'function') window.showToast('Aucune propriété à remplir');
|
||||
return;
|
||||
}
|
||||
const payload = {
|
||||
title: cp.title || '',
|
||||
content: cp.content || '',
|
||||
properties: props.map(p => ({ name: p.name, type: p.prop_type }))
|
||||
};
|
||||
if (typeof window.showToast === 'function') window.showToast('✨ FlowDeck AI : analyse en cours…');
|
||||
try {
|
||||
const r = await fetch('/api/agent/writing/properties', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': getCsrf() },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
const d = await r.json();
|
||||
if (!d || !d.ok) {
|
||||
if (typeof window.showToast === 'function') window.showToast((d && d.error) || 'Échec de la génération');
|
||||
return;
|
||||
}
|
||||
let pv;
|
||||
try { pv = typeof cp.property_values_json === 'string' ? JSON.parse(cp.property_values_json) : (cp.property_values_json || {}); }
|
||||
catch (e) { pv = {}; }
|
||||
let filled = 0;
|
||||
for (const p of props) {
|
||||
const v = d.suggestions ? d.suggestions[p.name] : undefined;
|
||||
if (v !== undefined && v !== null && v !== '') { pv[String(p.id)] = v; filled++; }
|
||||
}
|
||||
cp.property_values_json = pv;
|
||||
await fetch('/db/pages/' + pageId + '/api', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': getCsrf() },
|
||||
body: JSON.stringify({ title: cp.title, properties: pv })
|
||||
});
|
||||
render();
|
||||
if (typeof window.showToast === 'function') window.showToast(filled ? ('✨ ' + filled + ' propriété(s) remplie(s)') : 'Aucune suggestion disponible');
|
||||
} catch (e) {
|
||||
if (typeof window.showToast === 'function') window.showToast('Erreur IA');
|
||||
}
|
||||
}
|
||||
|
||||
async function addNewPage() {
|
||||
try {
|
||||
const r = await fetch('/api/collections/'+collectionId+'/pages',{method:'POST',headers:{'Content-Type':'application/json','X-CSRF-Token':getCsrf()},body:JSON.stringify({title:''})});
|
||||
|
||||
@@ -10,7 +10,11 @@
|
||||
{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:'⊞'},{id:'button',name:'Button / Automation',icon:'⚡'},
|
||||
]},{name:'MEDIA',items:[{id:'image',name:'Image',icon:'🖼️'}]},
|
||||
]},{name:'AI',items:[
|
||||
{id:'ai_write',name:'Write with AI',icon:'✨'},{id:'ai_summarize',name:'Summarize',icon:'📝'},
|
||||
{id:'ai_translate',name:'Translate',icon:'🌐'},{id:'ai_continue',name:'Continue writing',icon:'➡️'},
|
||||
]},
|
||||
{name:'MEDIA',items:[{id:'image',name:'Image',icon:'🖼️'}]},
|
||||
{name:'DATA',items:[{id:'database',name:'Database / Inline database',icon:'📊'}]},
|
||||
{name:'ACTIONS',items:[{id:'duplicate',name:'Duplicate',icon:'▣'},{id:'turn_into',name:'Turn into…',icon:'↩'},{id:'copy_link',name:'Copy link to block',icon:'🔗'},{id:'delete',name:'Delete block',icon:'✕'}]}];
|
||||
|
||||
@@ -195,8 +199,8 @@
|
||||
}
|
||||
|
||||
// ── 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',duplicate:'Ctrl+D',turn_into:'⋯',copy_link:'#',delete:'⌫'};
|
||||
const _GROUP_LABEL={BASIC:'Basic blocks',MEDIA:'Media',DATA:'Data',ACTIONS:'Actions'};
|
||||
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',ai_write:'/ai',ai_summarize:'/ai',ai_translate:'/ai',ai_continue:'/ai',duplicate:'Ctrl+D',turn_into:'⋯',copy_link:'#',delete:'⌫'};
|
||||
const _GROUP_LABEL={BASIC:'Basic blocks',AI:'AI writing',MEDIA:'Media',DATA:'Data',ACTIONS:'Actions'};
|
||||
// ── 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>';};
|
||||
@@ -356,6 +360,49 @@
|
||||
};
|
||||
window.AIC=AIC;
|
||||
|
||||
// ═══════════ AI Autocomplete (v5.9.0) ═══════════
|
||||
// After a short idle while typing in a text block, ask the AI for a short
|
||||
// continuation and surface it as a "Tab to accept" pill anchored to the
|
||||
// block. Tab inserts the suggestion; Escape dismisses it.
|
||||
const AIAC={_timer:null,_sug:'',_bid:null,_busy:false,_enabled:true,_req:0,
|
||||
ROOT:null,
|
||||
_allowed:function(b){ if(!b)return false; return ['paragraph','heading_1','heading_2','heading_3','heading_4','bulleted_list','numbered_list','quote','callout','to_do'].indexOf(b.type)>=0; },
|
||||
mk(){ if(this.ROOT)return this.ROOT; const d=document.createElement('div'); d.id='_aiAC'; d.style.cssText='position:fixed;z-index:1150;display:none;max-width:min(520px,90vw)'; document.body.appendChild(d); this.ROOT=d;
|
||||
document.addEventListener('mousedown',function(ev){ if(AIAC.ROOT&&!AIAC.ROOT.contains(ev.target))AIAC.clear(); });
|
||||
return d; },
|
||||
schedule(bid){ if(!this._enabled)return; const E=window.E; if(!E)return; const idx=E.getIdx(bid); const b=idx>=0?E.blocks[idx]:null; if(!this._allowed(b))return this.clear();
|
||||
clearTimeout(this._timer); const self=this; this._timer=setTimeout(function(){ self.fetch(bid); },900); },
|
||||
fetch(bid){ const E=window.E; if(!E)return; const idx=E.getIdx(bid); const b=idx>=0?E.blocks[idx]:null; if(!this._allowed(b))return this.clear();
|
||||
const el=E.getEl(bid); const prefix=((el&&el.textContent)||b.content||'').trim(); if(prefix.length<8)return this.clear();
|
||||
const ctx=(typeof window.fdGetPageContext==='function')?String(window.fdGetPageContext()||''):'';
|
||||
const req=++this._req; this._busy=true; const self=this; const csrf=(E.csrfTok?E.csrfTok():'');
|
||||
fetch('/api/agent/writing',{method:'POST',headers:{'Content-Type':'application/json','X-CSRF-Token':csrf},body:JSON.stringify({action:'autocomplete',prefix:prefix,context:ctx})})
|
||||
.then(function(r){return r.json();})
|
||||
.then(function(d){ self._busy=false; if(req!==self._req)return; if(d&&d.ok&&d.text){ self.show(bid,d.text); } else { self.clear(); } })
|
||||
.catch(function(){ self._busy=false; if(req===self._req)self.clear(); });
|
||||
},
|
||||
show(bid,text){ text=(text||'').trim(); if(!text){return this.clear();} const E=window.E; const el=E&&E.getEl(bid); if(!el)return;
|
||||
this._sug=text; this._bid=bid; this.mk(); const r=el.getBoundingClientRect();
|
||||
this.ROOT.innerHTML='<div style="display:flex;align-items:center;gap:8px;background:var(--bg-modal);border:1px solid var(--border-light);border-radius:8px;padding:6px 10px;box-shadow:0 6px 24px rgba(0,0,0,.4);font-size:13px;color:var(--text-dim)">'
|
||||
+'<span style="opacity:.7">✨</span><span style="color:var(--text);font-style:italic">'+esc(text)+'</span>'
|
||||
+'<span style="margin-left:6px;font-family:var(--font-mono);font-size:11px;background:var(--bg-secondary);border-radius:4px;padding:1px 6px">Tab</span>'
|
||||
+'<button class="aiac-x" title="Dismiss" style="background:none;border:none;color:var(--text-dim);cursor:pointer;font-size:14px">✕</button></div>';
|
||||
const btn=this.ROOT.querySelector('.aiac-x'); if(btn)btn.onclick=function(){ AIAC.clear(); };
|
||||
let left=Math.min(Math.max(8,r.left+24),window.innerWidth-360); let top=r.bottom+4;
|
||||
if(top+40>window.innerHeight)top=r.top-36;
|
||||
this.ROOT.style.left=left+'px'; this.ROOT.style.top=Math.max(8,top)+'px'; this.ROOT.style.display='block';
|
||||
},
|
||||
accept(){ if(!this._sug)return false; const E=window.E; const idx=E&&E.getIdx(this._bid); if(idx<0){ this.clear(); return false; }
|
||||
const b=E.blocks[idx]; const el=E.getEl(this._bid); E.sync(); E.pushHistory();
|
||||
const cur=((el&&el.textContent)||b.content||''); const next=(cur+this._sug).replace(/\s+$/,' ');
|
||||
b.content=next; if(el){ el.textContent=next; } this.clear(); E.dirty=true; E.autoSave(); if(window.__fdRT&&window.__fdRT.syncNow)window.__fdRT.syncNow();
|
||||
if(el){ el.focus(); ce(el); } return true;
|
||||
},
|
||||
clear(){ this._sug=''; this._bid=null; this._req++; if(this.ROOT){ this.ROOT.style.display='none'; this.ROOT.innerHTML=''; } },
|
||||
get active(){ return !!this._sug; }
|
||||
};
|
||||
window.AIAC=AIAC;
|
||||
|
||||
function renderBlock(b,idx){
|
||||
if(b.type==='meeting')return renderMeetingBlock(b,idx);
|
||||
if(b.type==='table')return renderTableBlock(b,idx);
|
||||
@@ -929,7 +976,7 @@
|
||||
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-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());if(window.AIAC)window.AIAC.schedule(el.getAttribute('data-bid'));})});
|
||||
ct.querySelectorAll('[data-placeholder]').forEach(el=>{el.innerHTML=el.innerHTML.replace(/<br[^>]*>/gi,'').trim()!==''?el.innerHTML:'';el.classList.toggle('empty',!el.textContent.trim());});
|
||||
_selApply();
|
||||
},
|
||||
@@ -1374,6 +1421,11 @@ applyAIBlocks(text){
|
||||
if(e.key==='Backspace'&&!SM._q&&(el.textContent||'').trim()==='/'){e.preventDefault();SM.close();return;}
|
||||
return;
|
||||
}
|
||||
if(window.AIAC&&window.AIAC.active){
|
||||
if(e.key==='Tab'){e.preventDefault();e.stopPropagation();window.AIAC.accept();return;}
|
||||
if(e.key==='Escape'){e.preventDefault();window.AIAC.clear();return;}
|
||||
if(e.key==='Enter'||e.key==='Backspace'){window.AIAC.clear();}
|
||||
}
|
||||
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){
|
||||
@@ -1420,6 +1472,7 @@ applyAIBlocks(text){
|
||||
applySlash(id){
|
||||
const idx=SM._i;if(idx<0||idx>=this.blocks.length)return;
|
||||
if(id==='database'){ this.openDbTemplatePicker(idx); return; }
|
||||
if(id.indexOf('ai_')===0){ this.aiSlash(id,idx); return; }
|
||||
if(id==='duplicate'){this.duplicateBlock(idx);return;}
|
||||
if(id==='delete'){this.removeBlock(idx);return;}
|
||||
if(id==='copy_link'){const blk=this.blocks[idx];if(blk)this.copyBlockLink(blk.id);return;}
|
||||
@@ -1433,6 +1486,40 @@ applyAIBlocks(text){
|
||||
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);}
|
||||
},
|
||||
// ── v5.9.0: AI writing slash commands ──
|
||||
aiSlash(id,idx){
|
||||
const action=id.slice(3); const block=this.blocks[idx];
|
||||
if(block){ this.sync(); this.pushHistory(); block.content=''; }
|
||||
if(action==='write'){ this.render(); this.dirty=true; this.autoSave(); if(window.AIC)window.AIC.open(idx); return; }
|
||||
if(action==='translate'){ this.render(); this.dirty=true; this.autoSave();
|
||||
const lang=window.prompt('Traduire en…','English'); if(!lang){return;}
|
||||
this._aiWritingRun('translate',idx,{target_language:lang},'replace'); return; }
|
||||
this.render(); this.dirty=true; this.autoSave();
|
||||
this._aiWritingRun(action,idx,{},'insert');
|
||||
},
|
||||
_aiWritingRun(action,idx,extra,mode){
|
||||
const self=this; this.sync();
|
||||
const ctx=(typeof window.fdGetPageContext==='function')?String(window.fdGetPageContext()||''):'';
|
||||
const payload={action:action,context:ctx};
|
||||
if(action==='continue'){ let parts=[]; for(let i=0;i<idx;i++){ const b=this.blocks[i]; if(b&&b.content)parts.push(b.content); } payload.prefix=parts.join('\n').slice(-4000); }
|
||||
if(extra&&extra.target_language)payload.target_language=extra.target_language;
|
||||
if(extra&&extra.prompt)payload.prompt=extra.prompt;
|
||||
if(extra&&extra.title)payload.title=extra.title;
|
||||
this.showToast('FlowDeck AI : génération…');
|
||||
fetch('/api/agent/writing',{method:'POST',headers:{'Content-Type':'application/json','X-CSRF-Token':this.csrfTok()},body:JSON.stringify(payload)})
|
||||
.then(function(r){return r.json();})
|
||||
.then(function(d){ if(d&&d.ok&&d.text){ self.applyAIBlocksAt(d.text,idx,mode); } else { self.showToast((d&&d.error)||'Échec de la génération','error'); } })
|
||||
.catch(function(err){ self.showToast((err&&err.message)||'Erreur réseau','error'); });
|
||||
},
|
||||
applyAIBlocksAt(text,idx,mode){
|
||||
const nb=this.md2b(text||''); if(!nb.length)return;
|
||||
this.sync(); this.pushHistory();
|
||||
if(mode==='replace'){ this.blocks=nb; }
|
||||
else{ const block=this.blocks[idx]; const empty=block&&block.type==='paragraph'&&!(block.content||'').trim();
|
||||
if(empty){ this.blocks.splice(idx,1,...nb); } else { this.blocks.splice(idx+1,0,...nb); } }
|
||||
this.dirty=true; this.autoSave(); this.render(); _rtSync();
|
||||
setTimeout(()=>{ const el=this.getEl(this.blocks[Math.min(idx,this.blocks.length-1)]?.id); if(el){el.focus();ce(el);} },60);
|
||||
},
|
||||
_doToggle(bid,v){const idx=this.getIdx(bid);if(idx>=0){this.pushHistory();this.blocks[idx].checked=v;this.dirty=true;this.autoSave();_rtSync();}},
|
||||
_doToggleExpand(bid){const idx=this.getIdx(bid);if(idx>=0){this.pushHistory();this.blocks[idx].expanded=!this.blocks[idx].expanded;this.render();this.dirty=true;this.autoSave();_rtSync();}},
|
||||
_addColumn(parentId){const idx=this.getIdx(parentId);if(idx<0)return;const b=this.blocks[idx];this.pushHistory();b.children.push({id:genId(),type:'paragraph',content:''});this.render();this.dirty=true;this.autoSave();_rtSync();setTimeout(()=>{const ne=this.getEl(b.children[b.children.length-1].id);if(ne)ne.focus();},60);},
|
||||
|
||||
Reference in New Issue
Block a user