mirror of
https://github.com/jacobinwwey/obsidian-NotEMD.git
synced 2026-07-22 05:48:27 +00:00
1830 lines
85 KiB
TypeScript
1830 lines
85 KiB
TypeScript
import { App, Editor, MarkdownView, Modal, Notice, Plugin, TFile, TFolder, PluginSettingTab, Setting, WorkspaceLeaf } from 'obsidian';
|
|
import { NotemdSettings, ProgressReporter, LLMProviderConfig, TaskKey } from './types';
|
|
import { DEFAULT_SETTINGS, NOTEMD_SIDEBAR_VIEW_TYPE, NOTEMD_SIDEBAR_DISPLAY_TEXT, NOTEMD_SIDEBAR_ICON } from './constants';
|
|
import { delay, createConcurrentProcessor, chunkArray, retry, normalizeNameForFilePath } from './utils';
|
|
import { testAPI, callLLM } from './llmUtils';
|
|
import {
|
|
handleFileRename,
|
|
handleFileDelete,
|
|
processFile,
|
|
generateContentForTitle,
|
|
batchGenerateContentForTitles,
|
|
checkAndRemoveDuplicateConceptNotes,
|
|
batchFixMermaidSyntaxInFolder,
|
|
findDuplicates,
|
|
saveMermaidSummaryFile,
|
|
extractConceptsFromFile,
|
|
createConceptNotes,
|
|
fixMermaidSyntaxInFile,
|
|
saveErrorLog // Import
|
|
} from './fileUtils';
|
|
import { fixFormulaFormatsInFile, batchFixFormulaFormatsInFolder } from './formulaFixer';
|
|
import { _performResearch, researchAndSummarize } from './searchUtils'; // Import _performResearch if needed directly, ensure researchAndSummarize is exported
|
|
import { ProgressModal } from './ui/ProgressModal';
|
|
import { ErrorModal } from './ui/ErrorModal';
|
|
import { NotemdSettingTab } from './ui/NotemdSettingTab';
|
|
import { showDeletionConfirmationModal } from './ui/modals'; // Import the modal function
|
|
import { NotemdSidebarView } from './ui/NotemdSidebarView';
|
|
import { translateFile, batchTranslateFolder } from './translate';
|
|
import { getSystemPrompt } from './promptUtils';
|
|
import { extractOriginalText } from './extractOriginalText';
|
|
|
|
export default class NotemdPlugin extends Plugin {
|
|
settings: NotemdSettings;
|
|
statusBarItem: HTMLElement;
|
|
private isBusy: boolean = false;
|
|
currentProcessingFileBasename: { value: string | null } = { value: null }; // Keep track of the file being processed
|
|
|
|
public getIsBusy(): boolean {
|
|
return this.isBusy;
|
|
}
|
|
|
|
// Method to set the busy state, primarily for internal use by command handlers
|
|
// Could potentially be used by UI components if needed, but command handlers are safer
|
|
public setBusy(busy: boolean) {
|
|
this.isBusy = busy;
|
|
// Optionally update status bar or trigger UI updates if needed globally
|
|
// this.updateStatusBar(busy ? 'Busy...' : 'Ready');
|
|
}
|
|
|
|
async onload() {
|
|
await this.loadSettings();
|
|
|
|
// --- Sidebar View ---
|
|
this.registerView(NOTEMD_SIDEBAR_VIEW_TYPE, (leaf) => new NotemdSidebarView(leaf, this));
|
|
|
|
this.addCommand({
|
|
id: 'notemd-summarize-as-mermaid',
|
|
name: 'Summarise as Mermaid diagram',
|
|
editorCallback: async (editor: Editor, view: MarkdownView) => {
|
|
const file = view.file;
|
|
if (file) {
|
|
const reporter = this.getReporter();
|
|
await this.summarizeToMermaidCommand(file, reporter);
|
|
}
|
|
},
|
|
});
|
|
|
|
this.addRibbonIcon(NOTEMD_SIDEBAR_ICON, NOTEMD_SIDEBAR_DISPLAY_TEXT, () => {
|
|
this.activateView();
|
|
});
|
|
|
|
// --- Status Bar ---
|
|
this.statusBarItem = this.addStatusBarItem();
|
|
this.updateStatusBar('Ready');
|
|
|
|
// --- Command Palette Integration ---
|
|
this.addCommand({
|
|
id: 'process-with-notemd',
|
|
name: 'Process current file (add links)',
|
|
checkCallback: (checking: boolean) => {
|
|
const activeFile = this.app.workspace.getActiveFile();
|
|
const condition = activeFile && (activeFile.extension === 'md' || activeFile.extension === 'txt');
|
|
if (condition) {
|
|
if (!checking) {
|
|
this.processWithNotemdCommand();
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
});
|
|
|
|
this.addCommand({
|
|
id: 'process-folder-with-notemd',
|
|
name: 'Process folder (add links)',
|
|
callback: async () => {
|
|
await this.processFolderWithNotemdCommand(); // Use the command handler method
|
|
}
|
|
});
|
|
|
|
// Command to check duplicates in the current file (simple version)
|
|
this.addCommand({
|
|
id: 'check-for-duplicates',
|
|
name: 'Check for duplicates in current file',
|
|
checkCallback: (checking: boolean) => {
|
|
const activeFile = this.app.workspace.getActiveFile();
|
|
const condition = activeFile && (activeFile.extension === 'md' || activeFile.extension === 'txt');
|
|
if (condition) {
|
|
if (!checking) {
|
|
// This command is simple and doesn't use the full reporter/busy state
|
|
// We need to ensure activeFile is valid here if it was captured in an outer scope for check,
|
|
// or re-fetch it. Given the example, it's safer to re-evaluate.
|
|
const currentActiveFile = this.app.workspace.getActiveFile();
|
|
if (currentActiveFile && (currentActiveFile.extension === 'md' || currentActiveFile.extension === 'txt')) {
|
|
(async () => { // Wrap async logic
|
|
try {
|
|
const content = await this.app.vault.read(currentActiveFile);
|
|
const duplicates = findDuplicates(content); // Use utility
|
|
const message = `Found ${duplicates.size} potential duplicate terms. Check console.`;
|
|
new Notice(message);
|
|
if (duplicates.size > 0) {
|
|
console.log(`Potential duplicates in ${currentActiveFile.name}:`, Array.from(duplicates));
|
|
}
|
|
} catch (error: unknown) {
|
|
let errorMessage = 'An unknown error occurred while checking duplicates.';
|
|
if (error instanceof Error) {
|
|
errorMessage = error.message;
|
|
}
|
|
new Notice(`Error checking duplicates: ${errorMessage}`);
|
|
console.error("Error checking duplicates:", error);
|
|
}
|
|
})();
|
|
} else if (!checking) { // If file became invalid between check and action
|
|
new Notice("No active '.md' or '.txt' file to check.");
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
});
|
|
|
|
|
|
this.addCommand({
|
|
id: 'test-llm-connection',
|
|
name: 'Test LLM connection',
|
|
checkCallback: (checking: boolean) => {
|
|
const provider = this.settings.providers.find(p => p.name === this.settings.activeProvider);
|
|
const condition = !!provider;
|
|
if (condition) {
|
|
if (!checking) {
|
|
this.testLlmConnectionCommand();
|
|
}
|
|
return true;
|
|
}
|
|
if (!checking) { // Only show notice if trying to execute, not just checking availability
|
|
new Notice("No active LLM provider configured. Please check Notemd settings.");
|
|
}
|
|
return false;
|
|
}
|
|
});
|
|
|
|
this.addCommand({
|
|
id: 'generate-content-from-title',
|
|
name: 'Generate content from note title',
|
|
checkCallback: (checking: boolean) => {
|
|
const activeFile = this.app.workspace.getActiveFile();
|
|
const condition = activeFile && activeFile instanceof TFile && activeFile.extension === 'md';
|
|
if (condition) {
|
|
if (!checking) {
|
|
// Re-fetch activeFile for safety, as it might change
|
|
const currentActiveFile = this.app.workspace.getActiveFile();
|
|
if (currentActiveFile && currentActiveFile instanceof TFile && currentActiveFile.extension === 'md') {
|
|
this.generateContentForTitleCommand(currentActiveFile);
|
|
} else {
|
|
new Notice('No active Markdown file selected or file changed.');
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
if (!checking) {
|
|
new Notice('No active Markdown file selected.');
|
|
}
|
|
return false;
|
|
}
|
|
});
|
|
|
|
this.addCommand({
|
|
id: 'research-and-summarize-topic',
|
|
name: 'Research and summarize topic',
|
|
checkCallback: (checking: boolean) => {
|
|
const activeView = this.app.workspace.getActiveViewOfType(MarkdownView);
|
|
const condition = activeView !== null;
|
|
if (condition) {
|
|
if (!checking) {
|
|
// Re-fetch activeView for safety
|
|
const currentActiveView = this.app.workspace.getActiveViewOfType(MarkdownView);
|
|
if (currentActiveView) {
|
|
this.researchAndSummarizeCommand(currentActiveView.editor, currentActiveView);
|
|
}
|
|
} else {
|
|
new Notice('No active Markdown editor found.');
|
|
}
|
|
return true;
|
|
}
|
|
if (!checking) {
|
|
new Notice('No active Markdown editor found.');
|
|
}
|
|
return false;
|
|
}
|
|
});
|
|
|
|
this.addCommand({
|
|
id: 'batch-generate-content-from-titles',
|
|
name: 'Batch generate content from titles',
|
|
callback: async () => {
|
|
await this.batchGenerateContentForTitlesCommand(); // Use the command handler method
|
|
}
|
|
});
|
|
|
|
this.addCommand({
|
|
id: 'check-and-remove-duplicate-concept-notes',
|
|
name: 'Check and remove duplicate concept notes',
|
|
callback: async () => {
|
|
await this.checkAndRemoveDuplicateConceptNotesCommand(); // Use the command handler method
|
|
}
|
|
});
|
|
|
|
this.addCommand({
|
|
id: 'batch-mermaid-fix',
|
|
name: 'Batch fix Mermaid syntax',
|
|
callback: async () => {
|
|
await this.batchMermaidFixCommand(); // Use the new command handler method
|
|
}
|
|
});
|
|
|
|
this.addCommand({
|
|
id: 'fix-formula-formats',
|
|
name: 'Fix formula formats (current file)',
|
|
checkCallback: (checking: boolean) => {
|
|
const activeFile = this.app.workspace.getActiveFile();
|
|
const condition = activeFile && (activeFile.extension === 'md' || activeFile.extension === 'txt');
|
|
if (condition) {
|
|
if (!checking) {
|
|
this.fixFormulaFormatsCommand(activeFile);
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
});
|
|
|
|
this.addCommand({
|
|
id: 'batch-fix-formula-formats',
|
|
name: 'Batch fix formula formats',
|
|
callback: async () => {
|
|
await this.batchFixFormulaFormatsCommand();
|
|
}
|
|
});
|
|
|
|
this.addCommand({
|
|
id: 'translate-file',
|
|
name: 'Translate current file',
|
|
checkCallback: (checking: boolean) => {
|
|
const activeFile = this.app.workspace.getActiveFile();
|
|
if (activeFile) {
|
|
if (!checking) {
|
|
this.translateFileCommand(activeFile);
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
});
|
|
|
|
this.addCommand({
|
|
id: 'extract-concepts-from-current-file',
|
|
name: 'Extract concepts (create concept notes only)',
|
|
checkCallback: (checking: boolean) => {
|
|
const activeFile = this.app.workspace.getActiveFile();
|
|
const condition = activeFile && (activeFile.extension === 'md' || activeFile.extension === 'txt');
|
|
if (condition) {
|
|
if (!checking) {
|
|
this.extractConceptsCommand();
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
});
|
|
|
|
this.addCommand({
|
|
id: 'batch-extract-concepts-from-folder',
|
|
name: 'Batch extract concepts from folder',
|
|
callback: async () => {
|
|
await this.batchExtractConceptsForFolderCommand();
|
|
}
|
|
});
|
|
|
|
this.addCommand({
|
|
id: 'batch-translate-folder',
|
|
name: 'Batch Translate Folder',
|
|
callback: async () => {
|
|
await this.batchTranslateFolderCommand();
|
|
}
|
|
});
|
|
|
|
this.registerEvent(
|
|
this.app.workspace.on('file-menu', (menu, file) => {
|
|
if (file instanceof TFolder) {
|
|
menu.addItem((item) => {
|
|
item
|
|
.setTitle('Batch Translate')
|
|
.setIcon('language')
|
|
.onClick(async () => {
|
|
await this.batchTranslateFolderCommand(file);
|
|
});
|
|
});
|
|
}
|
|
})
|
|
);
|
|
|
|
this.addCommand({
|
|
id: 'extract-concepts-and-generate-titles',
|
|
name: 'Extract Concepts and Generate Titles',
|
|
checkCallback: (checking: boolean) => {
|
|
const activeFile = this.app.workspace.getActiveFile();
|
|
if (activeFile && activeFile.extension === 'md') {
|
|
if (!checking) {
|
|
this.extractConceptsAndGenerateTitlesCommand();
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
});
|
|
|
|
this.addCommand({
|
|
id: 'extract-concepts-and-generate-titles',
|
|
name: 'Extract Concepts and Generate Titles',
|
|
checkCallback: (checking: boolean) => {
|
|
const activeFile = this.app.workspace.getActiveFile();
|
|
if (activeFile && activeFile.extension === 'md') {
|
|
if (!checking) {
|
|
this.extractConceptsAndGenerateTitlesCommand();
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
});
|
|
|
|
this.addCommand({
|
|
id: 'create-wiki-link-and-generate-from-selection',
|
|
name: 'Create Wiki-Link & Generate Note from Selection',
|
|
editorCallback: async (editor: Editor, view: MarkdownView) => {
|
|
const word = editor.getSelection().trim();
|
|
if (!word || word.length < 2) {
|
|
new Notice('Select a valid word (2+ chars).');
|
|
return;
|
|
}
|
|
|
|
// Replace selection with wiki-link
|
|
editor.replaceSelection(`[[${word}]]`);
|
|
|
|
if (!this.settings.useCustomConceptNoteFolder || !this.settings.conceptNoteFolder) {
|
|
new Notice('Set Concept Note Folder in settings.');
|
|
return;
|
|
}
|
|
|
|
const safeName = normalizeNameForFilePath(word);
|
|
const notePath = `${this.settings.conceptNoteFolder}/${safeName}.md`;
|
|
const existingFile = this.app.vault.getAbstractFileByPath(notePath);
|
|
|
|
let newFile: TFile;
|
|
const reporter = this.getReporter();
|
|
|
|
try {
|
|
if (existingFile instanceof TFile) {
|
|
// Add backlink to existing (reuse createConceptNotes logic)
|
|
await createConceptNotes(this.app, this.settings, new Set([word]), view.file?.basename || null, { disableBacklink: false });
|
|
newFile = existingFile;
|
|
reporter.log(`Updated existing note: ${notePath}`);
|
|
} else {
|
|
// Create blank note (reuse createConceptNotes minimal template)
|
|
await createConceptNotes(this.app, this.settings, new Set([word]), view.file?.basename || null, { minimalTemplate: false });
|
|
const createdFile = this.app.vault.getAbstractFileByPath(notePath);
|
|
if (createdFile instanceof TFile) {
|
|
newFile = createdFile;
|
|
} else {
|
|
throw new Error("Failed to create note.");
|
|
}
|
|
reporter.log(`Created blank note: ${notePath}`);
|
|
}
|
|
|
|
// Auto-run Generate from Title
|
|
if (this.isBusy) throw new Error('Busy');
|
|
this.isBusy = true;
|
|
await generateContentForTitle(this.app, this.settings, newFile, reporter);
|
|
|
|
if (this.settings.autoMermaidFixAfterGenerate) {
|
|
reporter.log("Running automatic Mermaid syntax fix...");
|
|
await fixMermaidSyntaxInFile(this.app, newFile, reporter);
|
|
}
|
|
|
|
reporter.updateStatus('Wiki-Link & Generation complete!', 100);
|
|
if (reporter instanceof ProgressModal) {
|
|
setTimeout(() => reporter.close(), 2000);
|
|
}
|
|
|
|
new Notice(`Generated content for [[${word}]]!`);
|
|
} catch (error: any) {
|
|
new Notice(`Error: ${error.message}`);
|
|
reporter.log(`Error: ${error.message}`);
|
|
} finally {
|
|
this.isBusy = false;
|
|
}
|
|
},
|
|
});
|
|
|
|
// --- Settings Tab ---
|
|
this.addSettingTab(new NotemdSettingTab(this.app, this));
|
|
|
|
// --- Event Listeners ---
|
|
this.registerEvent(this.app.vault.on('rename', (file, oldPath) => {
|
|
if (file instanceof TFile && file.extension === 'md') {
|
|
handleFileRename(this.app, oldPath, file.path); // Call utility function
|
|
}
|
|
}));
|
|
this.registerEvent(this.app.vault.on('delete', (file) => {
|
|
if (file instanceof TFile && file.extension === 'md') {
|
|
handleFileDelete(this.app, file.path); // Call utility function
|
|
}
|
|
}));
|
|
|
|
// Optional: Register DOM events or intervals if needed
|
|
// this.registerDomEvent(document, 'click', (evt: MouseEvent) => {});
|
|
// this.registerInterval(window.setInterval(() => console.log('Interval'), 5 * 60 * 1000));
|
|
}
|
|
|
|
onunload() {
|
|
// Clean up resources if necessary
|
|
}
|
|
|
|
// --- Settings Management ---
|
|
async loadSettings() {
|
|
const savedData = await this.loadData() || {};
|
|
const savedProviders = savedData.providers || [];
|
|
const defaultProviders = DEFAULT_SETTINGS.providers;
|
|
const mergedProviders: LLMProviderConfig[] = [];
|
|
const savedProviderMap = new Map(savedProviders.map((p: LLMProviderConfig) => [p.name, p]));
|
|
|
|
savedProviders.forEach((savedProvider: LLMProviderConfig) => {
|
|
const defaultProvider = defaultProviders.find(dp => dp.name === savedProvider.name);
|
|
mergedProviders.push({ ...(defaultProvider || {}), ...savedProvider });
|
|
});
|
|
|
|
defaultProviders.forEach(defaultProvider => {
|
|
if (!savedProviderMap.has(defaultProvider.name)) {
|
|
mergedProviders.push(defaultProvider);
|
|
}
|
|
});
|
|
|
|
this.settings = Object.assign({}, DEFAULT_SETTINGS, savedData, { providers: mergedProviders });
|
|
|
|
if (!this.settings.providers.some(p => p.name === this.settings.activeProvider)) {
|
|
this.settings.activeProvider = DEFAULT_SETTINGS.activeProvider;
|
|
}
|
|
// Ensure task-specific providers fall back to active if invalid or not set
|
|
this.settings.addLinksProvider = this.settings.providers.some(p => p.name === this.settings.addLinksProvider) ? this.settings.addLinksProvider : this.settings.activeProvider;
|
|
this.settings.researchProvider = this.settings.providers.some(p => p.name === this.settings.researchProvider) ? this.settings.researchProvider : this.settings.activeProvider;
|
|
this.settings.generateTitleProvider = this.settings.providers.some(p => p.name === this.settings.generateTitleProvider) ? this.settings.generateTitleProvider : this.settings.activeProvider;
|
|
this.settings.translateProvider = this.settings.providers.some(p => p.name === this.settings.translateProvider) ? this.settings.translateProvider : this.settings.activeProvider;
|
|
this.settings.extractConceptsProvider = this.settings.providers.some(p => p.name === this.settings.extractConceptsProvider) ? this.settings.extractConceptsProvider : this.settings.activeProvider;
|
|
|
|
// Merge availableLanguages to ensure new languages are added for existing users
|
|
const defaultLanguages = DEFAULT_SETTINGS.availableLanguages;
|
|
const savedLanguages = this.settings.availableLanguages || [];
|
|
const savedLanguageCodes = new Set(savedLanguages.map(l => l.code));
|
|
const mergedLanguages = [...savedLanguages];
|
|
|
|
defaultLanguages.forEach(defaultLang => {
|
|
if (!savedLanguageCodes.has(defaultLang.code)) {
|
|
mergedLanguages.push(defaultLang);
|
|
}
|
|
});
|
|
|
|
this.settings.availableLanguages = mergedLanguages;
|
|
}
|
|
|
|
async saveSettings() {
|
|
await this.saveData(this.settings);
|
|
}
|
|
|
|
// --- UI and Status ---
|
|
updateStatusBar(text: string) {
|
|
if (this.statusBarItem) {
|
|
this.statusBarItem.setText(`Notemd: ${text}`);
|
|
}
|
|
}
|
|
|
|
async activateView() {
|
|
const existingLeaves = this.app.workspace.getLeavesOfType(NOTEMD_SIDEBAR_VIEW_TYPE);
|
|
if (existingLeaves.length > 0) {
|
|
this.app.workspace.revealLeaf(existingLeaves[0]);
|
|
return;
|
|
}
|
|
const leaf = this.app.workspace.getRightLeaf(false);
|
|
if (leaf) {
|
|
await leaf.setViewState({ type: NOTEMD_SIDEBAR_VIEW_TYPE, active: true });
|
|
this.app.workspace.revealLeaf(leaf);
|
|
} else {
|
|
console.error("Could not get right sidebar leaf.");
|
|
new Notice("Could not open Notemd sidebar.");
|
|
}
|
|
}
|
|
|
|
/** Helper to get a progress reporter (Sidebar or new Modal) */
|
|
getReporter(): ProgressReporter {
|
|
const view = this.app.workspace.getLeavesOfType(NOTEMD_SIDEBAR_VIEW_TYPE)[0]?.view;
|
|
if (view instanceof NotemdSidebarView) {
|
|
this.app.workspace.revealLeaf(view.leaf); // Ensure sidebar is visible
|
|
view.clearDisplay(); // Clear previous logs/status in sidebar
|
|
return view;
|
|
} else {
|
|
const modal = new ProgressModal(this.app);
|
|
modal.open();
|
|
return modal;
|
|
}
|
|
}
|
|
|
|
/** Helper to show folder selection modal */
|
|
async getFolderSelection(): Promise<string | null> {
|
|
const folders = this.app.vault.getAllLoadedFiles()
|
|
.filter((f): f is TFolder => f instanceof TFolder) // Type guard
|
|
.map(f => f.path);
|
|
folders.unshift('/'); // Add root
|
|
|
|
return new Promise((resolve) => {
|
|
const modal = new Modal(this.app);
|
|
modal.titleEl.setText('Select Folder');
|
|
const selectEl = modal.contentEl.createEl('select');
|
|
folders.forEach(folder => selectEl.createEl('option', { text: folder === '/' ? '(Vault Root)' : folder, value: folder }));
|
|
const btnContainer = modal.contentEl.createDiv({ cls: 'modal-button-container' });
|
|
btnContainer.createEl('button', { text: 'Select', cls: 'mod-cta' }).onclick = () => { modal.close(); resolve(selectEl.value); };
|
|
btnContainer.createEl('button', { text: 'Cancel' }).onclick = () => { modal.close(); resolve(null); };
|
|
modal.open();
|
|
});
|
|
}
|
|
|
|
log(message: string) {
|
|
// Simple console log for now. Could be expanded to log to a file.
|
|
console.log(`[Notemd] ${message}`);
|
|
}
|
|
|
|
getProviderAndModelForTask(taskKey: 'addLinks' | 'research' | 'generateTitle' | 'translate' | 'summarizeToMermaid' | 'extractConcepts' | 'extractOriginalText'): { provider: LLMProviderConfig, modelName: string } {
|
|
let providerName: string = this.settings.activeProvider;
|
|
let modelName: string | undefined = undefined;
|
|
|
|
if (this.settings.useMultiModelSettings) {
|
|
switch (taskKey) {
|
|
case 'addLinks':
|
|
providerName = this.settings.addLinksProvider;
|
|
modelName = this.settings.addLinksModel;
|
|
break;
|
|
case 'research':
|
|
providerName = this.settings.researchProvider;
|
|
modelName = this.settings.researchModel;
|
|
break;
|
|
case 'generateTitle':
|
|
providerName = this.settings.generateTitleProvider;
|
|
modelName = this.settings.generateTitleModel;
|
|
break;
|
|
case 'translate':
|
|
providerName = this.settings.translateProvider;
|
|
modelName = this.settings.translateModel;
|
|
break;
|
|
case 'summarizeToMermaid':
|
|
providerName = this.settings.summarizeToMermaidProvider;
|
|
modelName = this.settings.summarizeToMermaidModel;
|
|
break;
|
|
case 'extractConcepts':
|
|
providerName = this.settings.extractConceptsProvider;
|
|
modelName = this.settings.extractConceptsModel;
|
|
break;
|
|
case 'extractOriginalText':
|
|
providerName = this.settings.extractOriginalTextProvider;
|
|
modelName = this.settings.extractOriginalTextModel;
|
|
break;
|
|
}
|
|
}
|
|
|
|
const provider = this.settings.providers.find(p => p.name === providerName) || this.settings.providers.find(p => p.name === this.settings.activeProvider);
|
|
if (!provider) {
|
|
throw new Error("Could not find a valid LLM provider. Please check your settings.");
|
|
}
|
|
|
|
return { provider, modelName: modelName || provider.model };
|
|
}
|
|
|
|
getPromptForTask(taskKey: TaskKey, replacements: Record<string, string> = {}): string {
|
|
const prompt = getSystemPrompt(this.settings, taskKey, replacements);
|
|
if (this.settings.enableGlobalCustomPrompts &&
|
|
((taskKey === 'addLinks' && this.settings.useCustomPromptForAddLinks) ||
|
|
(taskKey === 'generateTitle' && this.settings.useCustomPromptForGenerateTitle) ||
|
|
(taskKey === 'researchSummarize' && this.settings.useCustomPromptForResearchSummarize) ||
|
|
(taskKey === 'summarizeToMermaid' && this.settings.useCustomPromptForSummarizeToMermaid))) {
|
|
this.log(`Using custom prompt for task: ${taskKey}`);
|
|
} else if (this.settings.enableFocusedLearning && this.settings.focusedLearningDomain) {
|
|
this.log(`Using focused learning domain for task: ${taskKey}`);
|
|
} else {
|
|
this.log(`Using default prompt for task: ${taskKey}`);
|
|
}
|
|
return prompt;
|
|
}
|
|
|
|
// --- Command Handler Methods ---
|
|
// These methods contain the core logic initiated by commands or sidebar buttons.
|
|
|
|
/** Command: Process Current File (Add Links) */
|
|
async processWithNotemdCommand(reporter?: ProgressReporter) {
|
|
if (this.isBusy) { new Notice("Notemd is busy."); return; }
|
|
this.isBusy = true;
|
|
const useReporter = reporter || this.getReporter();
|
|
|
|
const maybeSidebar = useReporter as any;
|
|
if (maybeSidebar instanceof NotemdSidebarView) {
|
|
maybeSidebar.startProcessing('Processing current file...');
|
|
} else {
|
|
useReporter.clearDisplay();
|
|
useReporter.updateStatus('Processing current file...', 0);
|
|
}
|
|
|
|
try {
|
|
await this.loadSettings(); // Load latest settings
|
|
const activeFile = this.app.workspace.getActiveFile();
|
|
if (!activeFile || !(activeFile instanceof TFile) || (activeFile.extension !== 'md' && activeFile.extension !== 'txt')) {
|
|
throw new Error("No active '.md' or '.txt' file to process.");
|
|
}
|
|
|
|
this.updateStatusBar(`Processing: ${activeFile.name}`);
|
|
|
|
// Pass the ref object for currentProcessingFileBasename
|
|
await processFile(this.app, this.settings, activeFile, useReporter, this.currentProcessingFileBasename);
|
|
|
|
this.updateStatusBar('Processing complete');
|
|
useReporter.updateStatus('Processing complete!', 100);
|
|
new Notice('Notemd processing complete!');
|
|
if (useReporter instanceof ProgressModal) setTimeout(() => useReporter.close(), 2000);
|
|
|
|
} catch (error: unknown) { // Changed to unknown
|
|
this.updateStatusBar('Error occurred');
|
|
let errorMessage = 'An unknown error occurred during processing.';
|
|
let errorDetails = String(error);
|
|
if (error instanceof Error) {
|
|
errorMessage = error.message;
|
|
errorDetails = error.stack || error.message;
|
|
}
|
|
console.error("Notemd Processing Error:", errorDetails);
|
|
// Check if it's a cancellation error before showing notice/modal
|
|
if (!errorMessage.includes("cancelled by user")) {
|
|
new Notice(`Error during processing: ${errorMessage}. See console.`, 10000);
|
|
new ErrorModal(this.app, "Notemd Processing Error", errorDetails).open();
|
|
|
|
// Save error log
|
|
await saveErrorLog(this.app, useReporter, error, this.settings);
|
|
}
|
|
useReporter.log(`Error: ${errorMessage}`);
|
|
useReporter.updateStatus('Error occurred', -1);
|
|
// Keep reporter open on error/cancellation
|
|
} finally {
|
|
if (maybeSidebar instanceof NotemdSidebarView) {
|
|
maybeSidebar.finishProcessing();
|
|
}
|
|
this.isBusy = false;
|
|
}
|
|
}
|
|
|
|
/** Command: Process Folder (Add Links) */
|
|
async processFolderWithNotemdCommand(reporter?: ProgressReporter) {
|
|
if (this.isBusy) { new Notice("Notemd is busy."); return; }
|
|
this.isBusy = true;
|
|
const useReporter = reporter || this.getReporter();
|
|
|
|
const maybeSidebar = useReporter as any;
|
|
if (maybeSidebar instanceof NotemdSidebarView) {
|
|
maybeSidebar.startProcessing('Processing folder...');
|
|
} else {
|
|
useReporter.clearDisplay();
|
|
useReporter.updateStatus('Processing folder...', 0);
|
|
}
|
|
|
|
try {
|
|
await this.loadSettings();
|
|
const folderPath = await this.getFolderSelection();
|
|
if (!folderPath) { useReporter.log("Folder selection cancelled."); useReporter.updateStatus("Cancelled", -1); throw new Error("Folder selection cancelled."); }
|
|
|
|
const folder = this.app.vault.getAbstractFileByPath(folderPath);
|
|
if (!folder || !(folder instanceof TFolder)) throw new Error(`Invalid folder selected: ${folderPath}`);
|
|
|
|
const files = this.app.vault.getFiles().filter(f =>
|
|
(f.extension === 'md' || f.extension === 'txt') &&
|
|
(f.path === folderPath || f.path.startsWith(folderPath === '/' ? '' : folderPath + '/'))
|
|
);
|
|
|
|
if (files.length === 0) {
|
|
new Notice(`No '.md' or '.txt' files found in selected folder: ${folderPath}`);
|
|
useReporter.log(`No eligible files found in "${folderPath}".`);
|
|
useReporter.updateStatus('No files found', 100);
|
|
if (useReporter instanceof ProgressModal) setTimeout(() => useReporter.close(), 2000);
|
|
return; // Exit gracefully
|
|
}
|
|
|
|
this.updateStatusBar(`Batch processing ${files.length} files...`);
|
|
useReporter.log(`Starting batch processing for ${files.length} files in "${folderPath}"...`);
|
|
const errors: { file: string; message: string }[] = [];
|
|
|
|
if (!this.settings.enableBatchParallelism || this.settings.batchConcurrency <= 1) {
|
|
for (let i = 0; i < files.length; i++) {
|
|
const file = files[i];
|
|
if (useReporter.cancelled) { new Notice('Batch processing cancelled.'); this.updateStatusBar('Cancelled'); useReporter.updateStatus('Cancelled', -1); break; }
|
|
|
|
const progress = Math.floor(((i) / files.length) * 100);
|
|
useReporter.updateStatus(`Processing ${i + 1}/${files.length}: ${file.name}`, progress);
|
|
|
|
try {
|
|
// Pass the ref object
|
|
await processFile(this.app, this.settings, file, useReporter, this.currentProcessingFileBasename);
|
|
} catch (fileError: unknown) {
|
|
const message = fileError instanceof Error ? fileError.message : String(fileError);
|
|
const stack = fileError instanceof Error ? fileError.stack : undefined;
|
|
const errorMsg = `Error processing ${file.name}: ${message}`;
|
|
console.error(errorMsg, fileError);
|
|
useReporter.log(`❌ ${errorMsg}`);
|
|
errors.push({ file: file.name, message: message });
|
|
// Log silently
|
|
const timestamp = new Date().toISOString();
|
|
const logEntry = `[${timestamp}] Error processing ${file.path}:\nMessage: ${message}\nStack Trace:\n${stack || fileError}\n\n`;
|
|
try { await this.app.vault.adapter.append('error_processing_filename.log', logEntry); }
|
|
catch (logError) { console.error("Failed to write to error log:", logError); useReporter.log("⚠️ Failed to write error details to log file."); }
|
|
if (message.includes("cancelled by user")) break; // Exit loop on cancellation
|
|
}
|
|
} // End loop
|
|
} else {
|
|
const concurrency = Math.min(this.settings.batchConcurrency, 20); // Cap
|
|
const processor = createConcurrentProcessor(concurrency, this.settings.apiCallIntervalMs, useReporter);
|
|
const fileBatches = chunkArray(files, this.settings.batchSize);
|
|
let processedCount = 0;
|
|
|
|
for (let b = 0; b < fileBatches.length; b++) {
|
|
const batch = fileBatches[b];
|
|
useReporter.log(`Processing batch ${b + 1}/${fileBatches.length} (${batch.length} files)`);
|
|
if (useReporter.cancelled) break;
|
|
|
|
const tasks = batch.map(file => async () => {
|
|
const fileProgressReporter: ProgressReporter = {
|
|
log: (msg: string) => useReporter.log(`[${file.name}] ${msg}`),
|
|
updateStatus: (msg: string, percentage?: number) => {
|
|
if (percentage !== undefined) {
|
|
const overallProgress = Math.floor(((processedCount + (percentage / 100)) / files.length) * 100);
|
|
useReporter.updateStatus(`Batch: ${processedCount}/${files.length} (${file.name}: ${msg})`, overallProgress);
|
|
} else {
|
|
useReporter.updateStatus(`Batch: ${processedCount}/${files.length} (${file.name}: ${msg})`);
|
|
}
|
|
},
|
|
cancelled: useReporter.cancelled,
|
|
requestCancel: () => useReporter.requestCancel(),
|
|
clearDisplay: () => { },
|
|
abortController: useReporter.abortController,
|
|
activeTasks: useReporter.activeTasks,
|
|
updateActiveTasks: (delta: number) => useReporter.updateActiveTasks(delta),
|
|
};
|
|
|
|
try {
|
|
await processFile(this.app, this.settings, file, fileProgressReporter, this.currentProcessingFileBasename);
|
|
return { file, success: true };
|
|
} catch (e: unknown) {
|
|
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
fileProgressReporter.log(`❌ Error processing ${file.name}: ${errorMessage}`);
|
|
return { file, success: false, error: e };
|
|
}
|
|
});
|
|
|
|
const results = await processor(tasks);
|
|
processedCount += batch.length;
|
|
|
|
results.forEach(r => {
|
|
const result = r as { success: boolean; file: TFile; error?: any };
|
|
if (!result.success && result.error) {
|
|
const errorMessage = result.error.message || String(result.error);
|
|
errors.push({ file: result.file.name, message: errorMessage });
|
|
}
|
|
});
|
|
|
|
if (useReporter.cancelled) {
|
|
useReporter.log('Cancellation requested, stopping batch processing.');
|
|
break;
|
|
}
|
|
|
|
if (this.settings.batchInterDelayMs > 0 && b < fileBatches.length - 1) {
|
|
useReporter.log(`Delaying for ${this.settings.batchInterDelayMs}ms before next batch...`);
|
|
await delay(this.settings.batchInterDelayMs);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
if (!useReporter.cancelled) {
|
|
if (errors.length > 0) {
|
|
const errorSummary = `Batch processing finished with ${errors.length} error(s). Check 'error_processing_filename.log'.`;
|
|
useReporter.log(`⚠️ ${errorSummary}`); useReporter.updateStatus(errorSummary, -1);
|
|
this.updateStatusBar(`Batch complete with errors`); new Notice(errorSummary, 10000);
|
|
} else {
|
|
useReporter.updateStatus('Batch processing complete!', 100); this.updateStatusBar('Batch complete');
|
|
new Notice(`Successfully processed ${files.length} files.`, 5000);
|
|
if (useReporter instanceof ProgressModal) setTimeout(() => useReporter.close(), 2000);
|
|
}
|
|
}
|
|
|
|
} catch (error: unknown) { // Changed to unknown
|
|
this.updateStatusBar('Error occurred');
|
|
let errorMessage = 'An unknown error occurred during batch processing.';
|
|
let errorDetails = String(error);
|
|
if (error instanceof Error) {
|
|
errorMessage = error.message;
|
|
errorDetails = error.stack || error.message;
|
|
}
|
|
console.error("Notemd Batch Processing Error:", errorDetails);
|
|
// Check if it's a cancellation error before showing notice/modal
|
|
if (!errorMessage.includes("cancelled")) {
|
|
new Notice(`Error during batch processing: ${errorMessage}. See console.`, 10000);
|
|
new ErrorModal(this.app, "Notemd Batch Processing Error", errorDetails).open();
|
|
|
|
// Save error log
|
|
await saveErrorLog(this.app, useReporter, error, this.settings);
|
|
}
|
|
useReporter.log(`Batch Error: ${errorMessage}`);
|
|
useReporter.updateStatus('Error occurred during batch processing', -1);
|
|
} finally {
|
|
if (maybeSidebar instanceof NotemdSidebarView) {
|
|
maybeSidebar.finishProcessing();
|
|
}
|
|
this.isBusy = false;
|
|
}
|
|
}
|
|
|
|
// Note: The simple 'Check Duplicates' command logic is now directly in onload()
|
|
|
|
/** Command: Test LLM Connection */
|
|
// This command handler now uses the reporter for UI feedback
|
|
async testLlmConnectionCommand(reporter?: ProgressReporter) {
|
|
if (this.isBusy) { new Notice("Cannot test connection while processing."); return; }
|
|
this.isBusy = true; // Prevent other actions during test
|
|
const useReporter = reporter || this.getReporter();
|
|
if (!reporter) useReporter.clearDisplay();
|
|
|
|
try {
|
|
await this.loadSettings();
|
|
const provider = this.settings.providers.find(p => p.name === this.settings.activeProvider);
|
|
if (!provider) throw new Error('No active provider configured');
|
|
|
|
useReporter.log(`Testing connection to ${provider.name}...`);
|
|
useReporter.updateStatus(`Testing ${provider.name}...`, 50);
|
|
const testingNotice = new Notice(`Testing connection to ${provider.name}...`, 0);
|
|
|
|
const result = await testAPI(provider, this.settings.enableApiErrorDebugMode); // Use utility function
|
|
testingNotice.hide();
|
|
|
|
if (result.success) {
|
|
useReporter.log(`✅ Success: ${result.message}`);
|
|
new Notice(`✅ Success: ${result.message}`, 5000);
|
|
useReporter.updateStatus("Connection successful!", 100);
|
|
} else {
|
|
useReporter.log(`❌ Failed: ${result.message}. Check console.`);
|
|
new Notice(`❌ Failed: ${result.message}. Check console.`, 10000);
|
|
useReporter.updateStatus("Connection failed.", -1);
|
|
}
|
|
} catch (error: unknown) { // Changed to unknown
|
|
let errorMessage = 'An unknown error occurred during connection test.';
|
|
let errorDetails = String(error);
|
|
if (error instanceof Error) {
|
|
errorMessage = error.message;
|
|
errorDetails = error.stack || error.message;
|
|
}
|
|
useReporter.log(`❌ ${errorMessage}`);
|
|
new Notice(errorMessage, 10000);
|
|
console.error('LLM Connection Test Error:', errorDetails); // Log details
|
|
useReporter.updateStatus("Connection test error.", -1);
|
|
new ErrorModal(this.app, "LLM Connection Test Error", errorDetails).open();
|
|
|
|
// Save error log
|
|
await saveErrorLog(this.app, useReporter, error, this.settings);
|
|
} finally {
|
|
this.isBusy = false;
|
|
// No need to call useReporter.updateButtonStates() here
|
|
}
|
|
}
|
|
|
|
/** Command: Generate Content from Title */
|
|
async generateContentForTitleCommand(file: TFile, reporter?: ProgressReporter) {
|
|
if (this.isBusy) { new Notice("Notemd is busy."); return; }
|
|
this.isBusy = true;
|
|
const useReporter = reporter || this.getReporter();
|
|
|
|
const maybeSidebar = useReporter as any;
|
|
if (maybeSidebar instanceof NotemdSidebarView) {
|
|
maybeSidebar.startProcessing(`Generating: ${file.name}`);
|
|
} else {
|
|
useReporter.clearDisplay();
|
|
useReporter.updateStatus(`Generating: ${file.name}`, 0);
|
|
}
|
|
|
|
this.updateStatusBar(`Generating: ${file.name}`);
|
|
try {
|
|
await this.loadSettings();
|
|
await generateContentForTitle(this.app, this.settings, file, useReporter); // Call utility
|
|
|
|
if (this.settings.autoMermaidFixAfterGenerate) {
|
|
useReporter.log("Running automatic Mermaid syntax fix...");
|
|
await fixMermaidSyntaxInFile(this.app, file, useReporter);
|
|
}
|
|
|
|
this.updateStatusBar('Generation complete');
|
|
useReporter.updateStatus('Content generation complete!', 100);
|
|
new Notice(`Content generated successfully for ${file.name}!`);
|
|
if (useReporter instanceof ProgressModal) setTimeout(() => useReporter.close(), 2000);
|
|
} catch (error: unknown) { // Changed to unknown
|
|
this.updateStatusBar('Error during generation');
|
|
let errorMessage = 'An unknown error occurred during content generation.';
|
|
let errorDetails = String(error);
|
|
if (error instanceof Error) {
|
|
errorMessage = error.message;
|
|
errorDetails = error.stack || error.message;
|
|
}
|
|
// Check if it's a cancellation error before logging/showing modal
|
|
if (!errorMessage.includes("cancelled by user")) {
|
|
console.error(`Error generating content for ${file.name}:`, errorDetails);
|
|
new Notice(`Error generating content: ${errorMessage}. See console.`, 10000);
|
|
new ErrorModal(this.app, "Content Generation Error", errorDetails).open();
|
|
|
|
// Save error log
|
|
await saveErrorLog(this.app, useReporter, error, this.settings);
|
|
}
|
|
useReporter.log(`Error generating content for ${file.name}: ${errorMessage}`);
|
|
useReporter.updateStatus('Error occurred', -1);
|
|
} finally {
|
|
if (maybeSidebar instanceof NotemdSidebarView) {
|
|
maybeSidebar.finishProcessing();
|
|
}
|
|
this.isBusy = false;
|
|
}
|
|
}
|
|
|
|
/** Command: Research and Summarize Topic */
|
|
async researchAndSummarizeCommand(editor: Editor, view: MarkdownView, reporter?: ProgressReporter) {
|
|
if (this.isBusy) { new Notice("Notemd is busy."); return; }
|
|
this.isBusy = true;
|
|
const useReporter = reporter || this.getReporter();
|
|
|
|
const maybeSidebar = useReporter as any;
|
|
if (maybeSidebar instanceof NotemdSidebarView) {
|
|
maybeSidebar.startProcessing('Researching and summarizing...');
|
|
} else {
|
|
useReporter.clearDisplay();
|
|
useReporter.updateStatus('Researching and summarizing...', 0);
|
|
}
|
|
|
|
const activeFile = view.file;
|
|
if (!activeFile) { new Notice('No active file.'); this.isBusy = false; return; }
|
|
const selectedText = editor.getSelection();
|
|
const topic = selectedText ? selectedText.trim() : activeFile.basename;
|
|
if (!topic) { new Notice('No topic found (select text or use note title).'); this.isBusy = false; return; }
|
|
|
|
this.updateStatusBar(`Researching: ${topic}`);
|
|
useReporter.log(`Starting research for topic: "${topic}"`);
|
|
try {
|
|
await this.loadSettings();
|
|
// Assuming researchAndSummarize is now in searchUtils and takes app, settings
|
|
await researchAndSummarize(this.app, this.settings, editor, view, useReporter); // Call utility
|
|
// Success/error handling is now within researchAndSummarize
|
|
// Update status bar based on final reporter state?
|
|
if (!useReporter.cancelled) {
|
|
this.updateStatusBar('Research complete');
|
|
} else {
|
|
this.updateStatusBar('Research cancelled');
|
|
}
|
|
} catch (error: unknown) { // Changed to unknown, Catch errors propagated from researchAndSummarize
|
|
this.updateStatusBar('Error during research');
|
|
let errorMessage = 'An unknown error occurred during research.';
|
|
let errorDetails = String(error);
|
|
if (error instanceof Error) {
|
|
errorMessage = error.message;
|
|
errorDetails = error.stack || error.message;
|
|
}
|
|
if (!errorMessage.includes("cancelled by user")) {
|
|
console.error(`Error researching "${topic}":`, errorDetails);
|
|
new Notice(`Error during research: ${errorMessage}. See console.`, 10000);
|
|
new ErrorModal(this.app, "Research Error", errorDetails).open();
|
|
|
|
// Save error log
|
|
await saveErrorLog(this.app, useReporter, error, this.settings);
|
|
}
|
|
// Reporter status should already be set by the utility function if it's used
|
|
// If reporter wasn't used or failed early, log here
|
|
if (!useReporter.cancelled) { // Avoid double logging cancellation
|
|
useReporter.log(`Error: ${errorMessage}`);
|
|
useReporter.updateStatus('Error occurred', -1);
|
|
}
|
|
} finally {
|
|
if (maybeSidebar instanceof NotemdSidebarView) {
|
|
maybeSidebar.finishProcessing();
|
|
}
|
|
this.isBusy = false;
|
|
}
|
|
}
|
|
|
|
/** Command: Batch Generate Content from Titles */
|
|
async batchGenerateContentForTitlesCommand(reporter?: ProgressReporter) {
|
|
if (this.isBusy) { new Notice("Notemd is busy."); return; }
|
|
this.isBusy = true;
|
|
const useReporter = reporter || this.getReporter();
|
|
|
|
const maybeSidebar = useReporter as any;
|
|
if (maybeSidebar instanceof NotemdSidebarView) {
|
|
maybeSidebar.startProcessing('Batch generating content...');
|
|
} else {
|
|
useReporter.clearDisplay();
|
|
useReporter.updateStatus('Batch generating content...', 0);
|
|
}
|
|
|
|
try {
|
|
await this.loadSettings();
|
|
const folderPath = await this.getFolderSelection();
|
|
if (!folderPath) { useReporter.log("Folder selection cancelled."); useReporter.updateStatus("Cancelled", -1); throw new Error("Folder selection cancelled."); }
|
|
|
|
this.updateStatusBar(`Batch generating...`);
|
|
useReporter.log(`Starting batch generation for folder: "${folderPath}"...`);
|
|
|
|
const { errors } = await batchGenerateContentForTitles(this.app, this.settings, folderPath, useReporter); // Call utility
|
|
|
|
if (this.settings.autoMermaidFixAfterGenerate && !useReporter.cancelled) {
|
|
useReporter.log("Running automatic Mermaid syntax fix on completed folder...");
|
|
const folder = this.app.vault.getAbstractFileByPath(folderPath);
|
|
if (folder instanceof TFolder) {
|
|
let completeFolderName: string;
|
|
if (this.settings.useCustomGenerateTitleOutputFolder) {
|
|
completeFolderName = this.settings.generateTitleOutputFolderName || DEFAULT_SETTINGS.generateTitleOutputFolderName;
|
|
} else {
|
|
const baseFolderName = folderPath === '/' ? 'Vault' : folder.name;
|
|
completeFolderName = `${baseFolderName}_complete`;
|
|
}
|
|
const parentPath = folder.parent?.path === '/' ? '' : (folder.parent?.path ? folder.parent.path + '/' : '');
|
|
const completeFolderPath = `${parentPath}${completeFolderName}`;
|
|
|
|
useReporter.log(`Targeting completed folder for Mermaid fix: ${completeFolderPath}`);
|
|
await batchFixMermaidSyntaxInFolder(this.app, this.settings, completeFolderPath, useReporter);
|
|
} else {
|
|
useReporter.log("Could not determine completed folder path for Mermaid fix.");
|
|
}
|
|
}
|
|
|
|
if (!useReporter.cancelled) {
|
|
if (errors.length > 0) {
|
|
const errorSummary = `Batch generation finished with ${errors.length} error(s). Check 'error_processing_filename.log'.`;
|
|
useReporter.log(`⚠️ ${errorSummary}`); useReporter.updateStatus(errorSummary, -1);
|
|
this.updateStatusBar(`Batch generation complete with errors`); new Notice(errorSummary, 10000);
|
|
} else {
|
|
const fileCount = this.app.vault.getMarkdownFiles().filter(f => f.path.startsWith(folderPath === '/' ? '' : folderPath + '/')).length; // Re-count for notice
|
|
useReporter.updateStatus('Batch generation complete!', 100); this.updateStatusBar('Batch generation complete');
|
|
new Notice(`Successfully generated content for eligible files in "${folderPath}".`, 5000);
|
|
if (useReporter instanceof ProgressModal) setTimeout(() => useReporter.close(), 2000);
|
|
}
|
|
} else {
|
|
this.updateStatusBar('Batch generation cancelled');
|
|
new Notice('Batch generation cancelled.');
|
|
}
|
|
|
|
} catch (error: unknown) { // Changed to unknown
|
|
this.updateStatusBar('Error during batch generation');
|
|
let errorMessage = 'An unknown error occurred during batch generation.';
|
|
let errorDetails = String(error);
|
|
if (error instanceof Error) {
|
|
errorMessage = error.message;
|
|
errorDetails = error.stack || error.message;
|
|
}
|
|
if (!errorMessage.includes("cancelled")) {
|
|
console.error("Notemd Batch Generation Error:", errorDetails);
|
|
new Notice(`Error during batch generation: ${errorMessage}. See console.`, 10000);
|
|
new ErrorModal(this.app, "Notemd Batch Generation Error", errorDetails).open();
|
|
|
|
// Save error log
|
|
await saveErrorLog(this.app, useReporter, error, this.settings);
|
|
}
|
|
useReporter.log(`Batch Error: ${errorMessage}`);
|
|
useReporter.updateStatus('Error occurred during batch generation', -1);
|
|
} finally {
|
|
if (maybeSidebar instanceof NotemdSidebarView) {
|
|
maybeSidebar.finishProcessing();
|
|
}
|
|
this.isBusy = false;
|
|
}
|
|
}
|
|
|
|
/** Command: Check and Remove Duplicate Concept Notes */
|
|
async checkAndRemoveDuplicateConceptNotesCommand(reporter?: ProgressReporter) {
|
|
if (this.isBusy) { new Notice("Notemd is busy."); return; }
|
|
this.isBusy = true;
|
|
const useReporter = reporter || this.getReporter();
|
|
|
|
const maybeSidebar = useReporter as any;
|
|
if (maybeSidebar instanceof NotemdSidebarView) {
|
|
maybeSidebar.startProcessing('Checking duplicates...');
|
|
} else {
|
|
useReporter.clearDisplay();
|
|
useReporter.updateStatus('Checking duplicates...', 0);
|
|
}
|
|
|
|
this.updateStatusBar("Checking duplicates...");
|
|
useReporter.log("Starting: Check & Remove Duplicate Concept Notes...");
|
|
try {
|
|
await this.loadSettings();
|
|
await checkAndRemoveDuplicateConceptNotes(this.app, this.settings, useReporter); // Call utility
|
|
// Status is updated within the utility function
|
|
this.updateStatusBar("Duplicate check complete.");
|
|
} catch (error: unknown) { // Changed to unknown
|
|
this.updateStatusBar('Error during duplicate check');
|
|
let errorMessage = 'An unknown error occurred during duplicate check.';
|
|
let errorDetails = String(error);
|
|
if (error instanceof Error) {
|
|
errorMessage = error.message;
|
|
errorDetails = error.stack || error.message;
|
|
}
|
|
console.error("Error checking/removing duplicate concept notes:", errorDetails);
|
|
new Notice(`Error checking/removing duplicates: ${errorMessage}. See console.`, 10000);
|
|
useReporter.log(`Error: ${errorMessage}`);
|
|
useReporter.updateStatus('Error occurred', -1);
|
|
new ErrorModal(this.app, "Duplicate Check/Remove Error", errorDetails).open();
|
|
} finally {
|
|
if (maybeSidebar instanceof NotemdSidebarView) {
|
|
maybeSidebar.finishProcessing();
|
|
}
|
|
this.isBusy = false;
|
|
}
|
|
}
|
|
/** Command: Batch Fix Mermaid Syntax */
|
|
async batchMermaidFixCommand(reporter?: ProgressReporter) {
|
|
if (this.isBusy) { new Notice("Notemd is busy."); return; }
|
|
this.isBusy = true;
|
|
const useReporter = reporter || this.getReporter();
|
|
|
|
const maybeSidebar = useReporter as any;
|
|
if (maybeSidebar instanceof NotemdSidebarView) {
|
|
maybeSidebar.startProcessing('Batch fixing Mermaid syntax...');
|
|
} else {
|
|
useReporter.clearDisplay();
|
|
useReporter.updateStatus('Batch fixing Mermaid syntax...', 0);
|
|
}
|
|
|
|
try {
|
|
await this.loadSettings(); // Load settings in case needed by future logic
|
|
const folderPath = await this.getFolderSelection();
|
|
if (!folderPath) { useReporter.log("Folder selection cancelled."); useReporter.updateStatus("Cancelled", -1); throw new Error("Folder selection cancelled."); }
|
|
|
|
this.updateStatusBar(`Batch fixing Mermaid syntax...`);
|
|
useReporter.log(`Starting batch Mermaid fix for folder: "${folderPath}"...`);
|
|
|
|
const { errors, modifiedCount } = await batchFixMermaidSyntaxInFolder(this.app, this.settings, folderPath, useReporter); // Call utility
|
|
|
|
if (!useReporter.cancelled) {
|
|
if (errors.length > 0) {
|
|
const errorSummary = `Batch Mermaid fix finished with ${errors.length} error(s). Modified ${modifiedCount} files. Check console/log.`;
|
|
useReporter.log(`⚠️ ${errorSummary}`); useReporter.updateStatus(errorSummary, -1);
|
|
this.updateStatusBar(`Batch fix complete with errors`); new Notice(errorSummary, 10000);
|
|
} else {
|
|
const finalMessage = `Batch Mermaid fix complete! Modified ${modifiedCount} files.`;
|
|
useReporter.updateStatus(finalMessage, 100); this.updateStatusBar('Batch fix complete');
|
|
new Notice(finalMessage, 5000);
|
|
if (useReporter instanceof ProgressModal) setTimeout(() => useReporter.close(), 2000);
|
|
}
|
|
}
|
|
|
|
} catch (error: unknown) { // Changed to unknown
|
|
this.updateStatusBar('Error during batch fix');
|
|
let errorMessage = 'An unknown error occurred during batch Mermaid fix.';
|
|
let errorDetails = String(error);
|
|
if (error instanceof Error) {
|
|
errorMessage = error.message;
|
|
errorDetails = error.stack || error.message;
|
|
}
|
|
if (!errorMessage.includes("cancelled")) {
|
|
console.error("Notemd Batch Mermaid Fix Error:", errorDetails);
|
|
new Notice(`Error during batch fix: ${errorMessage}. See console.`, 10000);
|
|
new ErrorModal(this.app, "Notemd Batch Mermaid Fix Error", errorDetails).open();
|
|
|
|
// Save error log
|
|
await saveErrorLog(this.app, useReporter, error, this.settings);
|
|
}
|
|
useReporter.log(`Batch Fix Error: ${errorMessage}`);
|
|
useReporter.updateStatus('Error occurred during batch fix', -1);
|
|
} finally {
|
|
if (maybeSidebar instanceof NotemdSidebarView) {
|
|
maybeSidebar.finishProcessing();
|
|
}
|
|
this.isBusy = false;
|
|
}
|
|
}
|
|
|
|
async fixFormulaFormatsCommand(file: TFile, reporter?: ProgressReporter) {
|
|
if (this.isBusy) { new Notice("Notemd is busy."); return; }
|
|
this.isBusy = true;
|
|
const useReporter = reporter || this.getReporter();
|
|
|
|
const maybeSidebar = useReporter as any;
|
|
if (maybeSidebar instanceof NotemdSidebarView) {
|
|
maybeSidebar.startProcessing(`Fixing formulas in ${file.name}...`);
|
|
} else {
|
|
useReporter.clearDisplay();
|
|
useReporter.updateStatus(`Fixing formulas in ${file.name}...`, 0);
|
|
}
|
|
|
|
try {
|
|
await this.loadSettings();
|
|
const modified = await fixFormulaFormatsInFile(this.app, file, useReporter);
|
|
|
|
if (modified) {
|
|
useReporter.log(`✅ Fixed formulas in ${file.name}`);
|
|
new Notice(`Fixed formulas in ${file.name}`);
|
|
} else {
|
|
useReporter.log(`No formula fixes needed for ${file.name}`);
|
|
new Notice(`No formula fixes needed for ${file.name}`);
|
|
}
|
|
useReporter.updateStatus('Formula fix complete!', 100);
|
|
if (useReporter instanceof ProgressModal) setTimeout(() => useReporter.close(), 1000);
|
|
|
|
} catch (error: unknown) {
|
|
this.updateStatusBar('Error during formula fix');
|
|
let errorMessage = 'An unknown error occurred.';
|
|
if (error instanceof Error) errorMessage = error.message;
|
|
|
|
useReporter.log(`Error: ${errorMessage}`);
|
|
useReporter.updateStatus('Error occurred', -1);
|
|
new Notice(`Error: ${errorMessage}`);
|
|
await saveErrorLog(this.app, useReporter, error, this.settings);
|
|
} finally {
|
|
if (maybeSidebar instanceof NotemdSidebarView) {
|
|
maybeSidebar.finishProcessing();
|
|
}
|
|
this.isBusy = false;
|
|
}
|
|
}
|
|
|
|
async batchFixFormulaFormatsCommand(reporter?: ProgressReporter) {
|
|
if (this.isBusy) { new Notice("Notemd is busy."); return; }
|
|
this.isBusy = true;
|
|
const useReporter = reporter || this.getReporter();
|
|
|
|
const maybeSidebar = useReporter as any;
|
|
if (maybeSidebar instanceof NotemdSidebarView) {
|
|
maybeSidebar.startProcessing('Batch fixing formula formats...');
|
|
} else {
|
|
useReporter.clearDisplay();
|
|
useReporter.updateStatus('Batch fixing formula formats...', 0);
|
|
}
|
|
|
|
try {
|
|
await this.loadSettings();
|
|
const folderPath = await this.getFolderSelection();
|
|
if (!folderPath) { useReporter.log("Cancelled."); useReporter.updateStatus("Cancelled", -1); throw new Error("Cancelled"); }
|
|
|
|
const { modifiedCount, errors } = await batchFixFormulaFormatsInFolder(this.app, folderPath, useReporter);
|
|
|
|
if (!useReporter.cancelled) {
|
|
if (errors.length > 0) {
|
|
const msg = `Batch fix finished with ${errors.length} errors. Modified ${modifiedCount} files.`;
|
|
useReporter.log(msg);
|
|
useReporter.updateStatus('Finished with errors', -1);
|
|
new Notice(msg);
|
|
} else {
|
|
const msg = `Batch fix complete! Modified ${modifiedCount} files.`;
|
|
useReporter.log(msg);
|
|
useReporter.updateStatus('Complete', 100);
|
|
new Notice(msg);
|
|
if (useReporter instanceof ProgressModal) setTimeout(() => useReporter.close(), 2000);
|
|
}
|
|
}
|
|
} catch (error: unknown) {
|
|
let errorMessage = 'An unknown error occurred.';
|
|
if (error instanceof Error) errorMessage = error.message;
|
|
if (!errorMessage.includes("Cancelled")) {
|
|
useReporter.log(`Error: ${errorMessage}`);
|
|
useReporter.updateStatus('Error occurred', -1);
|
|
new Notice(`Error: ${errorMessage}`);
|
|
await saveErrorLog(this.app, useReporter, error, this.settings);
|
|
}
|
|
} finally {
|
|
if (maybeSidebar instanceof NotemdSidebarView) {
|
|
maybeSidebar.finishProcessing();
|
|
}
|
|
this.isBusy = false;
|
|
}
|
|
}
|
|
|
|
async batchTranslateFolderCommand(folder?: TFolder, reporter?: ProgressReporter) {
|
|
if (this.isBusy) {
|
|
new Notice("Notemd is busy.");
|
|
return;
|
|
}
|
|
this.isBusy = true;
|
|
const useReporter = reporter || this.getReporter();
|
|
|
|
const maybeSidebar = useReporter as any;
|
|
if (maybeSidebar instanceof NotemdSidebarView) {
|
|
maybeSidebar.startProcessing("Batch translating...");
|
|
} else {
|
|
useReporter.clearDisplay();
|
|
useReporter.updateStatus("Batch translating...", 0);
|
|
}
|
|
|
|
this.updateStatusBar("Batch translating...");
|
|
|
|
try {
|
|
await this.loadSettings();
|
|
let targetFolder = folder;
|
|
if (!targetFolder) {
|
|
const folderPath = await this.getFolderSelection();
|
|
if (!folderPath) {
|
|
throw new Error("Folder selection cancelled.");
|
|
}
|
|
const abstractFile = this.app.vault.getAbstractFileByPath(folderPath);
|
|
if (!(abstractFile instanceof TFolder)) {
|
|
throw new Error("Invalid folder selected.");
|
|
}
|
|
targetFolder = abstractFile;
|
|
}
|
|
|
|
await batchTranslateFolder(this.app, this.settings, targetFolder, this.settings.language);
|
|
|
|
} catch (error: unknown) {
|
|
this.updateStatusBar("Batch translation failed");
|
|
let errorMessage = 'An unknown error occurred during batch translation.';
|
|
if (error instanceof Error) {
|
|
errorMessage = error.message;
|
|
}
|
|
if (!errorMessage.includes("cancelled")) {
|
|
new Notice(`Failed to translate folder: ${errorMessage}. See console for details.`, 10000);
|
|
new ErrorModal(this.app, "Batch Translation Error", errorMessage).open();
|
|
|
|
// Save error log
|
|
await saveErrorLog(this.app, useReporter, error, this.settings);
|
|
}
|
|
useReporter.log(`Error: ${errorMessage}`);
|
|
useReporter.updateStatus('Error occurred', -1);
|
|
} finally {
|
|
if (maybeSidebar instanceof NotemdSidebarView) {
|
|
maybeSidebar.finishProcessing();
|
|
}
|
|
this.isBusy = false;
|
|
}
|
|
}
|
|
|
|
async translateFileCommand(file: TFile, signal?: AbortSignal, reporter?: ProgressReporter) {
|
|
if (this.isBusy) {
|
|
new Notice("Notemd is busy.");
|
|
return;
|
|
}
|
|
this.isBusy = true;
|
|
const useReporter = reporter || this.getReporter();
|
|
|
|
const maybeSidebar = useReporter as any;
|
|
if (maybeSidebar instanceof NotemdSidebarView) {
|
|
maybeSidebar.startProcessing("Translating...");
|
|
} else {
|
|
useReporter.clearDisplay();
|
|
useReporter.updateStatus("Translating...", 0);
|
|
}
|
|
|
|
this.updateStatusBar("Translating...");
|
|
|
|
try {
|
|
await this.loadSettings();
|
|
await translateFile(this.app, this.settings, file, this.settings.language, useReporter, true, signal);
|
|
|
|
// Update status and progress on success
|
|
this.updateStatusBar("Translation complete");
|
|
useReporter.log("Translation complete.");
|
|
useReporter.updateStatus("Translation complete", 100);
|
|
|
|
} catch (error: unknown) {
|
|
this.updateStatusBar("Translation failed");
|
|
let errorMessage = 'An unknown error occurred during translation.';
|
|
let errorDetails = String(error);
|
|
if (error instanceof Error) {
|
|
errorMessage = error.message;
|
|
errorDetails = error.stack || error.message;
|
|
}
|
|
if (!errorMessage.includes("cancelled by user")) {
|
|
console.error("Translation Error:", errorDetails);
|
|
new Notice(`Failed to translate file: ${errorMessage}. See console for details.`, 10000);
|
|
new ErrorModal(this.app, "Translation Error", errorDetails).open();
|
|
|
|
// Save error log
|
|
await saveErrorLog(this.app, useReporter, error, this.settings);
|
|
}
|
|
useReporter.log(`Error: ${errorMessage}`);
|
|
useReporter.updateStatus('Error occurred', -1);
|
|
} finally {
|
|
if (maybeSidebar instanceof NotemdSidebarView) {
|
|
maybeSidebar.finishProcessing();
|
|
}
|
|
this.isBusy = false;
|
|
}
|
|
}
|
|
|
|
async summarizeToMermaidCommand(file: TFile, reporter: ProgressReporter) {
|
|
if (this.isBusy) {
|
|
new Notice('Another process is running. Please wait.');
|
|
return;
|
|
}
|
|
this.isBusy = true;
|
|
const useReporter = reporter || this.getReporter();
|
|
|
|
const maybeSidebar = useReporter as any;
|
|
if (maybeSidebar instanceof NotemdSidebarView) {
|
|
maybeSidebar.startProcessing(`Summarizing ${file.name}...`);
|
|
} else {
|
|
useReporter.clearDisplay();
|
|
reporter.log(`Starting Mermaid summarization for ${file.name}...`);
|
|
reporter.updateStatus(`Summarizing ${file.name}...`, 5);
|
|
}
|
|
|
|
try {
|
|
const fileContent = await this.app.vault.read(file);
|
|
if (!fileContent.trim()) {
|
|
throw new Error("File is empty. Cannot summarize.");
|
|
}
|
|
|
|
const { provider, modelName } = this.getProviderAndModelForTask('summarizeToMermaid');
|
|
reporter.log(`Using provider: ${provider.name}, Model: ${modelName}`);
|
|
|
|
const prompt = this.getPromptForTask('summarizeToMermaid');
|
|
|
|
reporter.updateStatus('Calling LLM for summarization...', 20);
|
|
const mermaidContent = await callLLM(provider, prompt, fileContent, this.settings, reporter, modelName);
|
|
reporter.updateStatus('LLM call complete. Processing response...', 90);
|
|
|
|
const outputFilePath = await saveMermaidSummaryFile(this.app, this.settings, file, mermaidContent, reporter);
|
|
|
|
reporter.updateStatus('Mermaid diagram saved successfully!', 100);
|
|
reporter.log(`Mermaid diagram saved to: ${outputFilePath}`);
|
|
new Notice('Mermaid diagram summarization complete!');
|
|
|
|
// Open the new file in a split pane
|
|
if (outputFilePath) {
|
|
const newLeaf = this.app.workspace.getLeaf('split', 'vertical');
|
|
const newFile = this.app.vault.getAbstractFileByPath(outputFilePath);
|
|
if (newFile instanceof TFile) {
|
|
newLeaf.openFile(newFile);
|
|
}
|
|
}
|
|
|
|
} catch (error: any) {
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
reporter.log(`Error during Mermaid summarization: ${message}`);
|
|
reporter.updateStatus('Error during summarization.', -1);
|
|
new Notice(`Summarization Error: ${message}`);
|
|
console.error("Summarization Error:", error);
|
|
|
|
// Save error log
|
|
await saveErrorLog(this.app, reporter, error, this.settings);
|
|
} finally {
|
|
if (maybeSidebar instanceof NotemdSidebarView) {
|
|
maybeSidebar.finishProcessing();
|
|
}
|
|
this.isBusy = false;
|
|
}
|
|
}
|
|
|
|
async extractConceptsCommand(reporter?: ProgressReporter) {
|
|
if (this.isBusy) { new Notice("Notemd is busy."); return; }
|
|
this.isBusy = true;
|
|
const useReporter = reporter || this.getReporter();
|
|
|
|
const maybeSidebar = useReporter as any;
|
|
if (maybeSidebar instanceof NotemdSidebarView) {
|
|
maybeSidebar.startProcessing('Extracting concepts...');
|
|
} else {
|
|
useReporter.clearDisplay();
|
|
useReporter.updateStatus('Extracting concepts...', 0);
|
|
}
|
|
|
|
try {
|
|
await this.loadSettings();
|
|
const activeFile = this.app.workspace.getActiveFile();
|
|
if (!activeFile || !(activeFile instanceof TFile) || (activeFile.extension !== 'md' && activeFile.extension !== 'txt')) {
|
|
throw new Error("No active '.md' or '.txt' file to extract concepts from.");
|
|
}
|
|
|
|
this.updateStatusBar(`Extracting concepts: ${activeFile.name}`);
|
|
|
|
const concepts = await extractConceptsFromFile(this.app, this, activeFile, useReporter);
|
|
|
|
if (concepts.size > 0) {
|
|
useReporter.log(`Found ${concepts.size} concepts. Creating concept notes...`);
|
|
await createConceptNotes(
|
|
this.app,
|
|
this.settings,
|
|
concepts,
|
|
activeFile.basename, // Pass the basename of the active file
|
|
{
|
|
disableBacklink: !this.settings.extractConceptsAddBacklink,
|
|
minimalTemplate: this.settings.extractConceptsMinimalTemplate
|
|
}
|
|
);
|
|
useReporter.updateStatus('Concept extraction complete!', 100);
|
|
new Notice(`Concept extraction complete! Found and created ${concepts.size} concept notes.`);
|
|
} else {
|
|
useReporter.updateStatus('No concepts found.', 100);
|
|
new Notice('No concepts found to extract.');
|
|
}
|
|
|
|
if (useReporter instanceof ProgressModal) setTimeout(() => useReporter.close(), 2000);
|
|
|
|
} catch (error: unknown) {
|
|
this.updateStatusBar('Error occurred');
|
|
let errorMessage = 'An unknown error occurred during concept extraction.';
|
|
if (error instanceof Error) {
|
|
errorMessage = error.message;
|
|
}
|
|
if (!errorMessage.includes("cancelled by user")) {
|
|
new Notice(`Error during concept extraction: ${errorMessage}.`, 10000);
|
|
new ErrorModal(this.app, "Concept Extraction Error", errorMessage).open();
|
|
|
|
// Save error log
|
|
await saveErrorLog(this.app, useReporter, error, this.settings);
|
|
}
|
|
useReporter.log(`Error: ${errorMessage}`);
|
|
useReporter.updateStatus('Error occurred', -1);
|
|
} finally {
|
|
if (maybeSidebar instanceof NotemdSidebarView) {
|
|
maybeSidebar.finishProcessing();
|
|
}
|
|
this.isBusy = false;
|
|
}
|
|
}
|
|
|
|
async batchExtractConceptsForFolderCommand(reporter?: ProgressReporter) {
|
|
if (this.isBusy) { new Notice("Notemd is busy."); return; }
|
|
this.isBusy = true;
|
|
const useReporter = reporter || this.getReporter();
|
|
|
|
const maybeSidebar = useReporter as any;
|
|
if (maybeSidebar instanceof NotemdSidebarView) {
|
|
maybeSidebar.startProcessing('Batch extracting concepts...');
|
|
} else {
|
|
useReporter.clearDisplay();
|
|
useReporter.updateStatus('Batch extracting concepts...', 0);
|
|
}
|
|
|
|
try {
|
|
await this.loadSettings();
|
|
const folderPath = await this.getFolderSelection();
|
|
if (!folderPath) { throw new Error("Folder selection cancelled."); }
|
|
|
|
const folder = this.app.vault.getAbstractFileByPath(folderPath);
|
|
if (!folder || !(folder instanceof TFolder)) throw new Error(`Invalid folder selected: ${folderPath}`);
|
|
|
|
const files = this.app.vault.getFiles().filter(f =>
|
|
(f.extension === 'md' || f.extension === 'txt') &&
|
|
(f.path.startsWith(folderPath === '/' ? '' : folderPath + '/'))
|
|
);
|
|
|
|
if (files.length === 0) {
|
|
new Notice(`No '.md' or '.txt' files found in selected folder: ${folderPath}`);
|
|
return;
|
|
}
|
|
|
|
this.updateStatusBar(`Batch extracting concepts from ${files.length} files...`);
|
|
useReporter.log(`Starting batch concept extraction for ${files.length} files in "${folderPath}"...`);
|
|
const errors: { file: string; message: string }[] = [];
|
|
let totalConcepts = 0;
|
|
|
|
if (!this.settings.enableBatchParallelism || this.settings.batchConcurrency <= 1) {
|
|
for (let i = 0; i < files.length; i++) {
|
|
const file = files[i];
|
|
if (useReporter.cancelled) { new Notice('Batch extraction cancelled.'); break; }
|
|
|
|
const progress = Math.floor(((i) / files.length) * 100);
|
|
useReporter.updateStatus(`Processing ${i + 1}/${files.length}: ${file.name}`, progress);
|
|
|
|
try {
|
|
const concepts = await extractConceptsFromFile(this.app, this, file, useReporter);
|
|
if (concepts.size > 0) {
|
|
totalConcepts += concepts.size;
|
|
await createConceptNotes(
|
|
this.app,
|
|
this.settings,
|
|
concepts,
|
|
file.basename, // Pass the basename of the current file
|
|
{
|
|
disableBacklink: !this.settings.extractConceptsAddBacklink,
|
|
minimalTemplate: this.settings.extractConceptsMinimalTemplate
|
|
}
|
|
);
|
|
}
|
|
} catch (fileError: unknown) {
|
|
const message = fileError instanceof Error ? fileError.message : String(fileError);
|
|
errors.push({ file: file.name, message: message });
|
|
useReporter.log(`❌ Error processing ${file.name}: ${message}`);
|
|
if (message.includes("cancelled by user")) break;
|
|
}
|
|
}
|
|
} else {
|
|
const concurrency = Math.min(this.settings.batchConcurrency, 20); // Cap
|
|
const processor = createConcurrentProcessor(concurrency, this.settings.apiCallIntervalMs, useReporter);
|
|
const fileBatches = chunkArray(files, this.settings.batchSize);
|
|
let processedCount = 0;
|
|
|
|
for (let b = 0; b < fileBatches.length; b++) {
|
|
const batch = fileBatches[b];
|
|
useReporter.log(`Processing batch ${b + 1}/${fileBatches.length} (${batch.length} files)`);
|
|
if (useReporter.cancelled) break;
|
|
|
|
const tasks = batch.map(file => async () => {
|
|
const fileProgressReporter: ProgressReporter = {
|
|
log: (msg: string) => useReporter.log(`[${file.name}] ${msg}`),
|
|
updateStatus: (msg: string, percentage?: number) => {
|
|
if (percentage !== undefined) {
|
|
const overallProgress = Math.floor(((processedCount + (percentage / 100)) / files.length) * 100);
|
|
useReporter.updateStatus(`Batch: ${processedCount}/${files.length} (${file.name}: ${msg})`, overallProgress);
|
|
} else {
|
|
useReporter.updateStatus(`Batch: ${processedCount}/${files.length} (${file.name}: ${msg})`);
|
|
}
|
|
},
|
|
cancelled: useReporter.cancelled,
|
|
requestCancel: () => useReporter.requestCancel(),
|
|
clearDisplay: () => { },
|
|
abortController: useReporter.abortController,
|
|
activeTasks: useReporter.activeTasks,
|
|
updateActiveTasks: (delta: number) => useReporter.updateActiveTasks(delta),
|
|
};
|
|
|
|
try {
|
|
const concepts = await extractConceptsFromFile(this.app, this, file, fileProgressReporter);
|
|
if (concepts.size > 0) {
|
|
totalConcepts += concepts.size;
|
|
await createConceptNotes(
|
|
this.app,
|
|
this.settings,
|
|
concepts,
|
|
file.basename, // Pass the basename of the current file
|
|
{
|
|
disableBacklink: !this.settings.extractConceptsAddBacklink,
|
|
minimalTemplate: this.settings.extractConceptsMinimalTemplate
|
|
}
|
|
);
|
|
}
|
|
return { file, success: true };
|
|
} catch (e: unknown) {
|
|
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
fileProgressReporter.log(`❌ Error processing ${file.name}: ${errorMessage}`);
|
|
return { file, success: false, error: e };
|
|
}
|
|
});
|
|
|
|
const results = await processor(tasks);
|
|
processedCount += batch.length;
|
|
|
|
results.forEach(r => {
|
|
const result = r as { success: boolean; file: TFile; error?: any };
|
|
if (!result.success && result.error) {
|
|
const errorMessage = result.error.message || String(result.error);
|
|
errors.push({ file: result.file.name, message: errorMessage });
|
|
}
|
|
});
|
|
|
|
if (useReporter.cancelled) {
|
|
useReporter.log('Cancellation requested, stopping batch processing.');
|
|
break;
|
|
}
|
|
|
|
if (this.settings.batchInterDelayMs > 0 && b < fileBatches.length - 1) {
|
|
useReporter.log(`Delaying for ${this.settings.batchInterDelayMs}ms before next batch...`);
|
|
await delay(this.settings.batchInterDelayMs);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
if (!useReporter.cancelled) {
|
|
if (errors.length > 0) {
|
|
const errorSummary = `Batch extraction finished with ${errors.length} error(s).`;
|
|
useReporter.log(`⚠️ ${errorSummary}`);
|
|
useReporter.updateStatus(errorSummary, -1);
|
|
new Notice(errorSummary, 10000);
|
|
} else {
|
|
const successMessage = `Batch extraction complete! Found ${totalConcepts} concepts across ${files.length} files.`;
|
|
useReporter.updateStatus(successMessage, 100);
|
|
new Notice(successMessage);
|
|
if (useReporter instanceof ProgressModal) setTimeout(() => useReporter.close(), 2000);
|
|
}
|
|
}
|
|
|
|
} catch (error: unknown) {
|
|
this.updateStatusBar('Error occurred');
|
|
let errorMessage = 'An unknown error occurred during batch extraction.';
|
|
if (error instanceof Error) {
|
|
errorMessage = error.message;
|
|
}
|
|
if (!errorMessage.includes("cancelled")) {
|
|
new Notice(`Error during batch extraction: ${errorMessage}.`, 10000);
|
|
new ErrorModal(this.app, "Batch Concept Extraction Error", errorMessage).open();
|
|
|
|
// Save error log
|
|
await saveErrorLog(this.app, useReporter, error, this.settings);
|
|
}
|
|
useReporter.log(`Batch Error: ${errorMessage}`);
|
|
useReporter.updateStatus('Error occurred', -1);
|
|
} finally {
|
|
if (maybeSidebar instanceof NotemdSidebarView) {
|
|
maybeSidebar.finishProcessing();
|
|
}
|
|
this.isBusy = false;
|
|
}
|
|
}
|
|
|
|
async extractConceptsAndGenerateTitlesCommand(reporter?: ProgressReporter) {
|
|
if (this.isBusy) {
|
|
new Notice("Notemd is busy.");
|
|
return;
|
|
}
|
|
this.isBusy = true;
|
|
const useReporter = reporter || this.getReporter();
|
|
|
|
const maybeSidebar = useReporter as any;
|
|
if (maybeSidebar instanceof NotemdSidebarView) {
|
|
maybeSidebar.startProcessing("Extracting concepts and generating titles...");
|
|
} else {
|
|
useReporter.clearDisplay();
|
|
useReporter.updateStatus("Extracting concepts and generating titles...", 0);
|
|
}
|
|
|
|
try {
|
|
await this.extractConceptsCommand(useReporter);
|
|
|
|
if (this.settings.useCustomConceptNoteFolder && this.settings.conceptNoteFolder) {
|
|
const conceptFolder = this.app.vault.getAbstractFileByPath(this.settings.conceptNoteFolder);
|
|
if (conceptFolder instanceof TFolder) {
|
|
await this.batchGenerateContentForTitlesCommand(useReporter);
|
|
} else {
|
|
throw new Error("Concept note folder not found.");
|
|
}
|
|
} else {
|
|
throw new Error("Concept note folder not set.");
|
|
}
|
|
|
|
} catch (error: unknown) {
|
|
this.updateStatusBar("Error occurred");
|
|
let errorMessage = 'An unknown error occurred during the process.';
|
|
if (error instanceof Error) {
|
|
errorMessage = error.message;
|
|
}
|
|
if (!errorMessage.includes("cancelled")) {
|
|
new Notice(`Error: ${errorMessage}. See console for details.`, 10000);
|
|
new ErrorModal(this.app, "Error", errorMessage).open();
|
|
|
|
// Save error log
|
|
await saveErrorLog(this.app, useReporter, error, this.settings);
|
|
}
|
|
useReporter.log(`Error: ${errorMessage}`);
|
|
useReporter.updateStatus('Error occurred', -1);
|
|
} finally {
|
|
if (maybeSidebar instanceof NotemdSidebarView) {
|
|
maybeSidebar.finishProcessing();
|
|
}
|
|
this.isBusy = false;
|
|
}
|
|
}
|
|
|
|
async extractOriginalTextCommand(reporter?: ProgressReporter) {
|
|
if (this.isBusy) {
|
|
new Notice("Notemd is busy.");
|
|
return;
|
|
}
|
|
this.isBusy = true;
|
|
const useReporter = reporter || this.getReporter();
|
|
|
|
const maybeSidebar = useReporter as any;
|
|
if (maybeSidebar instanceof NotemdSidebarView) {
|
|
maybeSidebar.startProcessing("Extracting specific original text...");
|
|
} else {
|
|
useReporter.clearDisplay();
|
|
useReporter.updateStatus("Extracting specific original text...", 0);
|
|
}
|
|
|
|
try {
|
|
await this.loadSettings();
|
|
const activeFile = this.app.workspace.getActiveFile();
|
|
if (!activeFile || !(activeFile instanceof TFile) || (activeFile.extension !== 'md' && activeFile.extension !== 'txt')) {
|
|
throw new Error("No active '.md' or '.txt' file to process.");
|
|
}
|
|
|
|
await extractOriginalText(this.app, this, activeFile, useReporter);
|
|
|
|
useReporter.updateStatus('Extraction complete!', 100);
|
|
|
|
} catch (error: unknown) {
|
|
this.updateStatusBar("Error occurred");
|
|
let errorMessage = 'An unknown error occurred during extraction.';
|
|
if (error instanceof Error) {
|
|
errorMessage = error.message;
|
|
}
|
|
if (!errorMessage.includes("cancelled")) {
|
|
new Notice(`Error: ${errorMessage}. See console for details.`, 10000);
|
|
new ErrorModal(this.app, "Extraction Error", errorMessage).open();
|
|
|
|
// Save error log
|
|
await saveErrorLog(this.app, useReporter, error, this.settings);
|
|
}
|
|
useReporter.log(`Error: ${errorMessage}`);
|
|
useReporter.updateStatus('Error occurred', -1);
|
|
} finally {
|
|
if (maybeSidebar instanceof NotemdSidebarView) {
|
|
maybeSidebar.finishProcessing();
|
|
}
|
|
this.isBusy = false;
|
|
}
|
|
}
|
|
|
|
} // End of NotemdPlugin class
|
|
|