mirror of
https://github.com/lllin000/PaperForge.git
synced 2026-07-22 06:50:53 +00:00
feat(plugin): tabbed settings UI (Installation + Features) with skill manager and feature toggles
This commit is contained in:
parent
9a48784b90
commit
c4dd09613a
1 changed files with 214 additions and 0 deletions
|
|
@ -489,6 +489,10 @@ Object.assign(LANG.en, {
|
|||
ocr_privacy_title: 'OCR Privacy Notice',
|
||||
ocr_privacy_warning: 'OCR will upload PDFs to the PaddleOCR API. Do not upload sensitive or confidential documents.',
|
||||
ocr_understand: 'I understand, continue',
|
||||
|
||||
/* ── Tabbed Settings ── */
|
||||
tab_setup: 'Installation',
|
||||
tab_features: 'Features',
|
||||
});
|
||||
|
||||
/* ── LANG.zh: v1.12 runtime health, OCR queue, pf-deep, dashboard translations ── */
|
||||
|
|
@ -524,6 +528,10 @@ Object.assign(LANG.zh, {
|
|||
install_validating: '正在校验安装环境…',
|
||||
install_bootstrapping: '未检测到 PaperForge Python 包,正在自动安装…',
|
||||
wizard_safety: '安全说明:如果你选择的目录里已经有文件,安装向导会保留已有内容,只补充缺失的 PaperForge 文件和目录。',
|
||||
|
||||
/* ── Tabbed Settings ── */
|
||||
tab_setup: '安装',
|
||||
tab_features: '功能',
|
||||
});
|
||||
|
||||
function langFromApp(app) {
|
||||
|
|
@ -553,6 +561,17 @@ const DEFAULT_SETTINGS = {
|
|||
paddleocr_api_key: '',
|
||||
zotero_data_dir: '',
|
||||
python_path: '',
|
||||
// Feature toggles
|
||||
features: {
|
||||
fts_search: true,
|
||||
agent_context: true,
|
||||
reading_log: true,
|
||||
vector_db: false,
|
||||
},
|
||||
vector_db_mode: 'local',
|
||||
vector_db_model: 'BAAI/bge-small-en-v1.5',
|
||||
vector_db_api_key: '',
|
||||
frozen_skills: {},
|
||||
};
|
||||
|
||||
// ACTIONS, resolvePythonExecutable extracted to src/ modules (Plan 53-001)
|
||||
|
|
@ -2196,6 +2215,7 @@ class PaperForgeSettingTab extends PluginSettingTab {
|
|||
this.plugin = plugin;
|
||||
this._saveTimeout = null;
|
||||
this._pfConfig = null; // cached paperforge.json config
|
||||
this.activeTab = 'setup';
|
||||
}
|
||||
|
||||
/** Reload path config from paperforge.json */
|
||||
|
|
@ -2208,6 +2228,55 @@ class PaperForgeSettingTab extends PluginSettingTab {
|
|||
containerEl.empty();
|
||||
this._refreshPfConfig();
|
||||
|
||||
// Inject tab CSS once
|
||||
if (!document.getElementById('paperforge-tab-styles')) {
|
||||
const style = document.createElement('style');
|
||||
style.id = 'paperforge-tab-styles';
|
||||
style.textContent = `
|
||||
.paperforge-settings-tabs { display: flex; gap: 4px; margin-bottom: 16px; border-bottom: 1px solid var(--background-modifier-border); }
|
||||
.paperforge-settings-tab { padding: 6px 16px; border: none; background: none; cursor: pointer; border-bottom: 2px solid transparent; font-size: 14px; color: var(--text-muted); }
|
||||
.paperforge-settings-tab--active { color: var(--text-accent); border-bottom-color: var(--text-accent); }
|
||||
.paperforge-tab-content { display: none; }
|
||||
.paperforge-tab-content--active { display: block; }
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
// --- Tab bar ---
|
||||
const tabBar = containerEl.createDiv({ cls: 'paperforge-settings-tabs' });
|
||||
const tabs = [
|
||||
{ id: 'setup', label: t('tab_setup') || 'Installation' },
|
||||
{ id: 'features', label: t('tab_features') || 'Features' },
|
||||
];
|
||||
const tabContents = {};
|
||||
|
||||
tabs.forEach(tab => {
|
||||
const btn = tabBar.createEl('button', {
|
||||
cls: 'paperforge-settings-tab' + (tab.id === this.activeTab ? ' paperforge-settings-tab--active' : ''),
|
||||
text: tab.label,
|
||||
});
|
||||
btn.addEventListener('click', () => {
|
||||
this.activeTab = tab.id;
|
||||
this.display(); // re-render with new active tab
|
||||
});
|
||||
});
|
||||
|
||||
// --- Tab content containers ---
|
||||
tabs.forEach(tab => {
|
||||
tabContents[tab.id] = containerEl.createDiv({
|
||||
cls: 'paperforge-tab-content' + (tab.id === this.activeTab ? ' paperforge-tab-content--active' : ''),
|
||||
});
|
||||
});
|
||||
|
||||
// --- Render active tab ---
|
||||
if (this.activeTab === 'setup') {
|
||||
this._renderSetupTab(tabContents.setup);
|
||||
} else {
|
||||
this._renderFeaturesTab(tabContents.features);
|
||||
}
|
||||
}
|
||||
|
||||
_renderSetupTab(containerEl) {
|
||||
const vaultPath = this.app.vault.adapter.basePath;
|
||||
if (!this.plugin.settings.vault_path) {
|
||||
this.plugin.settings.vault_path = vaultPath;
|
||||
|
|
@ -2413,6 +2482,146 @@ class PaperForgeSettingTab extends PluginSettingTab {
|
|||
}
|
||||
}
|
||||
|
||||
_renderFeaturesTab(containerEl) {
|
||||
// --- Section: Skills ---
|
||||
containerEl.createEl('h3', { text: 'Skills' });
|
||||
containerEl.createEl('p', { text: 'Agent skills extend PaperForge with specialized capabilities.', cls: 'setting-item-description' });
|
||||
|
||||
// Scan vault-local agent skill dirs
|
||||
const agentDirs = [
|
||||
'.opencode/skills', '.claude/skills', '.codex/skills',
|
||||
'.cursor/skills', '.windsurf/skills', '.github/skills',
|
||||
];
|
||||
const vaultPath = this.app.vault.adapter.basePath;
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
let foundSkills = false;
|
||||
|
||||
agentDirs.forEach(dir => {
|
||||
const skillDir = path.join(vaultPath, dir);
|
||||
if (!fs.existsSync(skillDir)) return;
|
||||
fs.readdirSync(skillDir, { withFileTypes: true }).forEach(entry => {
|
||||
if (!entry.isDirectory()) return;
|
||||
const skillFile = path.join(skillDir, entry.name, 'SKILL.md');
|
||||
if (!fs.existsSync(skillFile)) return;
|
||||
const content = fs.readFileSync(skillFile, 'utf-8');
|
||||
const nameMatch = content.match(/^name:\s*(.+)$/m);
|
||||
const descMatch = content.match(/^description:\s*(.+)$/m);
|
||||
const sourceMatch = content.match(/^source:\s*(.+)$/m);
|
||||
const disableMatch = content.match(/^disable-model-invocation:\s*(.+)$/m);
|
||||
const versionMatch = content.match(/^version:\s*(.+)$/m);
|
||||
|
||||
const name = nameMatch ? nameMatch[1].trim() : entry.name;
|
||||
const desc = descMatch ? descMatch[1].trim() : '';
|
||||
const source = sourceMatch ? sourceMatch[1].trim() : 'user';
|
||||
const disabled = disableMatch && disableMatch[1].trim() === 'true';
|
||||
const version = versionMatch ? versionMatch[1].trim() : '';
|
||||
const isSystem = source === 'paperforge';
|
||||
foundSkills = true;
|
||||
|
||||
const setting = new Setting(containerEl)
|
||||
.setName(name + (isSystem ? ' (system)' : ' (user)') + (version ? ' v' + version : ''))
|
||||
.setDesc(desc || 'No description');
|
||||
|
||||
setting.addToggle(toggle => {
|
||||
toggle.setValue(!disabled)
|
||||
.onChange(async (value) => {
|
||||
const newContent = disableMatch
|
||||
? content.replace(/^disable-model-invocation:\s*.+$/m, `disable-model-invocation: ${!value}`)
|
||||
: content.replace(/^(---\r?\n)/, `$1disable-model-invocation: ${!value}\n`);
|
||||
fs.writeFileSync(skillFile, newContent, 'utf-8');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
if (!foundSkills) {
|
||||
containerEl.createEl('p', { text: 'No skills found. Run setup to deploy skills.', cls: 'setting-item-description' });
|
||||
}
|
||||
|
||||
// --- Section: Memory Layer Features ---
|
||||
containerEl.createEl('h3', { text: 'Memory Layer' });
|
||||
const featureSettings = [
|
||||
{ key: 'fts_search', name: 'Full-text Search', desc: 'Enables paperforge search command (FTS5)' },
|
||||
{ key: 'agent_context', name: 'Agent Context', desc: 'Enables paperforge agent-context command' },
|
||||
{ key: 'reading_log', name: 'Reading Log', desc: 'Enables paper_events table for reading notes' },
|
||||
];
|
||||
featureSettings.forEach(fs => {
|
||||
new Setting(containerEl)
|
||||
.setName(fs.name)
|
||||
.setDesc(fs.desc)
|
||||
.addToggle(toggle => {
|
||||
toggle.setValue(this.plugin.settings.features[fs.key])
|
||||
.onChange(value => {
|
||||
this.plugin.settings.features[fs.key] = value;
|
||||
this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// --- Section: Vector Database ---
|
||||
containerEl.createEl('h3', { text: 'Vector Database' });
|
||||
new Setting(containerEl)
|
||||
.setName('Enable Vector Retrieval')
|
||||
.setDesc('Semantic search across OCR fulltext using embeddings. Requires pip install chromadb sentence-transformers.')
|
||||
.addToggle(toggle => {
|
||||
toggle.setValue(this.plugin.settings.features.vector_db)
|
||||
.onChange(value => {
|
||||
this.plugin.settings.features.vector_db = value;
|
||||
this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
|
||||
if (this.plugin.settings.features.vector_db) {
|
||||
// Mode selection
|
||||
new Setting(containerEl)
|
||||
.setName('Embedding Mode')
|
||||
.setDesc('Local: free, offline, CPU-based. API: higher quality, requires API key.')
|
||||
.addDropdown(dropdown => {
|
||||
dropdown.addOption('local', 'Local (bge-small-en-v1.5)');
|
||||
dropdown.addOption('api', 'API (text-embedding-3-small)');
|
||||
dropdown.setValue(this.plugin.settings.vector_db_mode)
|
||||
.onChange(value => {
|
||||
this.plugin.settings.vector_db_mode = value;
|
||||
this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
|
||||
// Model selection (local mode only)
|
||||
if (this.plugin.settings.vector_db_mode === 'local') {
|
||||
new Setting(containerEl)
|
||||
.setName('Local Model')
|
||||
.setDesc('Embedding model for vector search. First use downloads ~130MB.')
|
||||
.addDropdown(dropdown => {
|
||||
dropdown.addOption('BAAI/bge-small-en-v1.5', 'bge-small-en-v1.5 (384d, 130MB, fast)');
|
||||
dropdown.addOption('sentence-transformers/all-MiniLM-L6-v2', 'all-MiniLM-L6-v2 (384d, 80MB, fastest)');
|
||||
dropdown.addOption('BAAI/bge-base-en-v1.5', 'bge-base-en-v1.5 (768d, 440MB)');
|
||||
dropdown.addOption('sentence-transformers/all-mpnet-base-v2', 'all-mpnet-base-v2 (768d, 420MB)');
|
||||
dropdown.setValue(this.plugin.settings.vector_db_model)
|
||||
.onChange(value => {
|
||||
this.plugin.settings.vector_db_model = value;
|
||||
this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// API key (API mode only)
|
||||
if (this.plugin.settings.vector_db_mode === 'api') {
|
||||
new Setting(containerEl)
|
||||
.setName('API Key')
|
||||
.setDesc('OpenAI API key for text-embedding-3-small')
|
||||
.addText(text => {
|
||||
text.setPlaceholder('sk-...')
|
||||
.setValue(this.plugin.settings.vector_db_api_key)
|
||||
.onChange(value => {
|
||||
this.plugin.settings.vector_db_api_key = value;
|
||||
this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_getPythonDesc(pyPath, source) {
|
||||
if (source === 'stale') {
|
||||
return `[!!] ${pyPath} (stale — path no longer exists, update or clear the override below)`;
|
||||
|
|
@ -3512,6 +3721,11 @@ module.exports = class PaperForgePlugin extends Plugin {
|
|||
|
||||
async loadSettings() {
|
||||
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
||||
// Deep-merge nested objects (features, frozen_skills) to avoid overwrite
|
||||
if (this.settings.features && DEFAULT_SETTINGS.features) {
|
||||
this.settings.features = Object.assign({}, DEFAULT_SETTINGS.features, this.settings.features || {});
|
||||
}
|
||||
if (!this.settings.frozen_skills) { this.settings.frozen_skills = {}; }
|
||||
// Path fields come from paperforge.json, not from DEFAULT_SETTINGS or plugin data.json
|
||||
const pfConfig = this.readPaperforgeJson();
|
||||
this.settings.system_dir = pfConfig.system_dir;
|
||||
|
|
|
|||
Loading…
Reference in a new issue