fix(ai): liste de modeles corrompue et cles i18n brutes (BUG-009)
Locales chargees en cache no-store + SW_VERSION v4 pour eviter les cles brutes (fr.json obsolete). Picker : styles critiques en ligne (popover/liste/options), plafond de 200 modeles avec indicateur, et non-chevauchement entre la liste de modeles et la bulle de capacites.
This commit is contained in:
+31
-2
@@ -39,6 +39,11 @@ const AI_CAPABILITY_KEYS = [
|
||||
'video', 'audio_speech', 'audio_transcription', 'vision',
|
||||
];
|
||||
|
||||
// Maximum number of model options rendered at once. Providers such as
|
||||
// OpenRouter expose hundreds of models; rendering them all makes the popover
|
||||
// heavy and hard to scan, so the search box is the primary way to narrow down.
|
||||
const MODEL_RENDER_LIMIT = 200;
|
||||
|
||||
function _readPicker() {
|
||||
try {
|
||||
return JSON.parse(localStorage.getItem(PICKER_STORAGE_KEY) || '{}');
|
||||
@@ -180,6 +185,11 @@ async function _buildPickerUI() {
|
||||
|
||||
const modelPop = document.createElement('div');
|
||||
modelPop.className = 'ai-picker-model-pop hidden';
|
||||
// Critical layout is applied inline too: if a stale stylesheet is served, the
|
||||
// popover must still stack vertically instead of flowing as a block of text.
|
||||
modelPop.style.cssText = 'position:absolute;top:calc(100% + 4px);left:0;z-index:60;'
|
||||
+ 'min-width:220px;max-width:320px;background:var(--bg-primary);border:1px solid var(--border);'
|
||||
+ 'border-radius:8px;box-shadow:0 8px 24px rgba(0,0,0,0.35);padding:6px;';
|
||||
|
||||
const modelSearch = document.createElement('input');
|
||||
modelSearch.type = 'text';
|
||||
@@ -187,10 +197,12 @@ async function _buildPickerUI() {
|
||||
modelSearch.autocomplete = 'off';
|
||||
modelSearch.placeholder = t('ai.model_search');
|
||||
modelSearch.setAttribute('aria-label', t('ai.model_search'));
|
||||
modelSearch.style.cssText = 'width:100%;box-sizing:border-box;';
|
||||
|
||||
const modelList = document.createElement('div');
|
||||
modelList.className = 'ai-picker-model-list';
|
||||
modelList.setAttribute('role', 'listbox');
|
||||
modelList.style.cssText = 'max-height:200px;overflow-y:auto;display:flex;flex-direction:column;';
|
||||
|
||||
modelPop.appendChild(modelSearch);
|
||||
modelPop.appendChild(modelList);
|
||||
@@ -208,13 +220,20 @@ async function _buildPickerUI() {
|
||||
infoBtn.title = t('ai.model_info');
|
||||
const capsPop = document.createElement('div');
|
||||
capsPop.className = 'ai-picker-caps-pop hidden';
|
||||
capsPop.style.cssText = 'position:absolute;top:calc(100% + 4px);right:0;z-index:60;'
|
||||
+ 'min-width:220px;max-width:300px;background:var(--bg-primary);border:1px solid var(--border);'
|
||||
+ 'border-radius:8px;box-shadow:0 8px 24px rgba(0,0,0,0.35);padding:8px 10px;';
|
||||
capsHost.appendChild(infoBtn);
|
||||
capsHost.appendChild(capsPop);
|
||||
|
||||
let _hideCapsTimer = null;
|
||||
let _longPressTimer = null;
|
||||
let _longPressFired = false;
|
||||
const showCaps = () => { clearTimeout(_hideCapsTimer); capsPop.classList.remove('hidden'); };
|
||||
const showCaps = () => {
|
||||
clearTimeout(_hideCapsTimer);
|
||||
_closePop(); // never overlap the model dropdown
|
||||
capsPop.classList.remove('hidden');
|
||||
};
|
||||
const hideCaps = () => { _hideCapsTimer = setTimeout(() => capsPop.classList.add('hidden'), 140); };
|
||||
infoBtn.addEventListener('mouseenter', showCaps);
|
||||
infoBtn.addEventListener('focus', showCaps);
|
||||
@@ -261,6 +280,7 @@ async function _buildPickerUI() {
|
||||
function _renderList() {
|
||||
const q = _normalizeText(modelSearch.value);
|
||||
const matches = _models.filter((m) => !q || _normalizeText(m).includes(q));
|
||||
const shown = matches.slice(0, MODEL_RENDER_LIMIT);
|
||||
modelList.innerHTML = '';
|
||||
_activeIndex = -1;
|
||||
if (!matches.length) {
|
||||
@@ -270,16 +290,24 @@ async function _buildPickerUI() {
|
||||
modelList.appendChild(empty);
|
||||
return;
|
||||
}
|
||||
matches.forEach((model) => {
|
||||
shown.forEach((model) => {
|
||||
const option = document.createElement('button');
|
||||
option.type = 'button';
|
||||
option.className = 'ai-picker-model-option';
|
||||
option.dataset.value = model;
|
||||
option.textContent = model || t('ai.model_default');
|
||||
option.style.cssText = 'display:block;width:100%;text-align:left;';
|
||||
if (model === modelSelect.value) option.classList.add('active');
|
||||
option.addEventListener('mousedown', (e) => { e.preventDefault(); _pick(model); });
|
||||
modelList.appendChild(option);
|
||||
});
|
||||
if (matches.length > shown.length) {
|
||||
const more = document.createElement('div');
|
||||
more.className = 'ai-picker-model-more';
|
||||
more.textContent = t('ai.model_more', { count: matches.length - shown.length });
|
||||
more.style.cssText = 'font-size:0.7rem;color:var(--text-secondary);padding:6px 8px;text-align:center;';
|
||||
modelList.appendChild(more);
|
||||
}
|
||||
_activeIndex = _visibleOptions().findIndex((el) => el.dataset.value === modelSelect.value);
|
||||
}
|
||||
|
||||
@@ -293,6 +321,7 @@ async function _buildPickerUI() {
|
||||
}
|
||||
|
||||
function _openPop() {
|
||||
capsPop.classList.add('hidden'); // never overlap the capability bubble
|
||||
modelPop.classList.remove('hidden');
|
||||
modelTrigger.setAttribute('aria-expanded', 'true');
|
||||
modelSearch.value = '';
|
||||
|
||||
+3
-1
@@ -136,7 +136,9 @@ export async function initI18n() {
|
||||
|
||||
async function _fetchLocale(locale) {
|
||||
try {
|
||||
const resp = await fetch('/static/locales/' + locale + '.json');
|
||||
// ``no-store`` bypasses the HTTP cache: locale files are not content-hashed
|
||||
// and a stale copy would surface raw keys in the UI (missing translations).
|
||||
const resp = await fetch('/static/locales/' + locale + '.json', { cache: 'no-store' });
|
||||
if (!resp.ok) throw new Error('HTTP ' + resp.status);
|
||||
return await resp.json();
|
||||
} catch (err) {
|
||||
|
||||
@@ -130,6 +130,7 @@
|
||||
"ai.model_settings": "Provider & model",
|
||||
"ai.model_search": "Search a model…",
|
||||
"ai.model_info": "Model capabilities",
|
||||
"ai.model_more": "… {count} more — refine your search",
|
||||
"ai.add_context": "Add context",
|
||||
"ai.attach_image": "Attach an image",
|
||||
"ai.cap_audio_speech": "Audio Speech",
|
||||
|
||||
@@ -130,6 +130,7 @@
|
||||
"ai.model_settings": "Fournisseur & modèle",
|
||||
"ai.model_search": "Rechercher un modèle…",
|
||||
"ai.model_info": "Capacités du modèle",
|
||||
"ai.model_more": "… {count} autres — affinez la recherche",
|
||||
"ai.add_context": "Ajouter un contexte",
|
||||
"ai.attach_image": "Joindre une image",
|
||||
"ai.cap_audio_speech": "Audio Speech",
|
||||
|
||||
+4
-3
@@ -9385,11 +9385,12 @@ body.bookslm-resizing { cursor: ew-resize; user-select: none; }
|
||||
background: var(--bg-secondary); color: var(--text-primary); border: 1px solid var(--border);
|
||||
border-radius: 6px; padding: 5px 8px; margin-bottom: 4px; }
|
||||
.ai-picker-model-list { max-height: 200px; overflow-y: auto; display: flex; flex-direction: column; }
|
||||
.ai-picker-model-option { text-align: left; font-size: 0.72rem; padding: 5px 8px; border-radius: 5px;
|
||||
background: none; border: none; color: var(--text-primary); cursor: pointer;
|
||||
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.ai-picker-model-option { display: block; width: 100%; text-align: left; font-size: 0.72rem;
|
||||
padding: 5px 8px; border-radius: 5px; background: none; border: none; color: var(--text-primary);
|
||||
cursor: pointer; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.ai-picker-model-option:hover, .ai-picker-model-option.active { background: var(--surface2); }
|
||||
.ai-picker-model-empty { font-size: 0.7rem; color: var(--text-secondary); padding: 6px 8px; text-align: center; }
|
||||
.ai-picker-model-more { font-size: 0.7rem; color: var(--text-secondary); padding: 6px 8px; text-align: center; }
|
||||
/* Capability info button (ⓘ) + informational bubble. */
|
||||
.ai-picker-caps-host { position: relative; display: inline-flex; }
|
||||
.ai-picker-info { background: none; border: none; cursor: pointer; color: var(--text-secondary);
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
/* ObsiGate — Service Worker (PWA offline + push)
|
||||
* Version: 3 (2026-09-11)
|
||||
* Version: 4 (2026-09-12)
|
||||
*
|
||||
* Caching policy (fixes stale-asset loads behind Cloudflare / mobile):
|
||||
* - navigations & code (HTML / JS / CSS / manifest) : NETWORK-FIRST
|
||||
@@ -11,7 +11,7 @@
|
||||
* cache or Cloudflare does NOT clear the Service Worker Cache Storage, which is
|
||||
* a separate store. Bumping SW_VERSION invalidates it on every release.
|
||||
*/
|
||||
const SW_VERSION = 'v3';
|
||||
const SW_VERSION = 'v4';
|
||||
const CODE_CACHE = `obsigate-code-${SW_VERSION}`;
|
||||
const RUNTIME_CACHE = `obsigate-runtime-${SW_VERSION}`;
|
||||
const API_CACHE = `obsigate-api-${SW_VERSION}`;
|
||||
|
||||
Reference in New Issue
Block a user