fix: aide (bouton ✕), Insert below qui perdait le format, listes numérotées invisibles (v7.59.2)
- aide : bouton ✕ en haut à droite du panneau (classe partagée settings-close, 44 px mobile) + helpInit.close() (retour en arrière, sinon /workspaces) ; .settings-panel en position:relative pour ancrer hamburger + ✕ au panneau - éditeur « Insert below » (/Write with AI) : insère le markdown source conservé à la génération (AIC._md) au lieu du texte extrait du HTML rendu (_resultText() reste seulement en repli) → titres/listes/gras conservés - listes numérotées : aucun numéro n'était affiché (CSS = retrait seul) ; le rang des blocs numbered_list contigus est calculé au rendu (data-num) et rendu via content: attr(data-num) — /numbered list et l'insertion agent affichent 1, 2, 3 - md2b : cases à cocher - [ ] / - [x] testées avant la branche des puces (aligné sur paste2b) + séparateur 1) accepté (CommonMark) - menu slash : requête tapée dans le bloc (/numbered list) répercutée dans le filtre du menu quand le focus n'a pas rejoint l'entrée - tests : tests/test_v7592_ui_fixes.py (5 tests, dont 1 comportemental node qui exécute le vrai md2b) ; ruff I001 préexistant sur test_agent.py - livraison : VERSION + app/main = 7.59.2, OpenAPI 525 chemins, CHANGELOG
This commit is contained in:
@@ -1,5 +1,34 @@
|
|||||||
# Changelog - FlowDeck
|
# Changelog - FlowDeck
|
||||||
|
|
||||||
|
## v7.59.2 (2026-10-07) — Corrections : aide, « Insert below », listes numérotées
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **Aide** — bouton **✕** en haut à droite du panneau (classe partagée
|
||||||
|
`settings-close` : 32 px desktop / **44 px mobile**) + méthode `close()` dans
|
||||||
|
`help.js` (retour à la page précédente, sinon `/workspaces`) ;
|
||||||
|
`.settings-panel` passé en `position:relative` pour ancrer les boutons au
|
||||||
|
panneau (hamburger + ✕) plutôt qu'à la page.
|
||||||
|
- **Éditeur — « Insert below » (/Write with AI)** : on insère le **markdown
|
||||||
|
source** conservé à la génération (`AIC._md`) au lieu du texte extrait du
|
||||||
|
HTML rendu (`_resultText()` ne reste qu'en repli pour les messages d'erreur) —
|
||||||
|
titres, listes, gras et numéros survivent à l'insertion.
|
||||||
|
- **Listes numérotées** — elles n'avaient **aucun numéro au rendu** : le CSS
|
||||||
|
ne déclarait que le retrait, jamais le marqueur. Le rang des blocs
|
||||||
|
`numbered_list` contigus est désormais calculé au rendu (`data-num`) et
|
||||||
|
affiché via `content: attr(data-num) ". "` → **`/numbered list` et
|
||||||
|
l'insertion depuis l'assistant affichent enfin 1, 2, 3**.
|
||||||
|
- **`md2b`** (markdown → blocs, chemin agent) : les cases à cocher `- [ ]` /
|
||||||
|
`- [x]` sont testées **avant** la branche des puces (une ligne `- [ ]`
|
||||||
|
était avalée par cette branche) — aligné sur l'ordre déjà correct de
|
||||||
|
`paste2b` ; le séparateur `1)` est accepté (CommonMark).
|
||||||
|
- **Menu slash** — une requête tapée **dans le bloc** (`/numbered list`, focus
|
||||||
|
qui n'a pas rejoint l'entrée du menu) est répercutée dans le filtre au lieu
|
||||||
|
d'être ignorée.
|
||||||
|
- **Tests** — `tests/test_v7592_ui_fixes.py` : **5 tests**, dont 1 test
|
||||||
|
comportemental **node** qui exécute le vrai `md2b` extrait de la source.
|
||||||
|
- `ruff` : ordre d'imports de `tests/test_agent.py` (I001 préexistant au pull).
|
||||||
|
|
||||||
## v7.59.1 (2026-10-07) — Fix fournisseur Mistral (clé valide, 403 tier_not_allowed)
|
## v7.59.1 (2026-10-07) — Fix fournisseur Mistral (clé valide, 403 tier_not_allowed)
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
+1
-1
@@ -186,7 +186,7 @@ async def lifespan(_app: FastAPI):
|
|||||||
|
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title="FlowDeck",
|
title="FlowDeck",
|
||||||
version="7.59.1",
|
version="7.59.2",
|
||||||
docs_url="/docs",
|
docs_url="/docs",
|
||||||
redoc_url="/redoc",
|
redoc_url="/redoc",
|
||||||
lifespan=lifespan,
|
lifespan=lifespan,
|
||||||
|
|||||||
@@ -35,6 +35,7 @@
|
|||||||
<button class="settings-menu-btn" @click="navOpen = !navOpen" title="Sections" aria-label="Open sections menu">
|
<button class="settings-menu-btn" @click="navOpen = !navOpen" title="Sections" aria-label="Open sections menu">
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg>
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg>
|
||||||
</button>
|
</button>
|
||||||
|
<button class="settings-close" @click="close()" title="Close" aria-label="Close help">×</button>
|
||||||
|
|
||||||
<!-- Mobile drawer backdrop -->
|
<!-- Mobile drawer backdrop -->
|
||||||
<div class="settings-nav-backdrop" x-show="navOpen" x-cloak @click="navOpen = false"></div>
|
<div class="settings-nav-backdrop" x-show="navOpen" x-cloak @click="navOpen = false"></div>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"openapi": "3.1.0",
|
"openapi": "3.1.0",
|
||||||
"info": {
|
"info": {
|
||||||
"title": "FlowDeck",
|
"title": "FlowDeck",
|
||||||
"version": "7.58.0"
|
"version": "7.59.2"
|
||||||
},
|
},
|
||||||
"paths": {
|
"paths": {
|
||||||
"/auth/register": {
|
"/auth/register": {
|
||||||
@@ -1098,7 +1098,7 @@
|
|||||||
"dashboard"
|
"dashboard"
|
||||||
],
|
],
|
||||||
"summary": "Help Page",
|
"summary": "Help Page",
|
||||||
"description": "Comprehensive help & documentation page.",
|
"description": "Help & documentation page — settings-style panel with side navigation.",
|
||||||
"operationId": "help_page_help_get",
|
"operationId": "help_page_help_get",
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
|
|||||||
+11
-2
@@ -2476,11 +2476,20 @@ button.breadcrumb-current { max-width: 260px; }
|
|||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Numbered list */
|
/* Numbered list — data-num est calculé au rendu (rangée contiguë) */
|
||||||
.block-numbered {
|
.block-numbered {
|
||||||
padding-left: 22px;
|
padding-left: 32px;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
.block-numbered[data-num]::before {
|
||||||
|
content: attr(data-num) ". ";
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
min-width: 24px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
/* To-do */
|
/* To-do */
|
||||||
.block-todo {
|
.block-todo {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/* ═══ Panel skeleton shared by /settings and /help (Notion-style modal) ═══ */
|
/* ═══ Panel skeleton shared by /settings and /help (Notion-style modal) ═══ */
|
||||||
.settings-overlay{position:fixed;inset:0;z-index:500;display:flex;background:rgba(0,0,0,.4);backdrop-filter:blur(2px);}
|
.settings-overlay{position:fixed;inset:0;z-index:500;display:flex;background:rgba(0,0,0,.4);backdrop-filter:blur(2px);}
|
||||||
.settings-panel{display:flex;width:1050px;max-width:98vw;height:85vh;margin:auto;background:var(--bg-primary);border:1px solid var(--border-strong);border-radius:14px;box-shadow:var(--shadow-modal);overflow:hidden;}
|
.settings-panel{position:relative;display:flex;width:1050px;max-width:98vw;height:85vh;margin:auto;background:var(--bg-primary);border:1px solid var(--border-strong);border-radius:14px;box-shadow:var(--shadow-modal);overflow:hidden;}
|
||||||
.settings-nav{width:200px;min-width:200px;background:var(--bg-sidebar);border-right:1px solid var(--border);padding:8px 0;overflow-y:auto;}
|
.settings-nav{width:200px;min-width:200px;background:var(--bg-sidebar);border-right:1px solid var(--border);padding:8px 0;overflow-y:auto;}
|
||||||
.settings-nav-header{padding:12px 16px;font-size:13px;font-weight:600;color:var(--text-dim);text-transform:uppercase;letter-spacing:.5px;}
|
.settings-nav-header{padding:12px 16px;font-size:13px;font-weight:600;color:var(--text-dim);text-transform:uppercase;letter-spacing:.5px;}
|
||||||
.settings-nav-item{display:flex;align-items:center;gap:8px;padding:6px 16px;font-size:13px;color:var(--text-secondary);cursor:pointer;transition:background 100ms;border-radius:0;}
|
.settings-nav-item{display:flex;align-items:center;gap:8px;padding:6px 16px;font-size:13px;color:var(--text-secondary);cursor:pointer;transition:background 100ms;border-radius:0;}
|
||||||
|
|||||||
@@ -7,6 +7,11 @@ function helpInit() {
|
|||||||
section: 'start',
|
section: 'start',
|
||||||
navOpen: false,
|
navOpen: false,
|
||||||
closeNavOnMobile() { this.navOpen = false; },
|
closeNavOnMobile() { this.navOpen = false; },
|
||||||
|
// Bouton ✕ (haut droite, visible aussi en mobile) : ferme l'aide.
|
||||||
|
close() {
|
||||||
|
if (document.referrer && document.referrer !== location.href) { history.back(); }
|
||||||
|
else { location.href = '/workspaces'; }
|
||||||
|
},
|
||||||
go(sec) {
|
go(sec) {
|
||||||
this.section = sec;
|
this.section = sec;
|
||||||
this.navOpen = false;
|
this.navOpen = false;
|
||||||
|
|||||||
@@ -419,7 +419,7 @@ const PD=(()=>{try{const el=document.getElementById('page-data');return el?JSON.
|
|||||||
// ═══════════ Inline AI composer (Notion AI-style) ═══════════
|
// ═══════════ Inline AI composer (Notion AI-style) ═══════════
|
||||||
// Space on an empty paragraph → "Edit with AI" pill → Enter → "Brewing…"
|
// Space on an empty paragraph → "Edit with AI" pill → Enter → "Brewing…"
|
||||||
// → framed result → "Insert below" → applyAIBlocks.
|
// → framed result → "Insert below" → applyAIBlocks.
|
||||||
const AIC={_open:false,_idx:-1,_stage:'',_html:'',
|
const AIC={_open:false,_idx:-1,_stage:'',_html:'',_md:'',
|
||||||
ROOT:null,
|
ROOT:null,
|
||||||
mk(){ if(this.ROOT) return this.ROOT;
|
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';
|
const d=document.createElement('div'); d.id='_aiC'; d.style.cssText='position:fixed;z-index:1200;width:420px;max-width:92vw;display:none';
|
||||||
@@ -432,7 +432,7 @@ const PD=(()=>{try{const el=document.getElementById('page-data');return el?JSON.
|
|||||||
this.paint(); this.ROOT.style.display='block'; this._open=true; this._focusIngest();
|
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); } });
|
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=''; },
|
close(){ if(this.ROOT)this.ROOT.style.display='none'; this._open=false; this._html=''; this._md=''; 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};
|
_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}; },
|
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();} },
|
_focusIngest(){ const i=this.ROOT.querySelector('.aici-input'); if(i){i.focus();} },
|
||||||
@@ -478,12 +478,17 @@ const PD=(()=>{try{const el=document.getElementById('page-data');return el?JSON.
|
|||||||
const E=window.E; const ctx=(typeof window.fdGetPageContext==='function')?String(window.fdGetPageContext()||''):'';
|
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; }
|
if(!(window.fdAgent&&window.fdAgent.generate)){ this._err('Agent IA non configuré'); return; }
|
||||||
window.fdAgent.generate(prompt, ctx).then(function(text){
|
window.fdAgent.generate(prompt, ctx).then(function(text){
|
||||||
self._html=self._mdToHtml(text||''); self._stage='result'; self.paint();
|
// On conserve le MARKDOWN source : c'est lui qui est inséré (md2b),
|
||||||
|
// pas le texte extrait du HTML rendu qui perd titres/listes/gras.
|
||||||
|
self._md=String(text||'');
|
||||||
|
self._html=self._mdToHtml(self._md); self._stage='result'; self.paint();
|
||||||
}).catch(function(err){ self._err((err&&err.message)?err.message:'Erreur'); });
|
}).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(); },
|
_err(msg){ this._md=''; 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();
|
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(); },
|
// Markdown source d'abord ; repli sur le texte visible (erreurs).
|
||||||
|
const result=String(this._md||'').trim()||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(); },
|
_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'); }
|
toast(m){ if(window.showToast)window.showToast(m,'info'); }
|
||||||
};
|
};
|
||||||
@@ -595,11 +600,19 @@ const PD=(()=>{try{const el=document.getElementById('page-data');return el?JSON.
|
|||||||
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 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)'}${(b.style&&b.style.color)?';color:'+b.style.color:''}"`:((b.style&&(b.style.bgColor||b.style.color))?` style="${(b.style.bgColor?'background:'+b.style.bgColor+';':'')+(b.style.color?'color:'+b.style.color+';':'')}"`:'');
|
const style=b.type==='callout'?` style="background:${(b.style&&b.style.bgColor)||'var(--blue-bg)'}${(b.style&&b.style.color)?';color:'+b.style.color:''}"`:((b.style&&(b.style.bgColor||b.style.color))?` style="${(b.style.bgColor?'background:'+b.style.bgColor+';':'')+(b.style.color?'color:'+b.style.color+';':'')}"`:'');
|
||||||
const bidAttr=b.type.startsWith('heading_')?` id="h-${(b.id||'').replace(/[^a-zA-Z0-9]/g,'')}"`:'';
|
const bidAttr=b.type.startsWith('heading_')?` id="h-${(b.id||'').replace(/[^a-zA-Z0-9]/g,'')}"`:'';
|
||||||
|
// Numéro d'ordre : rang des blocs `numbered_list` contigus (1,2,3…) ;
|
||||||
|
// passé en attribut data-num → le CSS le rend hors du contenu éditable.
|
||||||
|
let numAttr='';
|
||||||
|
if(b.type==='numbered_list'&&idx>=0){
|
||||||
|
const bs=(window.E&&window.E.blocks)||[];
|
||||||
|
let s=idx; while(s>0&&bs[s-1]&&bs[s-1].type==='numbered_list')s--;
|
||||||
|
numAttr=` data-num="${idx-s+1}"`;
|
||||||
|
}
|
||||||
const actions=idx>=0?`<div class="block-actions"><button class="block-add-btn" onclick="E.addAfter(${idx})">+</button></div>`:'';
|
const actions=idx>=0?`<div class="block-actions"><button class="block-add-btn" onclick="E.addAfter(${idx})">+</button></div>`:'';
|
||||||
return `<div class="block-wrapper" data-id="${b.id}">
|
return `<div class="block-wrapper" data-id="${b.id}">
|
||||||
<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>
|
<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}
|
${actions}
|
||||||
<${tag}${style}${bidAttr} class="${cls}">${inner}</${tag}>
|
<${tag}${style}${bidAttr}${numAttr} class="${cls}">${inner}</${tag}>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1970,6 +1983,12 @@ applyAIBlocks(text){
|
|||||||
if(e.key==='Escape'){e.preventDefault();SM.close();return;}
|
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==='ArrowDown'||e.key==='ArrowUp'||e.key==='Enter'){e.preventDefault();return;}
|
||||||
if(e.key==='Backspace'&&!SM._q&&(el.textContent||'').trim()==='/'){e.preventDefault();SM.close();return;}
|
if(e.key==='Backspace'&&!SM._q&&(el.textContent||'').trim()==='/'){e.preventDefault();SM.close();return;}
|
||||||
|
// La requête peut être tapée DANS le bloc (« /numbered list ») si le
|
||||||
|
// focus n'a pas rejoint l'entrée du menu → on la répercute au filtre.
|
||||||
|
if(SM._in&&(e.key.length===1||e.key==='Backspace')){
|
||||||
|
const q=(el.textContent||'').replace(/^\s*\//,'');
|
||||||
|
if(SM._in.value!==q){SM._in.value=q;SM.flt();}
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if(window.AIAC&&window.AIAC.active){
|
if(window.AIAC&&window.AIAC.active){
|
||||||
@@ -2486,10 +2505,12 @@ applyAIBlocks(text){
|
|||||||
else if(t.startsWith('### ')&&!t.startsWith('#### '))bl.push(this.mkB('heading_3',t.substring(4)));
|
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.startsWith('#### '))bl.push(this.mkB('heading_4',t.substring(5)));
|
||||||
else if(t==='---'||t==='***')bl.push(this.mkB('divider',''));
|
else if(t==='---'||t==='***')bl.push(this.mkB('divider',''));
|
||||||
else if(/^[-*+] /.test(t))bl.push(this.mkB('bulleted_list',t.substring(2)));
|
// AVANT les puces : « - [ ] … » commence aussi par « - » →
|
||||||
else if(/^\d+\. /.test(t))bl.push(this.mkB('numbered_list',t.replace(/^\d+\. /,'')));
|
// sinon la case à cocher de l'agent devenait une puce.
|
||||||
else if(t.startsWith('- [ ] ')||t.startsWith('* [ ] '))bl.push(this.mkB('to_do',t.substring(6)));
|
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('- [x] ')||t.startsWith('* [x] '))bl.push(this.mkB('to_do',t.substring(6),{checked:true}));
|
||||||
|
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('> '))bl.push(this.mkB('quote',t.substring(2)));
|
else if(t.startsWith('> '))bl.push(this.mkB('quote',t.substring(2)));
|
||||||
else bl.push(this.mkB('paragraph',t));
|
else bl.push(this.mkB('paragraph',t));
|
||||||
}return bl;},
|
}return bl;},
|
||||||
|
|||||||
+1
-1
@@ -93,7 +93,7 @@ def test_mistral_defaults_are_tier_safe(client):
|
|||||||
"""La clé Mistral plan basique → 403 « tier_not_allowed » sur mistral-large/pixtral-large.
|
"""La clé Mistral plan basique → 403 « tier_not_allowed » sur mistral-large/pixtral-large.
|
||||||
Le modèle par défaut et le premier de la liste du test de connexion doivent
|
Le modèle par défaut et le premier de la liste du test de connexion doivent
|
||||||
être des modèles servis par tous les plans."""
|
être des modèles servis par tous les plans."""
|
||||||
from app.services.llm_client import PROVIDERS, PROVIDER_MODELS
|
from app.services.llm_client import PROVIDER_MODELS, PROVIDERS
|
||||||
tier_blocked = {"mistral-large-latest", "pixtral-large-latest"}
|
tier_blocked = {"mistral-large-latest", "pixtral-large-latest"}
|
||||||
assert PROVIDERS["mistral"][1] not in tier_blocked
|
assert PROVIDERS["mistral"][1] not in tier_blocked
|
||||||
assert PROVIDER_MODELS["mistral"][0] not in tier_blocked
|
assert PROVIDER_MODELS["mistral"][0] not in tier_blocked
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
"""7.59.2 — corrections : bouton ✕ de l'aide, « Insert below » qui perd le
|
||||||
|
format, listes numérotées invisibles (agent + /numbered list)."""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parent.parent
|
||||||
|
HELP_HTML = ROOT / "app" / "templates" / "help.html"
|
||||||
|
HELP_JS = ROOT / "static" / "js" / "help.js"
|
||||||
|
EDITOR_JS = ROOT / "static" / "js" / "page_editor_scripts.js"
|
||||||
|
APP_CSS = ROOT / "static" / "css" / "app.css"
|
||||||
|
SETTINGS_CSS = ROOT / "static" / "css" / "settings.css"
|
||||||
|
|
||||||
|
|
||||||
|
# ── 1. Aide : fermeture en haut à droite (mobile compris) ───────────────────
|
||||||
|
|
||||||
|
def test_help_page_has_close_button():
|
||||||
|
html = HELP_HTML.read_text(encoding="utf-8")
|
||||||
|
js = HELP_JS.read_text(encoding="utf-8")
|
||||||
|
css = SETTINGS_CSS.read_text(encoding="utf-8")
|
||||||
|
assert 'class="settings-close"' in html # bouton ✕ présent
|
||||||
|
assert '@click="close()"' in html
|
||||||
|
assert "close()" in js and "history.back()" in js # méthode réelle
|
||||||
|
assert ".settings-close{" in css # style partagé (32/44px)
|
||||||
|
assert "position:relative" in css.split(".settings-panel{")[1][:40]
|
||||||
|
|
||||||
|
|
||||||
|
# ── 2. Insert below : on insère le MARKDOWN, pas le texte du HTML rendu ─────
|
||||||
|
|
||||||
|
def test_ai_insert_below_keeps_source_markdown():
|
||||||
|
src = EDITOR_JS.read_text(encoding="utf-8")
|
||||||
|
assert "self._md=String(text||'')" in src # source conservée
|
||||||
|
assert "String(this._md||'').trim()||this._resultText()" in src # md d'abord
|
||||||
|
assert src.count("this._md='';") >= 2 # close() + _err()
|
||||||
|
# l'ancien chemin (texte extrait du DOM rendu) n'est plus que le repli
|
||||||
|
assert "_resultText(){ const el=this.ROOT&&this.ROOT.querySelector('.aici-result')" in src
|
||||||
|
|
||||||
|
|
||||||
|
# ── 3. Listes numérotées : numéros réellement affichés ──────────────────────
|
||||||
|
|
||||||
|
def test_numbered_list_renders_its_number():
|
||||||
|
src = EDITOR_JS.read_text(encoding="utf-8")
|
||||||
|
css = APP_CSS.read_text(encoding="utf-8")
|
||||||
|
assert "let numAttr=''" in src and "data-num=" in src # rangée contiguë → rang
|
||||||
|
assert ".block-numbered[data-num]::before" in css
|
||||||
|
assert 'content: attr(data-num) ". "' in css
|
||||||
|
# md2b accepte « 1. » et « 1) » (CommonMark) pour l'insertion agent
|
||||||
|
assert r"/^\d+[.)] /" in src
|
||||||
|
|
||||||
|
|
||||||
|
def test_slash_query_typed_in_block_is_synced_to_filter():
|
||||||
|
src = EDITOR_JS.read_text(encoding="utf-8")
|
||||||
|
# « /numbered list » tapé dans le bloc → répercuté dans l'entrée du menu
|
||||||
|
assert "SM._in.value=q" in src
|
||||||
|
assert "SM.flt();" in src
|
||||||
|
|
||||||
|
|
||||||
|
# ── md2b : test comportemental sur la source réelle (node) ──────────────────
|
||||||
|
|
||||||
|
def _md2b_source() -> str:
|
||||||
|
src = EDITOR_JS.read_text(encoding="utf-8")
|
||||||
|
i = src.index("md2b(md){")
|
||||||
|
j = src.index("return bl;},", i) + len("return bl;},")
|
||||||
|
snippet = src[i:j].rstrip().rstrip(",") # « md2b(md){…} »
|
||||||
|
return snippet
|
||||||
|
|
||||||
|
|
||||||
|
def test_md2b_converts_numbered_lists_for_real():
|
||||||
|
node = shutil.which("node")
|
||||||
|
if not node:
|
||||||
|
pytest.skip("node absent : test comportemental md2b ignoré")
|
||||||
|
md = ("# Titre\n"
|
||||||
|
"1. premier\n"
|
||||||
|
"2. second\n"
|
||||||
|
"3) troisième\n"
|
||||||
|
"- puce\n"
|
||||||
|
"- [ ] à faire\n"
|
||||||
|
"> citation\n")
|
||||||
|
script = (
|
||||||
|
"function _tblIsData(){return false;}\n"
|
||||||
|
"function _tblSplitLine(){return [];}\n"
|
||||||
|
"const editor={mkB(t,c,e){const o={type:t,content:c||''};"
|
||||||
|
"if(e)Object.assign(o,e);return o;},\n"
|
||||||
|
f"{_md2b_source()}}};\n"
|
||||||
|
f"console.log(JSON.stringify(editor.md2b({json.dumps(md)})));\n"
|
||||||
|
)
|
||||||
|
out = subprocess.run([node, "-e", script], capture_output=True, text=True,
|
||||||
|
timeout=30, encoding="utf-8")
|
||||||
|
assert out.returncode == 0, out.stderr
|
||||||
|
blocks = json.loads(out.stdout)
|
||||||
|
assert [b["type"] for b in blocks] == [
|
||||||
|
"heading_1",
|
||||||
|
"numbered_list", "numbered_list", "numbered_list", # 1. 2. 3)
|
||||||
|
"bulleted_list", "to_do", "quote",
|
||||||
|
]
|
||||||
|
assert [b["content"] for b in blocks[1:4]] == ["premier", "second", "troisième"]
|
||||||
Reference in New Issue
Block a user