feat(ai): sidebar epuree, suivi visuel des requetes et correctif menu @ (BUG-012, #82)
BUG-012: le menu @ est toujours rendu et _mentionVault() retombe sur le vault de contexte puis le premier vault disponible. #82: suppression des intitules Fournisseur & modele / Fournisseur:, description du contexte General en info-bulle, placeholder retire, bouton Envoyer en emoji, indicateur d'activite (envoi/reception/outils/confirmation/succes/echec), SW_VERSION v5.
This commit is contained in:
@@ -221,8 +221,26 @@ et [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
`localStorage`) : l'envoi bascule vers `/api/ai/bookslm/agent`, affiche la trace des appels d'outils
|
||||
et les cartes de confirmation (Apply + aperçu diff) ; `/chat` reste le défaut. i18n FR/EN.
|
||||
|
||||
### Modifié
|
||||
|
||||
- **#82 Assistant IA — barre latérale épurée & suivi visuel des requêtes** — la section
|
||||
« Fournisseur & modèle » perd ses intitulés (titre de section et libellé « Fournisseur : »)
|
||||
pour rester discrète ; la description du contexte Général n'est plus affichée (bandeau de
|
||||
statut masqué, description accessible au **survol** de l'en-tête) ; le placeholder
|
||||
« Posez une question sur ces documents… » est retiré ; le bouton « Envoyer » devient un **emoji
|
||||
compact** (✈️). Un **indicateur d'activité** apparaît pendant le traitement et détaille le
|
||||
workflow : envoi, réception de la réponse, appels d'outils (`Outil : X…`), attente de
|
||||
confirmation, succès ou échec. i18n FR/EN (`ai.activity_*`, `ai.composer_label`). Tests JSDOM
|
||||
(`tests/frontend/ai.test.mjs` +1).
|
||||
|
||||
### Corrigé
|
||||
|
||||
- **BUG-012 — Assistant IA : la commande `@` n'affichait aucun menu en mode Général** — sans vault
|
||||
résolu, la détection `@` ne listait rien (menu masqué) et le backend ignorait ensuite le
|
||||
contexte. Le menu est désormais **toujours rendu** et `_mentionVault()` retombe sur le vault de
|
||||
contexte de la barre latérale, puis sur le premier vault disponible, avant de basculer sur
|
||||
`vault=all` pour les recherches. Tests JSDOM (`tests/frontend/ai.test.mjs` +1).
|
||||
|
||||
- **BUG-011 — Assistant IA : noms de modèles illisibles dans la liste déroulante** — la liste
|
||||
déroulante des modèles s'ouvrait alignée à gauche (`left: 0`) et pouvait dépasser le bord droit
|
||||
de la barre latérale (ancrée à droite), rendant les noms tronqués/illisibles ; elle est
|
||||
|
||||
@@ -121,6 +121,7 @@ Avant de corriger quoi que ce soit, un agent IA doit :
|
||||
| *BUG-009* | Assistant IA : liste de modèles corrompue (art ASCII) et clés i18n brutes (`ai.model_search`) sous OpenRouter | 🟢 corrigé | P1 | 📱 frontend | IA | `frontend/js/ai.js`, `frontend/js/i18n.js`, `frontend/sw.js`, `frontend/style.css` | Ouvrir le menu modèle sous « openrouter » | Locale non content-hashée en cache HTTP → `cache:'no-store'` + bump `SW_VERSION` v4 ; styles critiques du picker appliqués **en ligne** (popover absolu, liste en colonne, options `display:block`) ; rendu plafonné à 200 modèles + indicateur « … N autres » | OpenRouter expose plusieurs centaines de modèles ; si `style.css` est en cache, la liste s'affichait en bloc inline (art ASCII) et les clés i18n brutes provenaient d'un `fr.json` obsolète. Tests : `tests/frontend/ai.test.mjs` (+1) |
|
||||
| *BUG-010* | Assistant IA : la commande `@` n'ajoute pas le contexte à la requête en mode Général | 🟢 corrigé | P1 | 📱 frontend | IA | `frontend/js/bookslm.js` | Assistant en mode Général, taper `@fichier`, sélectionner, envoyer | Capture du `vault` renvoyé par `/api/tree-search` dans la sélection `@` ; `_contextVault()` propage ce vault aux requêtes `/context` et `/chat` ; recherches `@` suivantes limitées à ce vault | En mode Général, `_vault` est nul : la requête partait sans vault et le backend ignorait `extra_files`. Tests : `tests/frontend/ai.test.mjs` (+1) |
|
||||
| *BUG-011* | Assistant IA : noms de modèles illisibles dans la liste déroulante | 🟢 corrigé | P2 | 📱 frontend | IA | `frontend/js/ai.js`, `frontend/style.css` | Ouvrir le menu modèle dans la barre latérale de l'assistant | Popover aligné à droite du déclencheur (`right:0`), largeur 340 px bornée à `100vw - 24px`, noms sur plusieurs lignes (`overflow-wrap:anywhere`, police 0,78 rem) + `title` | La liste s'ouvrait vers la droite et dépassait le bord de la sidebar ancrée à droite ; noms tronqués. Tests : `tests/frontend/ai.test.mjs` |
|
||||
| *BUG-012* | Assistant IA : la commande `@` n'affiche aucun menu de sélection en mode Général | 🟢 corrigé | P1 | 📱 frontend | IA | `frontend/js/bookslm.js` | Assistant en mode Général (aucun vault), taper `@` | `_showMentionMenu` rend **toujours** le menu (même vide) et `_mentionVault()` retombe sur `state.selectedContextVault` puis le premier `state.allVaults`, avant `vault=all` pour les requêtes | Sans vault résolu, l'ancien code masquait le menu et le backend ignorait le contexte. Tests : `tests/frontend/ai.test.mjs` (+1) |
|
||||
|
||||
### TODOs techniques (améliorations / nouvelles tâches)
|
||||
|
||||
@@ -147,6 +148,7 @@ Avant de corriger quoi que ce soit, un agent IA doit :
|
||||
| 2026-09-12 | BUG-007, BUG-008, #82 | Correction + refonte UI | `frontend/js/bookslm.js`, `frontend/js/ai.js`, `frontend/style.css`, `frontend/locales/{fr,en}.json`, `backend/bookslm_routes.py`, `tests/frontend/ai.test.mjs`, `tests/test_bookslm.py` | BUG-007 : motifs Unicode + recherche NFD insensible aux accents + navigation ↑/↓ fiabilisée (jeton de séquence, scrollIntoView). BUG-008 : contexte ad-hoc `@` pris en compte en mode Général (backend) + repli `vault=all`. #82 : section « Fournisseur & modèle » compacte (recherche modèle + bulle capacités ℹ️ au survol/clic/appui long). Vérifié : tests frontend 41/41, `tests/test_bookslm.py` 47 passed. | 🟢 corrigé (en attente vérif utilisateur) |
|
||||
| 2026-09-12 | BUG-009 | Correction | `frontend/js/ai.js`, `frontend/js/i18n.js`, `frontend/sw.js`, `frontend/style.css`, `frontend/locales/{fr,en}.json`, `tests/frontend/ai.test.mjs` | BUG-009 : locale non content-hashée servie depuis le cache HTTP → clés i18n brutes ; passage en `cache:'no-store'` + `SW_VERSION` v4. Liste de modèles corrompue (OpenRouter, styles en cache) → styles critiques en ligne, options `display:block`, plafond 200 + indicateur. Vérifié : tests frontend 42/42. | 🟢 corrigé (en attente vérif utilisateur) |
|
||||
| 2026-09-12 | BUG-010, BUG-011 | Correction | `frontend/js/bookslm.js`, `frontend/js/ai.js`, `frontend/style.css`, `tests/frontend/ai.test.mjs` | BUG-010 : capture du `vault` des résultats `@` + `_contextVault()` propagé aux requêtes `/context`/`/chat` (mode Général). BUG-011 : popover modèle aligné à droite, largeur 340 px, noms multi-lignes + `title`. Vérifié en navigateur (Playwright) et tests frontend 43/43. | 🟢 corrigé (en attente vérif utilisateur) |
|
||||
| 2026-09-12 | BUG-012, #82 | Correction + UX | `frontend/js/bookslm.js`, `frontend/js/ai.js`, `frontend/style.css`, `frontend/sw.js`, `frontend/locales/{fr,en}.json`, `tests/frontend/ai.test.mjs` | BUG-012 : le menu `@` est toujours rendu et `_mentionVault()` retombe sur le vault de contexte puis le premier vault disponible. #82 : suppression des intitulés « Fournisseur & modèle » / « Fournisseur : », description du contexte Général déplacée en info-bulle, placeholder retiré, bouton Envoyer en emoji ✈️, indicateur d'activité (envoi/réception/outils/confirmation/succès/échec), `SW_VERSION` v5. Vérifié Playwright + tests frontend 45/45. | 🟢 corrigé (en attente vérif utilisateur) |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -52,7 +52,18 @@
|
||||
propage ce vault aux requêtes `/context` et `/chat` et les recherches suivantes restent dans
|
||||
ce vault (BUG-010, mode Général).
|
||||
|
||||
## F. Points d'attention
|
||||
## F. Barre latérale épurée & suivi visuel (BUG-012, #82) — ✅ livré
|
||||
- [x] Section « Fournisseur & modèle » sans intitulés (ni titre de section, ni « Fournisseur : »).
|
||||
- [x] Description du contexte Général retirée de l'affichage (bandeau de statut masqué) et
|
||||
exposée via `title` au survol de l'en-tête.
|
||||
- [x] Placeholder « Posez une question sur ces documents… » retiré ; bouton « Envoyer » remplacé
|
||||
par l'emoji compact ✈️.
|
||||
- [x] **Indicateur d'activité** (`.bookslm-activity`) affichant le workflow : envoi, réception,
|
||||
outil en cours, attente de confirmation, terminé, échec (`ai.activity_*`).
|
||||
- [x] `@` : le menu est toujours rendu et `_mentionVault()` retombe sur le vault de contexte puis
|
||||
le premier vault disponible (BUG-012).
|
||||
|
||||
## G. Points d'attention
|
||||
- La table de capacités reste **statique** (`backend/model_capabilities.py`) : un modèle inconnu
|
||||
retombe sur le défaut du fournisseur.
|
||||
- Le select masqué est conservé uniquement pour la rétro-compatibilité ; ne pas le supprimer sans
|
||||
|
||||
+2
-6
@@ -139,13 +139,10 @@ async function _buildPickerUI() {
|
||||
const wrap = document.createElement('div');
|
||||
wrap.className = 'ai-picker';
|
||||
|
||||
// ── Provider select (compact) ──
|
||||
const providerLabel = document.createElement('span');
|
||||
providerLabel.className = 'ai-picker-label';
|
||||
providerLabel.textContent = t('ai.provider') + ':';
|
||||
|
||||
// ── Provider select (compact, no label to keep the toolbar discreet) ──
|
||||
const providerSelect = document.createElement('select');
|
||||
providerSelect.className = 'ai-picker-select';
|
||||
providerSelect.setAttribute('aria-label', t('ai.provider'));
|
||||
const defaultOpt = document.createElement('option');
|
||||
defaultOpt.value = '';
|
||||
defaultOpt.textContent = t('ai.provider_default');
|
||||
@@ -445,7 +442,6 @@ async function _buildPickerUI() {
|
||||
_renderList();
|
||||
}
|
||||
|
||||
wrap.appendChild(providerLabel);
|
||||
wrap.appendChild(providerSelect);
|
||||
wrap.appendChild(modelSelect);
|
||||
wrap.appendChild(combo);
|
||||
|
||||
+79
-18
@@ -107,6 +107,8 @@ class BooksLM {
|
||||
this._menuKind = null;
|
||||
// Monotonic token used to drop stale async menu renders (fast typing).
|
||||
this._menuSeq = 0;
|
||||
// Timer that hides the "done" activity indicator after a short delay.
|
||||
this._activityHideTimer = null;
|
||||
}
|
||||
|
||||
_readAgentMode() {
|
||||
@@ -333,6 +335,20 @@ class BooksLM {
|
||||
return this._resolveVault();
|
||||
}
|
||||
|
||||
/**
|
||||
* Concrete vault used to browse files in the `@` mention menu. Falls back to
|
||||
* the sidebar's selected context vault (then the first vault) so the menu can
|
||||
* list files even in General mode where no vault is otherwise resolved.
|
||||
*/
|
||||
_mentionVault() {
|
||||
const resolved = this._contextVault();
|
||||
if (resolved) return resolved;
|
||||
const selected = state.selectedContextVault;
|
||||
if (selected && selected !== 'all') return selected;
|
||||
const first = Array.isArray(state.allVaults) ? state.allVaults[0] : null;
|
||||
return (first && (first.name || first)) || null;
|
||||
}
|
||||
|
||||
/** Open a file mentioned in an assistant answer in the main viewer. */
|
||||
async _openFileLink(path) {
|
||||
const vault = this._resolveVault();
|
||||
@@ -433,7 +449,9 @@ class BooksLM {
|
||||
return {
|
||||
icon: 'sparkles',
|
||||
title: t('ai.context_general'),
|
||||
subtitle: t('ai.context_general_desc'),
|
||||
// The description is kept for the tooltip only, to keep the header clean.
|
||||
subtitle: '',
|
||||
tooltip: t('ai.context_general_desc'),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -468,12 +486,15 @@ class BooksLM {
|
||||
</div>
|
||||
<div class="bookslm-history-menu hidden"></div>
|
||||
<div class="bookslm-toolbar">
|
||||
<span class="bookslm-toolbar-label">${t('ai.model_settings')}</span>
|
||||
<span class="bookslm-picker-host"></span>
|
||||
</div>
|
||||
<div class="bookslm-status"></div>
|
||||
<div class="bookslm-suggestions"></div>
|
||||
<div class="bookslm-messages"></div>
|
||||
<div class="bookslm-activity hidden" role="status" aria-live="polite">
|
||||
<span class="bookslm-activity-spinner" aria-hidden="true"></span>
|
||||
<span class="bookslm-activity-text"></span>
|
||||
</div>
|
||||
<div class="bookslm-attachments"></div>
|
||||
<div class="bookslm-menu-layer">
|
||||
<div class="bookslm-command-menu hidden" role="listbox" aria-label="${t('ai.commands')}"></div>
|
||||
@@ -482,8 +503,8 @@ class BooksLM {
|
||||
<div class="bookslm-input-area">
|
||||
<button class="bookslm-btn-attach" title="${t('ai.attach_image')}" aria-label="${t('ai.attach_image')}"><i data-lucide="image-plus" style="width:18px;height:18px"></i></button>
|
||||
<input type="file" class="bookslm-file-input" accept="image/*" multiple hidden>
|
||||
<textarea placeholder="${t('bookslm.placeholder')}" rows="1" aria-label="${t('bookslm.placeholder')}"></textarea>
|
||||
<button class="bookslm-btn-send" title="${t('bookslm.send')}">${t('bookslm.send')}</button>
|
||||
<textarea placeholder="${t('bookslm.placeholder')}" rows="1" aria-label="${t('ai.composer_label')}"></textarea>
|
||||
<button class="bookslm-btn-send" title="${t('bookslm.send')}" aria-label="${t('bookslm.send')}">✈️</button>
|
||||
</div>
|
||||
<div class="bookslm-input-hint">${t('ai.input_hint')}</div>
|
||||
`;
|
||||
@@ -820,12 +841,7 @@ class BooksLM {
|
||||
async _showMentionMenu(query) {
|
||||
if (!this._panel) return;
|
||||
const seq = ++this._menuSeq;
|
||||
const vault = this._contextVault();
|
||||
if (!vault && !query) {
|
||||
// Nothing to list without a vault; a typed query can still search all.
|
||||
this._hideMenus();
|
||||
return;
|
||||
}
|
||||
const vault = this._mentionVault();
|
||||
try {
|
||||
let results = [];
|
||||
if (query) {
|
||||
@@ -834,24 +850,25 @@ class BooksLM {
|
||||
`/api/tree-search?q=${encodeURIComponent(query)}&vault=${encodeURIComponent(searchVault)}`
|
||||
);
|
||||
results = (data.results || []).slice(0, 20);
|
||||
} else {
|
||||
} else if (vault) {
|
||||
const data = await api(
|
||||
`/api/vault/${encodeURIComponent(vault)}/files?limit=20&recursive=true`
|
||||
);
|
||||
results = (data.files || []).slice(0, 20).map((f) => ({ path: f.path, type: 'file' }));
|
||||
results = (data.files || []).slice(0, 20).map((f) => ({ path: f.path, type: 'file', vault }));
|
||||
}
|
||||
if (seq !== this._menuSeq) return; // superseded while the request ran
|
||||
const items = results.map((r) => {
|
||||
const path = r.path || r;
|
||||
const rawType = r.type || (this._classifyPath(path) === 'dir' ? 'dir' : 'file');
|
||||
const type = rawType === 'directory' ? 'dir' : rawType;
|
||||
return { id: path, label: path, type, vault: r.vault || null };
|
||||
return { id: path, label: path, type, vault: r.vault || vault || null };
|
||||
});
|
||||
// Always render the menu (even empty) so typing `@` gives visible feedback.
|
||||
this._renderMenu('mention', items, (item) => this._selectMention(item));
|
||||
} catch (e) {
|
||||
if (seq !== this._menuSeq) return;
|
||||
console.warn('AI assistant: mention search failed', e);
|
||||
this._hideMenus();
|
||||
this._renderMenu('mention', [], () => {});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1185,37 +1202,68 @@ class BooksLM {
|
||||
_updateHeader() {
|
||||
if (!this._panel) return;
|
||||
const meta = this._contextMeta();
|
||||
const titles = this._panel.querySelector('.bookslm-header-titles');
|
||||
const title = this._panel.querySelector('.bookslm-title');
|
||||
const subtitle = this._panel.querySelector('.bookslm-subtitle');
|
||||
const icon = this._panel.querySelector('.bookslm-context-icon i');
|
||||
if (title) title.textContent = meta.title;
|
||||
if (subtitle) subtitle.textContent = meta.subtitle || '';
|
||||
if (titles) titles.title = meta.tooltip || meta.subtitle || meta.title || '';
|
||||
if (icon) icon.setAttribute('data-lucide', meta.icon);
|
||||
if (typeof safeCreateIcons === 'function') safeCreateIcons();
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the assistant activity state in the panel (visual workflow).
|
||||
* @param {'idle'|'working'|'done'|'error'} kind
|
||||
* @param {string} [text]
|
||||
*/
|
||||
_setActivity(kind, text) {
|
||||
if (!this._panel) return;
|
||||
if (this._activityHideTimer) {
|
||||
clearTimeout(this._activityHideTimer);
|
||||
this._activityHideTimer = null;
|
||||
}
|
||||
const el = this._panel.querySelector('.bookslm-activity');
|
||||
if (!el) return;
|
||||
const label = el.querySelector('.bookslm-activity-text');
|
||||
if (label) label.textContent = text || '';
|
||||
el.classList.toggle('hidden', kind === 'idle');
|
||||
el.classList.toggle('working', kind === 'working');
|
||||
el.classList.toggle('done', kind === 'done');
|
||||
el.classList.toggle('error', kind === 'error');
|
||||
}
|
||||
|
||||
_updateStatus(data) {
|
||||
if (!this._panel) return;
|
||||
const status = this._panel.querySelector('.bookslm-status');
|
||||
if (!status) return;
|
||||
|
||||
if (data && data.error) {
|
||||
status.classList.remove('hidden');
|
||||
status.innerHTML = `<span class="bookslm-status-error">⚠ ${t('bookslm.no_context')}</span>`;
|
||||
return;
|
||||
}
|
||||
if (data && Array.isArray(data.files)) {
|
||||
if (data.scope === MODE.GENERAL) {
|
||||
// No document context: keep the header clean. The description is
|
||||
// available as a tooltip on the header title.
|
||||
status.classList.add('hidden');
|
||||
status.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
status.classList.remove('hidden');
|
||||
const count = data.files.length;
|
||||
const chars = data.total_chars || 0;
|
||||
const maxChars = data.max_total_chars || 200000;
|
||||
const pct = Math.min(100, Math.round((chars / maxChars) * 100));
|
||||
const label = data.scope === MODE.GENERAL
|
||||
? t('ai.context_general_desc')
|
||||
: `${t('bookslm.files_indexed', { count })}, ${t('bookslm.chars_loaded', { chars: Math.round(chars / 1000) + 'K' })}`;
|
||||
const label = `${t('bookslm.files_indexed', { count })}, ${t('bookslm.chars_loaded', { chars: Math.round(chars / 1000) + 'K' })}`;
|
||||
status.innerHTML = `
|
||||
<span class="bookslm-status-text">${label}</span>
|
||||
<div class="bookslm-context-bar" role="progressbar" aria-valuenow="${pct}" aria-valuemin="0" aria-valuemax="100"><div class="bookslm-context-fill" style="width:${pct}%"></div></div>
|
||||
`;
|
||||
} else if (this._isLoading) {
|
||||
status.classList.remove('hidden');
|
||||
status.textContent = '⏳ …';
|
||||
}
|
||||
}
|
||||
@@ -1630,6 +1678,7 @@ class BooksLM {
|
||||
if (sendBtn) sendBtn.disabled = true;
|
||||
|
||||
this._abortCtrl = new AbortController();
|
||||
this._setActivity('working', t('ai.activity_sending'));
|
||||
|
||||
try {
|
||||
let resp = await this._postChat(payload);
|
||||
@@ -1643,6 +1692,7 @@ class BooksLM {
|
||||
throw new Error(detail);
|
||||
}
|
||||
|
||||
this._setActivity('working', t('ai.activity_streaming'));
|
||||
await this._streamResponse(resp, assistantMsg, payload);
|
||||
|
||||
if (!assistantMsg.sources.length && this._contextFiles.length) {
|
||||
@@ -1654,10 +1704,15 @@ class BooksLM {
|
||||
})
|
||||
.map((f) => f.path || f);
|
||||
}
|
||||
this._setActivity('done', t('ai.activity_done'));
|
||||
this._activityHideTimer = setTimeout(() => this._setActivity('idle'), 1500);
|
||||
} catch (e) {
|
||||
if (e.name !== 'AbortError') {
|
||||
assistantMsg.content = `⚠ Error: ${e.message}`;
|
||||
console.warn('AI assistant chat error:', e);
|
||||
this._setActivity('error', t('ai.activity_error'));
|
||||
} else {
|
||||
this._setActivity('idle');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1714,6 +1769,7 @@ class BooksLM {
|
||||
|
||||
if (currentEvent === 'tool') {
|
||||
assistantMsg.toolCalls.push({ name: data.name, ok: data.ok !== false, arguments: data.arguments || {} });
|
||||
this._setActivity('working', t('ai.activity_tool', { name: data.name }));
|
||||
return;
|
||||
}
|
||||
if (currentEvent === 'confirmation') {
|
||||
@@ -1721,15 +1777,20 @@ class BooksLM {
|
||||
pending: data.pending || data,
|
||||
messages: data.messages || [],
|
||||
};
|
||||
this._setActivity('working', t('ai.activity_confirmation'));
|
||||
return;
|
||||
}
|
||||
if (currentEvent === 'error' || data.error) {
|
||||
const msg = data.error || t('bookslm.error');
|
||||
assistantMsg.content += (assistantMsg.content ? '\n\n' : '') + '⚠ ' + msg;
|
||||
this._setActivity('error', t('ai.activity_error'));
|
||||
return;
|
||||
}
|
||||
const chunk = data.token || data.content || '';
|
||||
if (chunk) assistantMsg.content += chunk;
|
||||
if (chunk) {
|
||||
if (!assistantMsg.content) this._setActivity('working', t('ai.activity_streaming'));
|
||||
assistantMsg.content += chunk;
|
||||
}
|
||||
if (data.sources) assistantMsg.sources = data.sources;
|
||||
};
|
||||
|
||||
|
||||
@@ -142,6 +142,13 @@
|
||||
"ai.cap_video": "Video",
|
||||
"ai.cap_vision": "Vision capable",
|
||||
"ai.commands": "Commands",
|
||||
"ai.activity_sending": "Sending request…",
|
||||
"ai.activity_streaming": "Receiving response…",
|
||||
"ai.activity_tool": "Tool: {name}…",
|
||||
"ai.activity_confirmation": "Waiting for confirmation…",
|
||||
"ai.activity_error": "Request failed",
|
||||
"ai.activity_done": "Done",
|
||||
"ai.composer_label": "Message to the assistant",
|
||||
"ai.create_skill": "Create a skill",
|
||||
"ai.image": "image",
|
||||
"ai.keys": "Stored API keys",
|
||||
@@ -1712,7 +1719,7 @@
|
||||
"bookslm.title": "BooksLM",
|
||||
"bookslm.files_indexed": "{count} files indexed",
|
||||
"bookslm.chars_loaded": "{chars} chars loaded",
|
||||
"bookslm.placeholder": "Ask a question about these documents...",
|
||||
"bookslm.placeholder": "",
|
||||
"bookslm.send": "Send",
|
||||
"bookslm.new_conversation": "New conversation",
|
||||
"bookslm.export": "Export conversation",
|
||||
|
||||
@@ -142,6 +142,13 @@
|
||||
"ai.cap_video": "Video",
|
||||
"ai.cap_vision": "Vision capable",
|
||||
"ai.commands": "Commandes",
|
||||
"ai.activity_sending": "Envoi de la requête…",
|
||||
"ai.activity_streaming": "Réponse en cours…",
|
||||
"ai.activity_tool": "Outil : {name}…",
|
||||
"ai.activity_confirmation": "En attente de confirmation…",
|
||||
"ai.activity_error": "Échec de la requête",
|
||||
"ai.activity_done": "Terminé",
|
||||
"ai.composer_label": "Message à l'assistant",
|
||||
"ai.create_skill": "Créer un skill",
|
||||
"ai.image": "image",
|
||||
"ai.keys": "Clés API enregistrées",
|
||||
@@ -1712,7 +1719,7 @@
|
||||
"bookslm.title": "BooksLM",
|
||||
"bookslm.files_indexed": "{count} fichiers indexés",
|
||||
"bookslm.chars_loaded": "{chars} caractères chargés",
|
||||
"bookslm.placeholder": "Posez une question sur ces documents...",
|
||||
"bookslm.placeholder": "",
|
||||
"bookslm.send": "Envoyer",
|
||||
"bookslm.new_conversation": "Nouvelle conversation",
|
||||
"bookslm.export": "Exporter la conversation",
|
||||
|
||||
@@ -9202,6 +9202,19 @@ body.popup-mode .content-area {
|
||||
.bookslm-status .bookslm-context-bar { flex: 1; height: 4px; border-radius: 2px;
|
||||
background: var(--surface2); overflow: hidden; }
|
||||
.bookslm-status .bookslm-context-fill { height: 100%; border-radius: 2px; background: var(--accent); transition: width 0.3s; }
|
||||
/* Assistant activity indicator (visual workflow: sending/streaming/tools/done/error). */
|
||||
.bookslm-activity { display: flex; align-items: center; gap: 8px; padding: 6px 16px;
|
||||
font-size: 12px; color: var(--text-secondary); border-top: 1px solid var(--border);
|
||||
background: var(--bg-secondary); }
|
||||
.bookslm-activity.hidden { display: none; }
|
||||
.bookslm-activity.error { color: #f87171; }
|
||||
.bookslm-activity.done { color: var(--success, #3fb950); }
|
||||
.bookslm-activity-spinner { width: 12px; height: 12px; border: 2px solid var(--border);
|
||||
border-top-color: var(--accent); border-radius: 50%; flex-shrink: 0;
|
||||
animation: bookslm-spin 0.8s linear infinite; }
|
||||
.bookslm-activity.done .bookslm-activity-spinner,
|
||||
.bookslm-activity.error .bookslm-activity-spinner { animation: none; border-top-color: currentColor; }
|
||||
@keyframes bookslm-spin { to { transform: rotate(360deg); } }
|
||||
.bookslm-messages { flex: 1; overflow-y: auto; padding: 16px; display: flex; flex-direction: column; gap: 12px; }
|
||||
.bookslm-bubble { max-width: 85%; padding: 10px 14px; border-radius: 12px; font-size: 14px; line-height: 1.5; word-wrap: break-word; }
|
||||
.bookslm-bubble.user { align-self: flex-end; background: var(--accent); color: #fff; border-bottom-right-radius: 4px; }
|
||||
@@ -9220,6 +9233,7 @@ body.popup-mode .content-area {
|
||||
.bookslm-input-area textarea:focus { border-color: var(--accent); outline: none; }
|
||||
.bookslm-input-area button { padding: 8px 16px; border-radius: 8px; border: none;
|
||||
background: var(--accent); color: #fff; cursor: pointer; font-size: 14px; }
|
||||
.bookslm-input-area button.bookslm-btn-send { padding: 8px 12px; font-size: 16px; line-height: 1; }
|
||||
.bookslm-input-area button:hover { opacity: 0.9; }
|
||||
.bookslm-input-area button:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
.bookslm-input-hint { padding: 0 16px 10px; font-size: 10px; color: var(--text-secondary);
|
||||
|
||||
+1
-1
@@ -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 = 'v4';
|
||||
const SW_VERSION = 'v5';
|
||||
const CODE_CACHE = `obsigate-code-${SW_VERSION}`;
|
||||
const RUNTIME_CACHE = `obsigate-runtime-${SW_VERSION}`;
|
||||
const API_CACHE = `obsigate-api-${SW_VERSION}`;
|
||||
|
||||
@@ -1024,6 +1024,52 @@ async function main() {
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
// ── 42b. Assistant activity indicator (#82) ──
|
||||
await test("activity indicator reflects the request workflow", () => {
|
||||
const b = new BooksLM();
|
||||
b._panel = b._render();
|
||||
document.body.appendChild(b._panel);
|
||||
const el = b._panel.querySelector(".bookslm-activity");
|
||||
assert.ok(el, "activity element present");
|
||||
assert.ok(el.classList.contains("hidden"), "hidden initially");
|
||||
b._setActivity("working", "Envoi…");
|
||||
assert.ok(!el.classList.contains("hidden"), "shown while working");
|
||||
assert.ok(el.classList.contains("working"));
|
||||
assert.equal(el.querySelector(".bookslm-activity-text").textContent, "Envoi…");
|
||||
b._setActivity("error", "Échec");
|
||||
assert.ok(el.classList.contains("error"));
|
||||
b._setActivity("idle");
|
||||
assert.ok(el.classList.contains("hidden"), "hidden when idle");
|
||||
b._panel.remove();
|
||||
});
|
||||
|
||||
await test("mention menu falls back to an available vault", async () => {
|
||||
localStorage.clear();
|
||||
const stateMod = await import(pathToFileURL(path.join(JS_DIR, "state.js")).href);
|
||||
const prev = stateMod.state.allVaults;
|
||||
stateMod.state.allVaults = [{ name: "FallbackVault" }];
|
||||
let lastUrl = "";
|
||||
globalThis.fetch = async (url) => {
|
||||
lastUrl = String(url);
|
||||
if (lastUrl.includes("/files")) {
|
||||
return { ok: true, status: 200, json: async () => ({ files: [{ path: "a.md" }] }) };
|
||||
}
|
||||
return { ok: true, status: 200, json: async () => ({}) };
|
||||
};
|
||||
const b = new BooksLM();
|
||||
b._panel = b._render();
|
||||
document.body.appendChild(b._panel);
|
||||
b._mode = MODE.GENERAL;
|
||||
b._vault = null;
|
||||
await b._showMentionMenu("");
|
||||
const menu = b._panel.querySelector(".bookslm-mention-menu");
|
||||
assert.ok(!menu.classList.contains("hidden"), "empty-query menu is shown");
|
||||
assert.ok(lastUrl.includes("FallbackVault"), "uses the fallback vault");
|
||||
stateMod.state.allVaults = prev;
|
||||
b._panel.remove();
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
// ── Summary ──
|
||||
console.log(`\n${passCount}/${testCount} tests passed`);
|
||||
if (passCount !== testCount) {
|
||||
|
||||
Reference in New Issue
Block a user