feat(bookslm): v2.2.0 - BooksLM chat AI contextuel par répertoire + 4 providers AI
CI / lint (push) Failing after 16s
CI / test (push) Skipped
CI / build (push) Skipped
CI / e2e (push) Skipped
CI / security (push) Successful in 21s
Desktop Build / build-windows (push) Canceled after 0s
Desktop Build / build-linux (push) Canceled after 0s
CI / lint (push) Failing after 16s
CI / test (push) Skipped
CI / build (push) Skipped
CI / e2e (push) Skipped
CI / security (push) Successful in 21s
Desktop Build / build-windows (push) Canceled after 0s
Desktop Build / build-linux (push) Canceled after 0s
BooksLM (#76): - Panneau chat slide-in 450px, clic-droit répertoire → 🧠 BooksLM - Collecte récursive .md avec limites (200 fichiers, 200K chars) - Cache SHA-256, redaction secrets, priorité README/index - SSE streaming, badges sources cliquables, historique localStorage - Commandes palette: BooksLM ouvrir/nouvelle conversation Providers AI (4 nouveaux): - NVIDIA (integrate.api.nvidia.com) - QwenCloud (dashscope.aliyuncs.com) - Xiaomi (api.xiaomi.com) - Mistral (api.mistral.ai) - Tous OpenAI-compatible, auto-listing modèles Fix: dropdown config-select suit maintenant le thème (option bg/color) 27 tests BooksLM + 466 tests au total
This commit is contained in:
@@ -2012,6 +2012,90 @@
|
||||
<option value="">-- Modele --</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="config-row">
|
||||
<label
|
||||
class="config-label"
|
||||
for="cfg-nvidia-key"
|
||||
>NVIDIA API Key</label
|
||||
>
|
||||
<input
|
||||
type="password"
|
||||
id="cfg-nvidia-key"
|
||||
class="config-input"
|
||||
placeholder="nvapi-..."
|
||||
autocomplete="off"
|
||||
/>
|
||||
<select
|
||||
id="cfg-nvidia-model"
|
||||
class="config-select"
|
||||
style="width: 200px"
|
||||
>
|
||||
<option value="">-- Modele --</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="config-row">
|
||||
<label
|
||||
class="config-label"
|
||||
for="cfg-qwencloud-key"
|
||||
>QwenCloud API Key</label
|
||||
>
|
||||
<input
|
||||
type="password"
|
||||
id="cfg-qwencloud-key"
|
||||
class="config-input"
|
||||
placeholder="sk-..."
|
||||
autocomplete="off"
|
||||
/>
|
||||
<select
|
||||
id="cfg-qwencloud-model"
|
||||
class="config-select"
|
||||
style="width: 200px"
|
||||
>
|
||||
<option value="">-- Modele --</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="config-row">
|
||||
<label
|
||||
class="config-label"
|
||||
for="cfg-xiaomi-key"
|
||||
>Xiaomi API Key</label
|
||||
>
|
||||
<input
|
||||
type="password"
|
||||
id="cfg-xiaomi-key"
|
||||
class="config-input"
|
||||
placeholder="xm-..."
|
||||
autocomplete="off"
|
||||
/>
|
||||
<select
|
||||
id="cfg-xiaomi-model"
|
||||
class="config-select"
|
||||
style="width: 200px"
|
||||
>
|
||||
<option value="">-- Modele --</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="config-row">
|
||||
<label
|
||||
class="config-label"
|
||||
for="cfg-mistral-key"
|
||||
>Mistral API Key</label
|
||||
>
|
||||
<input
|
||||
type="password"
|
||||
id="cfg-mistral-key"
|
||||
class="config-input"
|
||||
placeholder="sk-..."
|
||||
autocomplete="off"
|
||||
/>
|
||||
<select
|
||||
id="cfg-mistral-model"
|
||||
class="config-select"
|
||||
style="width: 200px"
|
||||
>
|
||||
<option value="">-- Modele --</option>
|
||||
</select>
|
||||
</div>
|
||||
<div
|
||||
class="config-actions-row"
|
||||
style="margin-top: 16px"
|
||||
|
||||
@@ -0,0 +1,414 @@
|
||||
// BooksLM — Directory-scoped AI chat panel (style NotebookLM)
|
||||
import { t } from './i18n.js';
|
||||
|
||||
class BooksLM {
|
||||
constructor() {
|
||||
this._isOpen = false;
|
||||
this._vault = null;
|
||||
this._directory = null;
|
||||
this._messages = [];
|
||||
this._contextFiles = [];
|
||||
this._isLoading = false;
|
||||
this._abortCtrl = null;
|
||||
this._panel = null;
|
||||
this._isFullscreen = false;
|
||||
}
|
||||
|
||||
// ── Public API ──────────────────────────────────────────────────────
|
||||
|
||||
async open(vault, directory) {
|
||||
if (this._isOpen && this._vault === vault && this._directory === directory) {
|
||||
this.close();
|
||||
return;
|
||||
}
|
||||
this._vault = vault;
|
||||
this._directory = directory;
|
||||
this._messages = [];
|
||||
this._contextFiles = [];
|
||||
this._isLoading = true;
|
||||
|
||||
// Restore history
|
||||
this._loadHistory();
|
||||
|
||||
// Build panel if needed
|
||||
if (!this._panel) {
|
||||
this._panel = this._render();
|
||||
document.body.appendChild(this._panel);
|
||||
}
|
||||
this._updateHeader();
|
||||
this._updateStatus();
|
||||
this._renderMessages();
|
||||
|
||||
// Open with animation
|
||||
requestAnimationFrame(() => {
|
||||
this._panel.classList.add('open');
|
||||
});
|
||||
this._isOpen = true;
|
||||
|
||||
// Load context
|
||||
try {
|
||||
const resp = await fetch('/api/ai/bookslm/context', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ vault, directory })
|
||||
});
|
||||
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
||||
const data = await resp.json();
|
||||
this._contextFiles = data.files || [];
|
||||
this._updateStatus(data);
|
||||
this._showSuggestions();
|
||||
} catch (e) {
|
||||
console.warn('BooksLM: context load failed', e);
|
||||
this._contextFiles = [];
|
||||
this._updateStatus({ error: e.message });
|
||||
}
|
||||
this._isLoading = false;
|
||||
}
|
||||
|
||||
close() {
|
||||
if (!this._panel || !this._isOpen) return;
|
||||
this._panel.classList.remove('open');
|
||||
this._isOpen = false;
|
||||
// Abort any in-flight request
|
||||
if (this._abortCtrl) {
|
||||
this._abortCtrl.abort();
|
||||
this._abortCtrl = null;
|
||||
}
|
||||
}
|
||||
|
||||
newConversation() {
|
||||
this._messages = [];
|
||||
this._saveHistory();
|
||||
if (this._panel) {
|
||||
this._renderMessages();
|
||||
this._showSuggestions();
|
||||
}
|
||||
if (!this._isOpen && this._vault && this._directory) {
|
||||
this.open(this._vault, this._directory);
|
||||
}
|
||||
}
|
||||
|
||||
exportConversation() {
|
||||
if (!this._messages.length) return;
|
||||
let md = `# BooksLM — ${this._directory}\n\n`;
|
||||
for (const msg of this._messages) {
|
||||
const role = msg.role === 'user' ? '**You**' : '**AI**';
|
||||
md += `### ${role}\n\n${msg.content}\n\n---\n\n`;
|
||||
}
|
||||
const blob = new Blob([md], { type: 'text/markdown' });
|
||||
const a = document.createElement('a');
|
||||
a.href = URL.createObjectURL(blob);
|
||||
a.download = `bookslm-${this._directory.replace(/\//g, '_')}.md`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(a.href);
|
||||
}
|
||||
|
||||
openFile(path) {
|
||||
window.dispatchEvent(new CustomEvent('obsigate:open-file', {
|
||||
detail: { vault: this._vault, path }
|
||||
}));
|
||||
}
|
||||
|
||||
// ── Rendering ───────────────────────────────────────────────────────
|
||||
|
||||
_render() {
|
||||
const panel = document.createElement('div');
|
||||
panel.className = 'bookslm-panel';
|
||||
panel.innerHTML = `
|
||||
<div class="bookslm-header">
|
||||
<span>📚</span>
|
||||
<span class="bookslm-title"></span>
|
||||
<button class="bookslm-btn-new" title="${t('bookslm.new_conversation')}">✨</button>
|
||||
<button class="bookslm-btn-export" title="${t('bookslm.export')}">📥</button>
|
||||
<button class="bookslm-btn-fullscreen" title="⛶">⛶</button>
|
||||
<button class="bookslm-btn-close" title="✕">✕</button>
|
||||
</div>
|
||||
<div class="bookslm-status"></div>
|
||||
<div class="bookslm-suggestions"></div>
|
||||
<div class="bookslm-messages"></div>
|
||||
<div class="bookslm-input-area">
|
||||
<textarea placeholder="${t('bookslm.placeholder')}" rows="1"></textarea>
|
||||
<button class="bookslm-btn-send">${t('bookslm.send')}</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Wire events
|
||||
panel.querySelector('.bookslm-btn-close').addEventListener('click', () => this.close());
|
||||
panel.querySelector('.bookslm-btn-new').addEventListener('click', () => this.newConversation());
|
||||
panel.querySelector('.bookslm-btn-export').addEventListener('click', () => this.exportConversation());
|
||||
panel.querySelector('.bookslm-btn-fullscreen').addEventListener('click', () => {
|
||||
this._isFullscreen = !this._isFullscreen;
|
||||
panel.classList.toggle('fullscreen', this._isFullscreen);
|
||||
});
|
||||
|
||||
// Send button
|
||||
panel.querySelector('.bookslm-btn-send').addEventListener('click', () => this._sendMessage());
|
||||
|
||||
// Textarea auto-grow + Ctrl+Enter
|
||||
const textarea = panel.querySelector('textarea');
|
||||
textarea.addEventListener('input', () => {
|
||||
textarea.style.height = 'auto';
|
||||
textarea.style.height = Math.min(textarea.scrollHeight, 120) + 'px';
|
||||
});
|
||||
textarea.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
|
||||
e.preventDefault();
|
||||
this._sendMessage();
|
||||
}
|
||||
});
|
||||
|
||||
return panel;
|
||||
}
|
||||
|
||||
_updateHeader() {
|
||||
if (!this._panel) return;
|
||||
const title = this._panel.querySelector('.bookslm-title');
|
||||
if (title) {
|
||||
title.textContent = this._directory
|
||||
? `📚 ${this._directory.split('/').pop() || this._vault}`
|
||||
: t('bookslm.title');
|
||||
}
|
||||
}
|
||||
|
||||
_updateStatus(data) {
|
||||
if (!this._panel) return;
|
||||
const status = this._panel.querySelector('.bookslm-status');
|
||||
if (!status) return;
|
||||
|
||||
if (data && data.error) {
|
||||
status.innerHTML = `<span style="color:#f87171">⚠ ${t('bookslm.no_context')}</span>`;
|
||||
return;
|
||||
}
|
||||
if (data && data.files) {
|
||||
const count = data.files.length;
|
||||
const chars = data.total_chars || 0;
|
||||
const pct = Math.min(100, Math.round((chars / 100000) * 100));
|
||||
status.innerHTML = `
|
||||
<span>${t('bookslm.files_indexed', { count })}, ${t('bookslm.chars_loaded', { chars: Math.round(chars / 1000) + 'K' })}</span>
|
||||
<div class="bookslm-context-bar"><div class="bookslm-context-fill" style="width:${pct}%"></div></div>
|
||||
`;
|
||||
} else if (this._isLoading) {
|
||||
status.textContent = '⏳ ...';
|
||||
}
|
||||
}
|
||||
|
||||
_showSuggestions() {
|
||||
if (!this._panel) return;
|
||||
const sugEl = this._panel.querySelector('.bookslm-suggestions');
|
||||
if (!sugEl) return;
|
||||
sugEl.innerHTML = '';
|
||||
|
||||
if (!this._contextFiles.length && !this._isLoading) return;
|
||||
|
||||
const suggestions = [
|
||||
t('bookslm.suggestion_summary'),
|
||||
t('bookslm.suggestion_themes'),
|
||||
t('bookslm.suggestion_contradictions')
|
||||
];
|
||||
for (const s of suggestions) {
|
||||
const btn = document.createElement('button');
|
||||
btn.className = 'bookslm-suggestion';
|
||||
btn.textContent = s;
|
||||
btn.addEventListener('click', () => {
|
||||
const textarea = this._panel.querySelector('textarea');
|
||||
if (textarea) {
|
||||
textarea.value = s;
|
||||
this._sendMessage();
|
||||
}
|
||||
});
|
||||
sugEl.appendChild(btn);
|
||||
}
|
||||
}
|
||||
|
||||
_renderMessages() {
|
||||
if (!this._panel) return;
|
||||
const container = this._panel.querySelector('.bookslm-messages');
|
||||
if (!container) return;
|
||||
container.innerHTML = '';
|
||||
|
||||
for (const msg of this._messages) {
|
||||
const bubble = document.createElement('div');
|
||||
bubble.className = `bookslm-bubble ${msg.role}`;
|
||||
|
||||
if (msg.role === 'assistant') {
|
||||
bubble.innerHTML = this._renderMarkdown(msg.content || '');
|
||||
// Source badges
|
||||
if (msg.sources && msg.sources.length) {
|
||||
const sourcesDiv = document.createElement('div');
|
||||
sourcesDiv.className = 'bookslm-sources';
|
||||
for (const src of msg.sources) {
|
||||
const badge = document.createElement('span');
|
||||
badge.className = 'bookslm-source-badge';
|
||||
badge.textContent = `📄 ${src.split('/').pop()}`;
|
||||
badge.title = src;
|
||||
badge.addEventListener('click', () => this.openFile(src));
|
||||
sourcesDiv.appendChild(badge);
|
||||
}
|
||||
bubble.appendChild(sourcesDiv);
|
||||
}
|
||||
} else {
|
||||
bubble.textContent = msg.content;
|
||||
}
|
||||
|
||||
container.appendChild(bubble);
|
||||
}
|
||||
|
||||
// Scroll to bottom
|
||||
container.scrollTop = container.scrollHeight;
|
||||
}
|
||||
|
||||
// ── Messaging ───────────────────────────────────────────────────────
|
||||
|
||||
async _sendMessage() {
|
||||
const textarea = this._panel.querySelector('textarea');
|
||||
const text = (textarea.value || '').trim();
|
||||
if (!text || this._isLoading) return;
|
||||
|
||||
textarea.value = '';
|
||||
textarea.style.height = 'auto';
|
||||
|
||||
// Hide suggestions
|
||||
const sugEl = this._panel.querySelector('.bookslm-suggestions');
|
||||
if (sugEl) sugEl.innerHTML = '';
|
||||
|
||||
// Add user message
|
||||
this._messages.push({ role: 'user', content: text });
|
||||
this._renderMessages();
|
||||
|
||||
// Add assistant placeholder
|
||||
const assistantMsg = { role: 'assistant', content: '', sources: [] };
|
||||
this._messages.push(assistantMsg);
|
||||
this._renderMessages();
|
||||
this._isLoading = true;
|
||||
|
||||
// Disable send button
|
||||
const sendBtn = this._panel.querySelector('.bookslm-btn-send');
|
||||
if (sendBtn) sendBtn.disabled = true;
|
||||
|
||||
this._abortCtrl = new AbortController();
|
||||
|
||||
try {
|
||||
const resp = await fetch('/api/ai/bookslm/chat', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
vault: this._vault,
|
||||
directory: this._directory,
|
||||
messages: this._messages.slice(0, -1), // exclude empty assistant
|
||||
context_files: this._contextFiles.map(f => f.path || f)
|
||||
}),
|
||||
signal: this._abortCtrl.signal
|
||||
});
|
||||
|
||||
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
||||
|
||||
const reader = resp.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = '';
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
const lines = buffer.split('\n');
|
||||
buffer = lines.pop() || '';
|
||||
|
||||
for (const line of lines) {
|
||||
if (!line.startsWith('data: ')) continue;
|
||||
const payload = line.slice(6);
|
||||
if (payload === '[DONE]') continue;
|
||||
try {
|
||||
const data = JSON.parse(payload);
|
||||
if (data.content) {
|
||||
assistantMsg.content += data.content;
|
||||
}
|
||||
if (data.sources) {
|
||||
assistantMsg.sources = data.sources;
|
||||
}
|
||||
} catch {
|
||||
// Raw text token
|
||||
assistantMsg.content += payload;
|
||||
}
|
||||
this._renderMessages();
|
||||
}
|
||||
}
|
||||
|
||||
// Extract sources from context files if mentioned
|
||||
if (!assistantMsg.sources.length) {
|
||||
const contentLower = assistantMsg.content.toLowerCase();
|
||||
assistantMsg.sources = this._contextFiles
|
||||
.filter(f => {
|
||||
const name = (f.path || f || '').split('/').pop().toLowerCase();
|
||||
return name && contentLower.includes(name);
|
||||
})
|
||||
.map(f => f.path || f);
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
if (e.name !== 'AbortError') {
|
||||
assistantMsg.content = `⚠ Error: ${e.message}`;
|
||||
console.warn('BooksLM chat error:', e);
|
||||
}
|
||||
}
|
||||
|
||||
this._isLoading = false;
|
||||
if (sendBtn) sendBtn.disabled = false;
|
||||
this._abortCtrl = null;
|
||||
this._renderMessages();
|
||||
this._saveHistory();
|
||||
}
|
||||
|
||||
// ── Markdown (lightweight) ──────────────────────────────────────────
|
||||
|
||||
_renderMarkdown(text) {
|
||||
if (!text) return '';
|
||||
let html = text
|
||||
// Code blocks
|
||||
.replace(/```(\w*)\n([\s\S]*?)```/g, '<pre><code>$2</code></pre>')
|
||||
// Inline code
|
||||
.replace(/`([^`]+)`/g, '<code>$1</code>')
|
||||
// Bold
|
||||
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
||||
// Italic
|
||||
.replace(/\*(.+?)\*/g, '<em>$1</em>')
|
||||
// Links
|
||||
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener">$1</a>')
|
||||
// Unordered lists
|
||||
.replace(/^[*\-+] (.+)$/gm, '<li>$1</li>')
|
||||
// Headers
|
||||
.replace(/^### (.+)$/gm, '<h4>$1</h4>')
|
||||
.replace(/^## (.+)$/gm, '<h3>$1</h3>')
|
||||
// Paragraphs
|
||||
.replace(/\n\n/g, '</p><p>')
|
||||
.replace(/\n/g, '<br>');
|
||||
return `<p>${html}</p>`;
|
||||
}
|
||||
|
||||
// ── History persistence ─────────────────────────────────────────────
|
||||
|
||||
_historyKey() {
|
||||
return `bookslm-history-${this._vault}-${this._directory}`;
|
||||
}
|
||||
|
||||
_saveHistory() {
|
||||
try {
|
||||
localStorage.setItem(this._historyKey(), JSON.stringify(this._messages));
|
||||
} catch {}
|
||||
}
|
||||
|
||||
_loadHistory() {
|
||||
try {
|
||||
const data = localStorage.getItem(this._historyKey());
|
||||
if (data) {
|
||||
this._messages = JSON.parse(data);
|
||||
}
|
||||
} catch {
|
||||
this._messages = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const booksLM = new BooksLM();
|
||||
export default booksLM;
|
||||
export { BooksLM };
|
||||
@@ -1343,7 +1343,7 @@ function updateRegexPreview() {
|
||||
async function loadAIKeys() {
|
||||
try {
|
||||
const data = await api("/api/config/ai-keys");
|
||||
["DEEPSEEK_API_KEY","OPENROUTER_API_KEY","GEMINI_API_KEY"].forEach(k => {
|
||||
["DEEPSEEK_API_KEY","OPENROUTER_API_KEY","GEMINI_API_KEY","NVIDIA_API_KEY","QWENCLOUD_API_KEY","XIAOMI_API_KEY","MISTRAL_API_KEY"].forEach(k => {
|
||||
const id = "cfg-" + k.toLowerCase().replace(/_api_key/g, "") + "-key";
|
||||
const input = document.getElementById(id);
|
||||
if (input && data[k]) input.placeholder = data[k];
|
||||
@@ -1353,7 +1353,7 @@ async function loadAIKeys() {
|
||||
|
||||
async function saveAIKeys() {
|
||||
const keys = {};
|
||||
const map = { "cfg-deepseek-key": "DEEPSEEK_API_KEY", "cfg-openrouter-key": "OPENROUTER_API_KEY", "cfg-gemini-key": "GEMINI_API_KEY" };
|
||||
const map = { "cfg-deepseek-key": "DEEPSEEK_API_KEY", "cfg-openrouter-key": "OPENROUTER_API_KEY", "cfg-gemini-key": "GEMINI_API_KEY", "cfg-nvidia-key": "NVIDIA_API_KEY", "cfg-qwencloud-key": "QWENCLOUD_API_KEY", "cfg-xiaomi-key": "XIAOMI_API_KEY", "cfg-mistral-key": "MISTRAL_API_KEY" };
|
||||
for (const [id, name] of Object.entries(map)) {
|
||||
const input = document.getElementById(id);
|
||||
if (input && input.value.trim()) keys[name] = input.value.trim();
|
||||
@@ -1377,7 +1377,7 @@ async function testAIKeys() {
|
||||
var msg = Object.entries(results).map(([k,v]) => k + ": " + v).join(" | ");
|
||||
if (status) status.textContent = msg;
|
||||
// Fetch models for configured providers
|
||||
for (const p of ["deepseek","openrouter","gemini"]) {
|
||||
for (const p of ["deepseek","openrouter","gemini","nvidia","qwencloud","xiaomi","mistral"]) {
|
||||
if (results[p] === "ok") {
|
||||
try {
|
||||
const m = await api("/api/config/ai-models?provider=" + p);
|
||||
|
||||
@@ -58,6 +58,9 @@ function getCMDS() {
|
||||
{ id:'focus-next', label:'→ Panneau suivant', desc:'Active le panneau suivant (Ctrl+Alt+→)', cat:'Panneaux', act:()=>{close();if(window.PaneManager&&window.PaneManager.isSplit()){const n=(window.PaneManager.activePaneId+1)%window.PaneManager.panes.length;window.PaneManager.setActivePane(n);}} },
|
||||
{ id:'focus-prev', label:'← Panneau précédent', desc:'Active le panneau précédent (Ctrl+Alt+←)', cat:'Panneaux', act:()=>{close();if(window.PaneManager&&window.PaneManager.isSplit()){const n=(window.PaneManager.activePaneId-1+window.PaneManager.panes.length)%window.PaneManager.panes.length;window.PaneManager.setActivePane(n);}} },
|
||||
{ id:'reset-panes', label:'🔄 Réinitialiser les panneaux', desc:'Ferme tous les panneaux et revient au mode single-pane', cat:'Panneaux', act:()=>{close();if(window.PaneManager){while(window.PaneManager.isSplit()){window.PaneManager.closePane(window.PaneManager.panes.length-1);}localStorage.removeItem('obsigate-panes');}} },
|
||||
// BooksLM commands
|
||||
{ id:'bookslm-open', label:t('palette.bookslm_open'), desc:'Open BooksLM AI chat for current directory', cat:'AI', act:async()=>{close();const f=getCurrentFile();if(f){const m=await import('./bookslm.js');m.default.open(f.vault,f.path);}else{showToast(t('toast.no_open_file'),'error');}} },
|
||||
{ id:'bookslm-new', label:t('palette.bookslm_new'), desc:'Reset BooksLM conversation and open fresh', cat:'AI', act:async()=>{close();const f=getCurrentFile();if(f){const m=await import('./bookslm.js');m.default.newConversation();m.default.open(f.vault,f.path);}else{showToast(t('toast.no_open_file'),'error');}} },
|
||||
];
|
||||
return _cmdsCache;
|
||||
}
|
||||
|
||||
@@ -1698,6 +1698,8 @@ export const ContextMenuManager = {
|
||||
this._addSeparator();
|
||||
this._addItem('bookmark-plus', 'Ajouter aux recherches sauvegardees', () => this._saveDirectorySearch(), false);
|
||||
this._addSeparator();
|
||||
this._addItem('brain', '🧠 BooksLM', () => { import('./bookslm.js').then(m => m.default.open(this._targetVault, this._targetPath)); }, false);
|
||||
this._addSeparator();
|
||||
this._addItem('edit', 'Renommer', () => this._renameItem(), isReadonly);
|
||||
this._addItem('trash-2', 'Supprimer', () => this._deleteDirectory(), isReadonly);
|
||||
} else if (type === 'file') {
|
||||
|
||||
@@ -1546,5 +1546,23 @@
|
||||
"mfa.totp_code_label": "TOTP Code",
|
||||
"mfa.disable_confirm_btn": "Disable 2FA",
|
||||
"mfa.disabled_success": "2FA has been disabled.",
|
||||
"mfa.fill_all_fields": "Please fill in all fields."
|
||||
"mfa.fill_all_fields": "Please fill in all fields.",
|
||||
|
||||
"bookslm.title": "BooksLM",
|
||||
"bookslm.files_indexed": "{count} files indexed",
|
||||
"bookslm.chars_loaded": "{chars} chars loaded",
|
||||
"bookslm.placeholder": "Ask a question about these documents...",
|
||||
"bookslm.send": "Send",
|
||||
"bookslm.new_conversation": "New conversation",
|
||||
"bookslm.export": "Export conversation",
|
||||
"bookslm.copy": "Copy",
|
||||
"bookslm.regenerate": "Regenerate",
|
||||
"bookslm.suggestion_summary": "Summarize this directory",
|
||||
"bookslm.suggestion_themes": "What are the main themes?",
|
||||
"bookslm.suggestion_contradictions": "Are there contradictions between these documents?",
|
||||
"bookslm.no_context": "No files found in this directory",
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -1546,5 +1546,23 @@
|
||||
"mfa.totp_code_label": "Code TOTP",
|
||||
"mfa.disable_confirm_btn": "Désactiver la 2FA",
|
||||
"mfa.disabled_success": "La 2FA a été désactivée.",
|
||||
"mfa.fill_all_fields": "Veuillez remplir tous les champs."
|
||||
"mfa.fill_all_fields": "Veuillez remplir tous les champs.",
|
||||
|
||||
"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.send": "Envoyer",
|
||||
"bookslm.new_conversation": "Nouvelle conversation",
|
||||
"bookslm.export": "Exporter la conversation",
|
||||
"bookslm.copy": "Copier",
|
||||
"bookslm.regenerate": "Régénérer",
|
||||
"bookslm.suggestion_summary": "Résume ce répertoire",
|
||||
"bookslm.suggestion_themes": "Quels sont les thèmes principaux ?",
|
||||
"bookslm.suggestion_contradictions": "Y a-t-il des contradictions entre ces documents ?",
|
||||
"bookslm.no_context": "Aucun fichier trouvé dans ce répertoire",
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -3833,6 +3833,12 @@ body.resizing-v {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.config-select option {
|
||||
background: var(--bg-input, #1a1a2e);
|
||||
color: var(--text-primary, #e0e0e0);
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
.config-btn-add {
|
||||
padding: 8px 16px;
|
||||
border: 1px solid var(--accent);
|
||||
@@ -9006,3 +9012,50 @@ body.popup-mode .content-area {
|
||||
color: #f59e0b; font-size: 13px; padding: 8px 12px;
|
||||
background: #3a2a1a; border-radius: 6px; margin-top: 8px;
|
||||
}
|
||||
|
||||
/* ── BooksLM Panel ──────────────────────────────────── */
|
||||
.bookslm-panel { position: fixed; right: 0; top: 0; bottom: 0; width: 450px;
|
||||
background: var(--bg, #0d0d1a); border-left: 1px solid var(--border, #333);
|
||||
z-index: 100; display: flex; flex-direction: column;
|
||||
transform: translateX(100%); transition: transform 300ms ease-out;
|
||||
box-shadow: -4px 0 20px rgba(0,0,0,0.3); }
|
||||
.bookslm-panel.open { transform: translateX(0); }
|
||||
.bookslm-header { display: flex; align-items: center; gap: 8px;
|
||||
padding: 12px 16px; border-bottom: 1px solid var(--border, #333);
|
||||
font-size: 14px; font-weight: 600; }
|
||||
.bookslm-header .bookslm-title { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.bookslm-header button { background: none; border: none; cursor: pointer;
|
||||
color: var(--muted, #999); font-size: 16px; padding: 4px 8px; border-radius: 4px; }
|
||||
.bookslm-header button:hover { background: var(--surface2, #1e1e3a); color: var(--text, #fff); }
|
||||
.bookslm-status { padding: 6px 16px; font-size: 12px; color: var(--muted, #999);
|
||||
border-bottom: 1px solid var(--border, #333); display: flex; gap: 12px; align-items: center; }
|
||||
.bookslm-status .bookslm-context-bar { flex: 1; height: 4px; border-radius: 2px;
|
||||
background: var(--surface2, #1e1e3a); overflow: hidden; }
|
||||
.bookslm-status .bookslm-context-fill { height: 100%; border-radius: 2px; background: var(--accent, #7C3AED); transition: width 0.3s; }
|
||||
.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, #7C3AED); color: #fff; border-bottom-right-radius: 4px; }
|
||||
.bookslm-bubble.assistant { align-self: flex-start; background: var(--surface2, #1e1e3a); color: var(--text, #fff); border-bottom-left-radius: 4px; }
|
||||
.bookslm-bubble.assistant code { background: rgba(0,0,0,0.2); padding: 1px 4px; border-radius: 3px; font-size: 0.9em; }
|
||||
.bookslm-bubble.assistant pre { background: rgba(0,0,0,0.3); padding: 10px; border-radius: 6px; overflow-x: auto; margin: 8px 0; }
|
||||
.bookslm-sources { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 8px; }
|
||||
.bookslm-source-badge { display: inline-flex; align-items: center; gap: 4px; padding: 2px 8px;
|
||||
background: var(--surface, #16162e); border: 1px solid var(--border, #333); border-radius: 12px;
|
||||
font-size: 11px; color: var(--accent, #7C3AED); cursor: pointer; }
|
||||
.bookslm-source-badge:hover { background: var(--surface2, #1e1e3a); }
|
||||
.bookslm-input-area { display: flex; gap: 8px; padding: 12px 16px; border-top: 1px solid var(--border, #333); align-items: flex-end; }
|
||||
.bookslm-input-area textarea { flex: 1; resize: none; min-height: 36px; max-height: 120px;
|
||||
padding: 8px 12px; border-radius: 8px; border: 1px solid var(--border, #333);
|
||||
background: var(--bg, #0d0d1a); color: var(--text, #fff); font-size: 14px; font-family: inherit; }
|
||||
.bookslm-input-area textarea:focus { border-color: var(--accent, #7C3AED); outline: none; }
|
||||
.bookslm-input-area button { padding: 8px 16px; border-radius: 8px; border: none;
|
||||
background: var(--accent, #7C3AED); color: #fff; cursor: pointer; font-size: 14px; }
|
||||
.bookslm-input-area button:hover { opacity: 0.9; }
|
||||
.bookslm-input-area button:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
.bookslm-suggestions { display: flex; flex-direction: column; gap: 6px; padding: 8px 16px 0; }
|
||||
.bookslm-suggestion { padding: 8px 12px; border-radius: 8px; border: 1px solid var(--border, #333);
|
||||
background: var(--surface, #16162e); color: var(--muted, #999); cursor: pointer; font-size: 13px; text-align: left; }
|
||||
.bookslm-suggestion:hover { background: var(--surface2, #1e1e3a); color: var(--text, #fff); border-color: var(--accent, #7C3AED); }
|
||||
@media (max-width: 768px) {
|
||||
.bookslm-panel { width: 100%; }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user