feat(ui): badge version visible + FAB AI flottant
CI / lint (push) Successful in 37s
CI / security (push) Successful in 25s
CI / test (push) Successful in 48s
CI / build (push) Successful in 21s
CI / e2e (push) Successful in 6m4s
Desktop Build / build-windows (push) Canceled after 0s
Desktop Build / build-linux (push) Canceled after 0s
CI / lint (push) Successful in 37s
CI / security (push) Successful in 25s
CI / test (push) Successful in 48s
CI / build (push) Successful in 21s
CI / e2e (push) Successful in 6m4s
Desktop Build / build-windows (push) Canceled after 0s
Desktop Build / build-linux (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
// ObsiGate — Floating Action Button (FAB) to open/close the BooksLM AI sidebar.
|
||||
// Always visible in the bottom-right corner; hides while BooksLM is open.
|
||||
import booksLM from './bookslm.js';
|
||||
import { state } from './state.js';
|
||||
import { showToast, TabManager } from './ui.js';
|
||||
import { t } from './i18n.js';
|
||||
import { safeCreateIcons } from './utils.js';
|
||||
|
||||
let _fab = null;
|
||||
|
||||
// Mirror palette.js getCurrentFile(): prefer the active tab, then fall back to state.
|
||||
function getCurrentFile() {
|
||||
const id = TabManager._activeTabId;
|
||||
if (id) {
|
||||
const tab = TabManager._tabs.find((x) => x.id === id);
|
||||
if (tab) return { vault: tab.vault, path: tab.path };
|
||||
}
|
||||
if (state.currentVault && state.currentPath) return { vault: state.currentVault, path: state.currentPath };
|
||||
return null;
|
||||
}
|
||||
|
||||
// The BooksLM panel is considered visible when it has `.open` without `.hidden`
|
||||
// (the header toggle collapses it off-screen via `.hidden`).
|
||||
function isBooksLmVisible() {
|
||||
const panel = document.querySelector('.bookslm-panel');
|
||||
return !!(panel && panel.classList.contains('open') && !panel.classList.contains('hidden'));
|
||||
}
|
||||
|
||||
function syncVisibility() {
|
||||
if (!_fab) return;
|
||||
const visible = isBooksLmVisible();
|
||||
_fab.hidden = visible;
|
||||
const label = visible ? t('fab.close') : t('fab.open');
|
||||
_fab.title = label;
|
||||
_fab.setAttribute('aria-label', label);
|
||||
}
|
||||
|
||||
async function onClick() {
|
||||
const f = getCurrentFile();
|
||||
if (!f) {
|
||||
showToast(t('toast.no_open_file'), 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const sameDir = booksLM._isOpen && booksLM._vault === f.vault && booksLM._directory === f.path;
|
||||
if (sameDir) {
|
||||
// Already open for this directory: re-show a collapsed panel, or close it.
|
||||
const panel = document.querySelector('.bookslm-panel');
|
||||
if (panel && panel.classList.contains('hidden')) {
|
||||
booksLM._toggleSidebar();
|
||||
} else {
|
||||
booksLM.close();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
await booksLM.open(f.vault, f.path);
|
||||
}
|
||||
|
||||
export function initAIFab() {
|
||||
if (_fab) return;
|
||||
|
||||
const btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.id = 'ai-fab';
|
||||
btn.className = 'ai-fab';
|
||||
btn.innerHTML = '<i data-lucide="bot" style="width:24px;height:24px"></i>';
|
||||
document.body.appendChild(btn);
|
||||
_fab = btn;
|
||||
|
||||
btn.addEventListener('click', onClick);
|
||||
document.addEventListener('bookslm:opened', syncVisibility);
|
||||
document.addEventListener('bookslm:closed', syncVisibility);
|
||||
|
||||
if (typeof safeCreateIcons === 'function') safeCreateIcons();
|
||||
syncVisibility();
|
||||
}
|
||||
|
||||
export default initAIFab;
|
||||
+9
-5
@@ -5,6 +5,8 @@
|
||||
import * as UI from './ui.js';
|
||||
import * as Utils from './utils.js';
|
||||
import { initI18n, t } from './i18n.js';
|
||||
import { state } from './state.js';
|
||||
import { initAIFab } from './ai-fab.js';
|
||||
|
||||
// Wire up AI toolbar toast (avoids circular import in utils.js)
|
||||
window._obsigateShowToast = UI.showToast;
|
||||
@@ -99,6 +101,7 @@ async function init() {
|
||||
initMermaid();
|
||||
setupFocusMode();
|
||||
Utils.safeCreateIcons();
|
||||
initAIFab();
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
@@ -111,15 +114,16 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
window.__OBSIGATE_BOOTED = true;
|
||||
if (typeof window.__obsigateBootReady === "function") window.__obsigateBootReady();
|
||||
}
|
||||
// Show version in header
|
||||
// Show version in header — always visible, fallback before the async fetch
|
||||
const versionBadge = document.getElementById('version-badge');
|
||||
if (versionBadge) {
|
||||
versionBadge.textContent = window.__OBSIGATE_VERSION || state.APP_VERSION || 'dev';
|
||||
}
|
||||
fetch('/api/health')
|
||||
.then(r => r.json())
|
||||
.then(d => {
|
||||
const badge = document.getElementById('version-badge');
|
||||
if (badge && d.version) {
|
||||
badge.textContent = d.version;
|
||||
badge.style.display = 'inline-block';
|
||||
}
|
||||
if (badge && d.version) badge.textContent = d.version;
|
||||
})
|
||||
.catch(() => {});
|
||||
});
|
||||
|
||||
@@ -44,14 +44,17 @@ class BooksLM {
|
||||
// Open with animation
|
||||
requestAnimationFrame(() => {
|
||||
this._panel.classList.add('open');
|
||||
let hidden = false;
|
||||
// Apply persisted hidden state (user toggled off in a previous session)
|
||||
try {
|
||||
const savedHidden = localStorage.getItem('obsigate-bookslm-hidden');
|
||||
if (savedHidden === 'true') {
|
||||
this._panel.classList.add('hidden');
|
||||
this._updateToggleIcon();
|
||||
hidden = true;
|
||||
}
|
||||
} catch { /* */ }
|
||||
document.dispatchEvent(new CustomEvent(hidden ? 'bookslm:closed' : 'bookslm:opened', { detail: { vault, directory } }));
|
||||
});
|
||||
this._isOpen = true;
|
||||
|
||||
@@ -79,6 +82,7 @@ class BooksLM {
|
||||
if (!this._panel || !this._isOpen) return;
|
||||
this._panel.classList.remove('open');
|
||||
this._isOpen = false;
|
||||
document.dispatchEvent(new CustomEvent('bookslm:closed'));
|
||||
// Abort any in-flight request
|
||||
if (this._abortCtrl) {
|
||||
this._abortCtrl.abort();
|
||||
@@ -102,6 +106,7 @@ class BooksLM {
|
||||
localStorage.setItem('obsigate-bookslm-hidden', isHidden ? 'true' : 'false');
|
||||
} catch { /* */ }
|
||||
this._updateToggleIcon();
|
||||
document.dispatchEvent(new CustomEvent(isHidden ? 'bookslm:closed' : 'bookslm:opened'));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1599,5 +1599,7 @@
|
||||
"bookslm.context_too_large": "Directory too large — some files were truncated",
|
||||
"bookslm.source": "Source",
|
||||
"palette.bookslm_open": "BooksLM: Open for current directory",
|
||||
"palette.bookslm_new": "BooksLM: New conversation"
|
||||
"palette.bookslm_new": "BooksLM: New conversation",
|
||||
"fab.open": "Open AI assistant",
|
||||
"fab.close": "Close AI assistant"
|
||||
}
|
||||
|
||||
@@ -1599,5 +1599,7 @@
|
||||
"bookslm.context_too_large": "Répertoire trop volumineux — certains fichiers ont été tronqués",
|
||||
"bookslm.source": "Source",
|
||||
"palette.bookslm_open": "BooksLM: Ouvrir pour le répertoire courant",
|
||||
"palette.bookslm_new": "BooksLM: Nouvelle conversation"
|
||||
"palette.bookslm_new": "BooksLM: Nouvelle conversation",
|
||||
"fab.open": "Ouvrir l'assistant AI",
|
||||
"fab.close": "Fermer l'assistant AI"
|
||||
}
|
||||
|
||||
+38
-7
@@ -522,15 +522,16 @@ a:hover {
|
||||
background: color-mix(in srgb, var(--accent) 10%, transparent);
|
||||
}
|
||||
|
||||
/* Version badge in header */
|
||||
/* Version badge in header — always visible */
|
||||
.version-badge {
|
||||
display: none;
|
||||
font-size: 10px;
|
||||
color: var(--text-muted);
|
||||
padding: 2px 8px;
|
||||
display: inline-block;
|
||||
font-size: 11px;
|
||||
color: var(--text-secondary);
|
||||
font-weight: 500;
|
||||
padding: 3px 10px;
|
||||
border-radius: 10px;
|
||||
background: color-mix(in srgb, var(--text-muted) 8%, transparent);
|
||||
border: 1px solid color-mix(in srgb, var(--text-muted) 15%, transparent);
|
||||
background: color-mix(in srgb, var(--text-secondary) 10%, transparent);
|
||||
border: 1px solid color-mix(in srgb, var(--text-secondary) 20%, transparent);
|
||||
font-family: var(--font-mono, monospace);
|
||||
white-space: nowrap;
|
||||
cursor: default;
|
||||
@@ -9077,3 +9078,33 @@ body.popup-mode .content-area {
|
||||
@media (max-width: 768px) {
|
||||
.bookslm-panel { width: 100%; }
|
||||
}
|
||||
|
||||
/* ── AI Floating Action Button ───────────────────────── */
|
||||
.ai-fab {
|
||||
position: fixed;
|
||||
bottom: 24px;
|
||||
right: 24px;
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
border-radius: 50%;
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 99;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
transition: transform 200ms ease, opacity 200ms ease;
|
||||
padding: 0;
|
||||
}
|
||||
.ai-fab:hover {
|
||||
transform: scale(1.08);
|
||||
}
|
||||
.ai-fab:active {
|
||||
transform: scale(0.96);
|
||||
}
|
||||
.ai-fab[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -187,6 +187,25 @@ function testAdminModuleSyntax() {
|
||||
console.log(' ✓ admin.js parses without syntax errors');
|
||||
}
|
||||
|
||||
// ── Test ai-fab.js (FAB — Floating Action Button) ─────────────────────────
|
||||
const AIFAB_PATH = join(JS_DIR, 'ai-fab.js');
|
||||
|
||||
function testAIFabModuleExists() {
|
||||
const content = readFileSync(AIFAB_PATH, 'utf-8');
|
||||
assert.ok(content.length > 0, 'ai-fab.js must be a non-empty file');
|
||||
assert.ok(/from\s+['"]\.\/bookslm\.js['"]/.test(content), 'ai-fab.js must import from ./bookslm.js');
|
||||
assert.ok(/from\s+['"]\.\/state\.js['"]/.test(content), 'ai-fab.js must import from ./state.js');
|
||||
assert.ok(/from\s+['"]\.\/i18n\.js['"]/.test(content), 'ai-fab.js must import from ./i18n.js');
|
||||
console.log(' ✓ ai-fab.js exists and imports the expected modules');
|
||||
}
|
||||
|
||||
function testAIFabModuleExports() {
|
||||
const content = readFileSync(AIFAB_PATH, 'utf-8');
|
||||
const exported = _collectExportedNames(content);
|
||||
assert.ok(exported.has('initAIFab'), 'ai-fab.js must export initAIFab');
|
||||
console.log(' ✓ ai-fab.js exports initAIFab');
|
||||
}
|
||||
|
||||
// ── Run all tests ──────────────────────────────────────────────────────────
|
||||
async function main() {
|
||||
let passed = 0, failed = 0;
|
||||
@@ -199,6 +218,8 @@ async function main() {
|
||||
['admin module exists', testAdminModuleExists],
|
||||
['admin module exports', testAdminModuleExports],
|
||||
['admin module syntax', testAdminModuleSyntax],
|
||||
['ai-fab module exists', testAIFabModuleExists],
|
||||
['ai-fab module exports', testAIFabModuleExports],
|
||||
];
|
||||
|
||||
for (const [name, fn] of tests) {
|
||||
|
||||
Reference in New Issue
Block a user