@@ -204,6 +204,7 @@
get isOpen ( ) { return SM . _o ; } } ;
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> ` ;
@@ -282,6 +283,235 @@
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 {
@@ -377,8 +607,10 @@
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 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 ( ) ; } ) } ) ;
ct . querySelectorAll ( '[data-placeholder]' ) . forEach ( el => { el . innerHTML = el . innerHTML . replace ( /<br[^>]*>/gi , '' ) . trim ( ) !== '' ? el . innerHTML : '' ; el . classList . toggle ( 'empty' , ! el . textContent . trim ( ) ) ; } ) ;
} ,
@@ -401,7 +633,42 @@
this . aiRun ( '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.' ) ;
} ,
meetingNote ( ) {
this . aiRun ( 'Crée le squelette d\'une « AI meeting note » FlowDeck : une ligne d\'en-tête avec la date et les participants, puis les sections « Résumé », « Décisions » et « Action items » avec des exemples réalistes. Sépare chaque bloc/paragraphe par une ligne vide.' ) ;
// 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 ;
@@ -828,7 +1095,7 @@
const t = document . getElementById ( '_titleEl' ) ; if ( t ) this . pageTitle = t . textContent ? . trim ( ) || 'New page' ;
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' ] . forEach ( k => { if ( b [ k ] != null ) c [ k ] = b [ k ] ; } ) ; return c ; } ) } ) } )
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 ( ) ;