feat: Refine notices and error handling, introduce UI constants.

This commit is contained in:
dralkh 2026-01-01 12:14:46 +03:00
parent a5433e047d
commit 668fd66876
19 changed files with 243 additions and 216 deletions

View file

@ -1,4 +1,5 @@
import { Notice, requestUrl } from 'obsidian';
import { CLAUDE, MCQS, MCQ, API } from '../ui/constants';
import SpaceforgePlugin from '../main';
import { MCQQuestion, MCQSet } from '../models/mcq';
import { IMCQGenerationService } from './mcq-generation-service';
@ -13,16 +14,16 @@ export class ClaudeService implements IMCQGenerationService {
async generateMCQs(notePath: string, noteContent: string, settings: SpaceforgeSettings): Promise<MCQSet | null> {
if (!settings.claudeApiKey) {
new Notice('Claude API key is not set. Please add it in the Spaceforge settings');
new Notice(`${CLAUDE} ${API} key is not set. Please add it in the settings`);
return null;
}
if (!settings.claudeModel) {
new Notice('Claude model is not set. Please add it in the Spaceforge settings');
new Notice(`${CLAUDE} model is not set. Please add it in the settings`);
return null;
}
try {
new Notice('Generating MCQs using Claude...');
new Notice(`Generating ${MCQS} using ${CLAUDE}...`);
// Determine the number of questions to generate
let numQuestionsToGenerate: number;
@ -38,7 +39,7 @@ export class ClaudeService implements IMCQGenerationService {
const questions = this.parseResponse(response, settings, numQuestionsToGenerate);
if (questions.length === 0) {
new Notice('Failed to generate valid MCQs from Claude. Please try again');
new Notice(`Failed to generate valid ${MCQS} from ${CLAUDE}. Please try again`);
return null;
}
@ -47,8 +48,9 @@ export class ClaudeService implements IMCQGenerationService {
questions,
generatedAt: Date.now()
};
} catch {
new Notice('Failed to generate MCQs with Claude. Please check console for details');
} catch (error) {
console.error(`Error generating ${MCQS} with ${CLAUDE}:`, error);
new Notice(`Failed to generate ${MCQS} with ${CLAUDE}. Please check console for details`);
return null;
}
}
@ -222,7 +224,7 @@ export class ClaudeService implements IMCQGenerationService {
}
return questions.slice(0, numQuestionsToGenerate); // Use calculated number
} catch {
new Notice('Error parsing MCQ response from Claude. Please try again');
new Notice(`Error parsing ${MCQ} response from ${CLAUDE}. Please try again`);
return [];
}
}

View file

@ -1,4 +1,5 @@
import { Notice, requestUrl } from 'obsidian';
import { GEMINI, MCQS, MCQ, API } from '../ui/constants';
import SpaceforgePlugin from '../main';
import { MCQQuestion, MCQSet } from '../models/mcq';
import { IMCQGenerationService } from './mcq-generation-service';
@ -15,16 +16,16 @@ export class GeminiService implements IMCQGenerationService {
async generateMCQs(notePath: string, noteContent: string, settings: SpaceforgeSettings): Promise<MCQSet | null> {
if (!settings.geminiApiKey) {
new Notice('Gemini API key is not set. Please add it in the Spaceforge settings');
new Notice(`${GEMINI} ${API} key is not set. Please add it in the settings`);
return null;
}
if (!settings.geminiModel) {
new Notice('Gemini model is not set. Please add it in the Spaceforge settings');
new Notice(`${GEMINI} model is not set. Please add it in the settings`);
return null;
}
try {
new Notice('Generating MCQs using Gemini...');
new Notice(`Generating ${MCQS} using ${GEMINI}...`);
// Determine the number of questions to generate
let numQuestionsToGenerate: number;
@ -40,7 +41,7 @@ export class GeminiService implements IMCQGenerationService {
const questions = this.parseResponse(response, settings, numQuestionsToGenerate);
if (questions.length === 0) {
new Notice('Failed to generate valid MCQs from Gemini. Please try again');
new Notice(`Failed to generate valid ${MCQS} from ${GEMINI}. Please try again`);
return null;
}
@ -50,7 +51,7 @@ export class GeminiService implements IMCQGenerationService {
generatedAt: Date.now()
};
} catch {
new Notice('Failed to generate MCQs with Gemini. Please check console for details');
new Notice(`Failed to generate ${MCQS} with ${GEMINI}. Please check console for details`);
return null;
}
}
@ -174,7 +175,7 @@ export class GeminiService implements IMCQGenerationService {
}
return questions.slice(0, numQuestionsToGenerate); // Use calculated number
} catch {
new Notice('Error parsing MCQ response from Gemini. Please try again');
new Notice(`Error parsing ${MCQ} response from ${GEMINI}. Please try again`);
return [];
}
}

View file

@ -1,4 +1,5 @@
import { Notice, requestUrl } from 'obsidian';
import { OLLAMA, API } from '../ui/constants';
import SpaceforgePlugin from '../main';
import { MCQQuestion, MCQSet } from '../models/mcq';
import { IMCQGenerationService } from './mcq-generation-service';
@ -13,16 +14,16 @@ export class OllamaService implements IMCQGenerationService {
async generateMCQs(notePath: string, noteContent: string, settings: SpaceforgeSettings): Promise<MCQSet | null> {
if (!settings.ollamaApiUrl) {
new Notice('Ollama API URL is not set. Please add it in the Spaceforge settings');
new Notice(`${OLLAMA} ${API} ${URL} not set in settings.`);
return null;
}
if (!settings.ollamaModel) {
new Notice('Ollama model is not set. Please add it in the Spaceforge settings');
new Notice(`${OLLAMA} model not set in settings.`);
return null;
}
try {
new Notice('Generating MCQs using Ollama...');
new Notice(`Generating questions using ${OLLAMA}...`);
// Determine the number of questions to generate
let numQuestionsToGenerate: number;
@ -38,7 +39,7 @@ export class OllamaService implements IMCQGenerationService {
const questions = this.parseResponse(response, settings, numQuestionsToGenerate);
if (questions.length === 0) {
new Notice('Failed to generate valid MCQs from Ollama. Please try again');
new Notice(`Failed to generate valid questions from ${OLLAMA}. Try again.`);
return null;
}
@ -48,7 +49,7 @@ export class OllamaService implements IMCQGenerationService {
generatedAt: Date.now()
};
} catch {
new Notice('Failed to generate MCQs with Ollama. Please check console for details');
new Notice(`Failed to generate questions with ${OLLAMA}. Check console for details.`);
return null;
}
}
@ -171,7 +172,7 @@ export class OllamaService implements IMCQGenerationService {
}
return questions.slice(0, numQuestionsToGenerate); // Use calculated number
} catch {
new Notice('Error parsing MCQ response from Ollama. Please try again');
new Notice(`Error parsing response from ${OLLAMA}. Try again.`);
return [];
}
}

View file

@ -1,4 +1,5 @@
import { Notice, requestUrl } from 'obsidian';
import { OPENAI, API } from '../ui/constants';
import SpaceforgePlugin from '../main';
import { MCQQuestion, MCQSet } from '../models/mcq';
import { IMCQGenerationService } from './mcq-generation-service';
@ -13,16 +14,16 @@ export class OpenAIService implements IMCQGenerationService {
async generateMCQs(notePath: string, noteContent: string, settings: SpaceforgeSettings): Promise<MCQSet | null> {
if (!settings.openaiApiKey) {
new Notice('OpenAI API key is not set. Please add it in the Spaceforge settings');
new Notice(`${OPENAI} ${API} key not set in settings.`);
return null;
}
if (!settings.openaiModel) {
new Notice('OpenAI model is not set. Please add it in the Spaceforge settings');
new Notice(`${OPENAI} model not set in settings.`);
return null;
}
try {
new Notice('Generating MCQs using OpenAI...');
new Notice(`Generating questions using ${OPENAI}...`);
// Determine the number of questions to generate
let numQuestionsToGenerate: number;
@ -38,7 +39,7 @@ export class OpenAIService implements IMCQGenerationService {
const questions = this.parseResponse(response, settings, numQuestionsToGenerate);
if (questions.length === 0) {
new Notice('Failed to generate valid MCQs from OpenAI, please try again');
new Notice(`Failed to generate valid questions from ${OPENAI}. Try again.`);
return null;
}
@ -48,7 +49,7 @@ export class OpenAIService implements IMCQGenerationService {
generatedAt: Date.now()
};
} catch {
new Notice('Failed to generate MCQs with OpenAI, please check console for details');
new Notice(`Failed to generate questions with ${OPENAI}. Check console for details.`);
return null;
}
}
@ -170,7 +171,7 @@ export class OpenAIService implements IMCQGenerationService {
}
return questions.slice(0, numQuestionsToGenerate); // Use calculated number
} catch {
new Notice('Error parsing MCQ response from OpenAI. Please try again');
new Notice(`Error parsing response from ${OPENAI}. Try again.`);
return [];
}
}

View file

@ -1,4 +1,5 @@
import { Notice, requestUrl } from 'obsidian';
import { OPENROUTER, API } from '../ui/constants';
import SpaceforgePlugin from '../main';
import { MCQQuestion, MCQSet } from '../models/mcq';
import { IMCQGenerationService } from './mcq-generation-service';
@ -33,13 +34,13 @@ export class OpenRouterService implements IMCQGenerationService {
async generateMCQs(notePath: string, noteContent: string, settings: SpaceforgeSettings): Promise<MCQSet | null> {
// Check if API key is set
if (!settings.openRouterApiKey) {
new Notice('OpenRouter API key is not set. Please add it in the settings');
new Notice(`${OPENROUTER} ${API} key not set in settings.`);
return null;
}
try {
// Show loading notice
new Notice('Generating MCQs using OpenRouter...');
new Notice(`Generating questions using ${OPENROUTER}...`);
// Determine the number of questions to generate
let numQuestionsToGenerate: number;
@ -63,7 +64,7 @@ export class OpenRouterService implements IMCQGenerationService {
// Ensure we have at least one question if requested
if (questions.length === 0) {
new Notice('Failed to generate valid MCQs from OpenRouter. Please try again');
new Notice('Failed to generate valid questions. Try again.');
return null;
}
@ -76,7 +77,7 @@ export class OpenRouterService implements IMCQGenerationService {
return mcqSet;
} catch {
new Notice('Failed to generate MCQs with OpenRouter. Please check console for details');
new Notice('Failed to generate questions. Check console for details.');
return null;
}
}
@ -294,7 +295,7 @@ For example:
// Use numQuestionsToGenerate instead of settings.mcqQuestionsPerNote
return questions.slice(0, numQuestionsToGenerate);
} catch {
new Notice('Error parsing MCQ response from OpenRouter. Please try again');
new Notice(`Error parsing response from ${OPENROUTER}. Try again.`);
return [];
}
}

View file

@ -1,4 +1,5 @@
import { Notice, requestUrl } from 'obsidian';
import { TOGETHER_AI, API } from '../ui/constants';
import SpaceforgePlugin from '../main';
import { MCQQuestion, MCQSet } from '../models/mcq';
import { IMCQGenerationService } from './mcq-generation-service';
@ -13,16 +14,16 @@ export class TogetherService implements IMCQGenerationService {
async generateMCQs(notePath: string, noteContent: string, settings: SpaceforgeSettings): Promise<MCQSet | null> {
if (!settings.togetherApiKey) {
new Notice('Together AI API key is not set. Please add it in the Spaceforge settings');
new Notice(`${TOGETHER_AI} ${API} key not set in settings.`);
return null;
}
if (!settings.togetherModel) {
new Notice('Together AI model is not set. Please add it in the Spaceforge settings');
new Notice(`${TOGETHER_AI} model not set in settings.`);
return null;
}
try {
new Notice('Generating MCQs using Together AI...');
new Notice(`Generating questions using ${TOGETHER_AI}...`);
// Determine the number of questions to generate
let numQuestionsToGenerate: number;
@ -38,7 +39,7 @@ export class TogetherService implements IMCQGenerationService {
const questions = this.parseResponse(response, settings, numQuestionsToGenerate);
if (questions.length === 0) {
new Notice('Failed to generate valid MCQs from Together AI. Please try again');
new Notice('Failed to generate valid questions. Try again.');
return null;
}
@ -48,7 +49,7 @@ export class TogetherService implements IMCQGenerationService {
generatedAt: Date.now()
};
} catch {
new Notice('Failed to generate MCQs with Together AI. Please check console for details');
new Notice('Failed to generate questions. Check console for details.');
return null;
}
}
@ -200,7 +201,7 @@ export class TogetherService implements IMCQGenerationService {
}
return questions.slice(0, numQuestionsToGenerate); // Use calculated number
} catch {
new Notice('Error parsing MCQ response from Together AI. Please try again');
new Notice(`Error parsing response from ${TOGETHER_AI}. Try again.`);
return [];
}
}

View file

@ -33,7 +33,7 @@ export class ReviewBatchController implements IReviewBatchController {
const todayNotes = reviewController.getTodayNotes();
if (todayNotes.length === 0) {
new Notice("No notes due for review today!");
new Notice("No notes due for review today.");
return;
}
@ -134,7 +134,7 @@ export class ReviewBatchController implements IReviewBatchController {
}
if (!this.plugin.mcqController) {
new Notice("MCQ controller not initialized. Please check MCQ settings");
new Notice("Questions feature not initialized. Check settings.");
return;
}

View file

@ -68,12 +68,12 @@ export class MCQController {
externalOnCompleteCallback?: (path: string, success: boolean) => void
): Promise<void> {
if (!this.plugin.settings.enableMCQ) {
new Notice('MCQ feature is disabled in settings.');
new Notice('Feature is disabled in settings.');
if (externalOnCompleteCallback) externalOnCompleteCallback(notePath, false);
return;
}
if (!this.mcqGenerationService) {
new Notice('MCQ generation service is not available. Check API provider settings.');
new Notice('Generation service is not available. Check API provider settings.');
return;
}
@ -81,23 +81,23 @@ export class MCQController {
let mcqSet = this.mcqService.getMCQSetForNote(notePath);
if (mcqSet && mcqSet.needsQuestionRegeneration) {
new Notice('Questions for this note are flagged for regeneration. Generating new set...');
new Notice('Questions flagged for regeneration. Generating new set...');
mcqSet = await this.generateMCQs(notePath, true);
if (mcqSet) {
mcqSet.needsQuestionRegeneration = false;
this.mcqService.saveMCQSet(mcqSet);
await this.plugin.savePluginData();
} else {
new Notice('Failed to regenerate MCQs. Using existing set if available.');
new Notice('Failed to regenerate. Using existing set if available.');
mcqSet = this.mcqService.getMCQSetForNote(notePath);
}
}
if (!mcqSet || mcqSet.questions.length === 0) {
new Notice('No MCQs found for this note. Generating new set...');
new Notice('No questions found. Generating new set...');
mcqSet = await this.generateMCQs(notePath);
if (!mcqSet) {
new Notice('Failed to generate MCQs for this note.');
new Notice('Failed to generate questions.');
return;
}
}
@ -135,7 +135,7 @@ export class MCQController {
// Pass the new internal callback to the modal constructor
new MCQModal(this.plugin, notePath, mcqSet, internalOnComplete).open();
} catch {
new Notice('Error starting MCQ review. Please check console for details.');
new Notice('Error starting review. Please check console for details.');
if (externalOnCompleteCallback) externalOnCompleteCallback(notePath, false);
}
}
@ -149,7 +149,7 @@ export class MCQController {
*/
async generateMCQs(notePath: string, forceRegeneration = false): Promise<MCQSet | null> {
if (!this.plugin.settings.enableMCQ || !this.mcqGenerationService) {
new Notice('MCQ feature is disabled or the generation service is not available. Check API provider settings.');
new Notice('Feature is disabled or the generation service is not available. Check API provider settings.');
return null;
}
@ -202,11 +202,11 @@ export class MCQController {
*/
async startConsolidatedMCQReviewForSelectedDate(): Promise<void> {
if (!this.plugin.settings.enableMCQ) {
new Notice('MCQ feature is disabled in settings.');
new Notice('Feature is disabled in settings.');
return;
}
if (!this.mcqGenerationService) {
new Notice('MCQ generation service is not available. Check API provider settings.');
new Notice('Generation service is not available. Check API provider settings.');
return;
}
@ -220,7 +220,7 @@ export class MCQController {
const mcqSetsForReview: { path: string; mcqSet: MCQSet; fileName: string }[] = [];
let notesWithMCQsCount = 0;
new Notice(`Fetching MCQs for ${dueNotes.length} due note(s)...`);
new Notice(`Fetching questions for ${dueNotes.length} due note(s)...`);
for (const noteSchedule of dueNotes) {
const notePath = noteSchedule.path;
@ -228,7 +228,7 @@ export class MCQController {
// If MCQ set needs regeneration, attempt to regenerate it.
if (mcqSet && mcqSet.needsQuestionRegeneration) {
new Notice(`Regenerating flagged MCQs for ${notePath}...`);
new Notice(`Regenerating flagged questions for ${notePath}...`);
const regeneratedMcqSet = await this.generateMCQs(notePath, true); // forceRegeneration = true
if (regeneratedMcqSet) {
mcqSet = regeneratedMcqSet; // Use the new set
@ -236,19 +236,19 @@ export class MCQController {
this.mcqService.saveMCQSet(mcqSet);
// Overall plugin data save will happen once after the loop
} else {
new Notice(`Failed to regenerate MCQs for ${notePath}.Using existing set if available(might be outdated or empty).`);
new Notice(`Failed to regenerate questions for ${notePath}. Using existing set if available (might be outdated or empty).`);
// mcqSet remains the old one, or null if it didn't exist.
}
}
// If no MCQ set exists after potential regeneration, or if it's empty, try to generate one.
if (!mcqSet || mcqSet.questions.length === 0) {
new Notice(`No MCQs found or set is empty for ${notePath}.Attempting to generate new set...`);
new Notice(`No questions found or set is empty for ${notePath}. Attempting to generate new set...`);
const newMcqSet = await this.generateMCQs(notePath, false); // forceRegeneration = false (respects cache if fresh)
if (newMcqSet) {
mcqSet = newMcqSet;
} else {
new Notice(`Failed to generate MCQs for ${notePath}.This note will be skipped in MCQ review.`);
new Notice(`Failed to generate questions for ${notePath}. This note will be skipped.`);
}
}
@ -264,13 +264,13 @@ export class MCQController {
}
if (mcqSetsForReview.length === 0) {
new Notice('No MCQs available or generated for the due notes.');
new Notice('No questions available for the due notes.');
return;
}
await this.plugin.savePluginData(); // Save any changes from MCQ generation
new Notice(`Starting consolidated MCQ review for ${notesWithMCQsCount} note(s) with ${mcqSetsForReview.reduce((sum, s) => sum + s.mcqSet.questions.length, 0)} questions.`);
new Notice(`Starting consolidated review for ${notesWithMCQsCount} note(s) with ${mcqSetsForReview.reduce((sum, s) => sum + s.mcqSet.questions.length, 0)} questions.`);
const onConsolidatedComplete = (
results: Array<{ path: string; success: boolean; response: ReviewResponse; score?: number }>
@ -295,7 +295,7 @@ export class MCQController {
if (reviewsProcessed > 0) {
new Notice(`${reviewsProcessed} note review(s) updated based on consolidated MCQ session.`);
} else {
new Notice("No note reviews were updated from the MCQ session");
new Notice("No reviews updated.");
}
})();
};

View file

@ -139,8 +139,8 @@ export class ReviewController implements IReviewController {
*
* @param path Path to the postponed note
*/
async handleNotePostponed(path: string): Promise<void> {
await this.coreController.handleNotePostponed(path);
handleNotePostponed(path: string): void {
this.coreController.handleNotePostponed(path);
}
/**

49
main.ts
View file

@ -145,7 +145,7 @@ export default class SpaceforgePlugin extends Plugin {
await this.savePluginData();
new Notice(`Added "${selectedFile.path}" to review schedule.`);
} else {
new Notice("Selected item is not a markdown file.");
new Notice("Selected item is not a Markdown file.");
}
} else {
new Notice("No file selected in file explorer");
@ -236,7 +236,7 @@ export default class SpaceforgePlugin extends Plugin {
}
// @typescript-eslint/no-misused-promises -- onunload can be async
async onunload(): Promise<void> {
onunload(): void {
if (this.cssHotReloadIntervalId !== null) {
window.clearInterval(this.cssHotReloadIntervalId);
this.cssHotReloadIntervalId = null;
@ -251,10 +251,11 @@ export default class SpaceforgePlugin extends Plugin {
let existingData: Partial<SpaceforgePluginData> = {};
try {
const loaded = await this.loadData();
if (loaded) {
existingData = loaded;
}
this.loadData().then(loaded => {
if (loaded) {
existingData = loaded;
}
}).catch(() => { /* handle error */ });
}
catch { /* handle error */ }
@ -275,7 +276,7 @@ export default class SpaceforgePlugin extends Plugin {
} catch { /* handle error */ }
try {
await this.savePluginData();
this.savePluginData().catch(e => console.error("Error saving data on unload:", e));
} catch { /* handle error */ }
if (this.pomodoroService) this.pomodoroService.destroy();
@ -444,7 +445,7 @@ export default class SpaceforgePlugin extends Plugin {
if (parsedBackup.reviewData) {
this.settings = { ...DEFAULT_SETTINGS, ...parsedBackup.settings };
this.pluginState = { ...DEFAULT_PLUGIN_STATE_DATA, ...parsedBackup.reviewData };
new Notice("Spaceforge: Recovered data from backup", 5000);
new Notice("Recovered data from backup", 5000);
} else {
throw new Error("Invalid backup format");
}
@ -455,7 +456,7 @@ export default class SpaceforgePlugin extends Plugin {
console.warn("Spaceforge: Backup recovery failed, using defaults:", backupError);
this.settings = { ...DEFAULT_SETTINGS };
this.pluginState = { ...DEFAULT_PLUGIN_STATE_DATA };
new Notice("Spaceforge: Error loading data, using defaults to prevent crash", 5000);
new Notice("Error loading data, using defaults to prevent crash", 5000);
}
// Repopulate services
@ -526,7 +527,7 @@ export default class SpaceforgePlugin extends Plugin {
const dirPathOnly = effectiveSavePath.substring(0, effectiveSavePath.lastIndexOf('/'));
if (dirPathOnly && !this.app.vault.getAbstractFileByPath(dirPathOnly)) {
await this.app.vault.createFolder(dirPathOnly);
new Notice(`Spaceforge: Created directory for custom data: ${dirPathOnly}`, 3000);
new Notice(`Created directory for custom data: ${dirPathOnly}`, 3000);
}
// SIMPLE SAVE: Use Obsidian's standard file operations
@ -545,16 +546,16 @@ export default class SpaceforgePlugin extends Plugin {
if (oldFile instanceof TFile) {
// Instead of deleting, we'll keep the old file as a backup
// Users can manually clean it up later if needed
new Notice(`Spaceforge: Successfully saved to custom path, original data file kept as backup for safety`, 5000);
new Notice(`Successfully saved to custom path, original data file kept as backup for safety`, 5000);
}
} catch (writeError) {
new Notice(`Error saving data to custom path ${effectiveSavePath}: ${writeError.message}. Falling back to default path for this save.`, 10000);
// Fallback save to default location if custom path write fails
try {
await this.saveData(dataToSave); // Plugin's internal save
new Notice(`Spaceforge: Data saved to default plugin folder due to error with custom path`, 5000);
new Notice(`Data saved to default plugin folder due to error with custom path`, 5000);
} catch {
new Notice(`CRITICAL: Spaceforge failed to save data to both custom and default locations`, 10000);
new Notice(`Critical error: failed to save data to both custom and default locations`, 10000);
}
}
} else {
@ -567,7 +568,7 @@ export default class SpaceforgePlugin extends Plugin {
// If user wants to backup settings, they can use the export feature.
} catch {
new Notice("Error saving Spaceforge data. Check console for details", 5000);
new Notice("Error saving data. Check console for details", 5000);
}
}
@ -586,7 +587,7 @@ export default class SpaceforgePlugin extends Plugin {
});
void this.app.workspace.revealLeaf(leaf); // Reveal the newly created leaf
} else {
new Notice("Spaceforge: Could not open sidebar view");
new Notice("Could not open sidebar view");
}
}
}
@ -616,7 +617,7 @@ export default class SpaceforgePlugin extends Plugin {
if (activeFile && activeFile instanceof TFile && activeFile.extension === 'md') {
void this.reviewController.reviewNote(activeFile.path);
} else {
new Notice('No active markdown file to review');
new Notice('No active Markdown file to review');
}
},
});
@ -631,7 +632,7 @@ export default class SpaceforgePlugin extends Plugin {
await this.savePluginData();
new Notice(`Added "${activeFile.path}" to review schedule`);
} else {
new Notice('No active markdown file to add to review');
new Notice('No active Markdown file to add to review');
}
},
});
@ -664,7 +665,7 @@ export default class SpaceforgePlugin extends Plugin {
// Pass the mcqService for data management, and the new mcqGenerationService for API calls
this.mcqController = new MCQController(this, this.mcqService, this.mcqGenerationService);
} else {
new Notice('MCQ generation service could not be initialized. Check API provider settings in Spaceforge settings');
new Notice('Generation service could not be initialized. Check API provider settings in settings');
}
}
}
@ -673,37 +674,37 @@ export default class SpaceforgePlugin extends Plugin {
switch (this.settings.mcqApiProvider) {
case ApiProvider.OpenRouter:
if (!this.settings.openRouterApiKey) {
new Notice('OpenRouter API key is not set in Spaceforge settings');
new Notice('OpenRouter API key is not set in settings');
return undefined;
}
return new OpenRouterService(this);
case ApiProvider.OpenAI:
if (!this.settings.openaiApiKey) {
new Notice('OpenAI API key is not set in Spaceforge settings');
new Notice('OpenAI API key is not set in settings');
return undefined;
}
return new OpenAIService(this);
case ApiProvider.Ollama:
if (!this.settings.ollamaApiUrl || !this.settings.ollamaModel) {
new Notice('Ollama API URL or model is not set in Spaceforge settings');
new Notice('Ollama API URL or model is not set in settings');
return undefined;
}
return new OllamaService(this);
case ApiProvider.Gemini:
if (!this.settings.geminiApiKey) {
new Notice('Gemini API key is not set in Spaceforge settings');
new Notice('Gemini API key is not set in settings');
return undefined;
}
return new GeminiService(this);
case ApiProvider.Claude:
if (!this.settings.claudeApiKey || !this.settings.claudeModel) {
new Notice('Claude API key or model is not set in Spaceforge settings');
new Notice('Claude API key or model is not set in settings');
return undefined;
}
return new ClaudeService(this);
case ApiProvider.Together:
if (!this.settings.togetherApiKey || !this.settings.togetherModel) {
new Notice('Together AI API key or model is not set in Spaceforge settings');
new Notice('Together AI API key or model is not set in settings');
return undefined;
}
return new TogetherService(this);

View file

@ -76,7 +76,7 @@ export class ReviewScheduleService {
// Check if file exists and is a markdown file
const file = this.plugin.app.vault.getAbstractFileByPath(path);
if (!file || !(file instanceof TFile) || file.extension !== "md") {
new Notice("Only markdown files can be added to the review schedule.");
new Notice("Only Markdown files can be added to review.");
return;
}
@ -143,7 +143,7 @@ export class ReviewScheduleService {
this.plugin.events.emit('sidebar-update');
}
new Notice(`Note added to review schedule.`);
new Notice(`Note added to review.`);
}
/**
@ -708,7 +708,7 @@ export class ReviewScheduleService {
this.customNoteOrder = this.customNoteOrder.filter(p => p !== path);
// Data saving is now handled by main.ts after this method returns
new Notice("Note removed from review schedule.");
new Notice("Note removed from review.");
// Note: The controller will be notified separately to update its state
// This prevents immediate reordering based on link analysis after removal.
@ -726,7 +726,7 @@ export class ReviewScheduleService {
this.schedules = {};
this.customNoteOrder = []; // Also clear custom order
// Data saving is now handled by main.ts after this method returns
new Notice("All review schedules have been cleared.");
new Notice("All review schedules cleared.");
// Notify any listeners (for UI updates)
if (this.plugin.events) {

View file

@ -170,12 +170,12 @@ export class BatchReviewModal extends Modal {
);
consolidatedModal.open();
} catch {
new Notice("Error showing MCQ review. Falling back to manual review.");
new Notice("Error showing review. Falling back to manual review.");
this.open();
void this.processNextManual();
}
} else {
new Notice("MCQ controller not available. Falling back to manual review.");
new Notice("Controller not available. Falling back to manual review.");
this.open();
void this.processNextManual();
}
@ -196,7 +196,7 @@ export class BatchReviewModal extends Modal {
const note = this.notes[this.currentIndex];
const { contentEl } = this;
contentEl.empty();
new Setting(contentEl).setName("MCQ review in progress").setHeading();
new Setting(contentEl).setName("Review in progress").setHeading();
const progressEl = contentEl.createDiv("batch-review-progress");
progressEl.createEl("p", { text: `Processing note ${this.currentIndex + 1}/${this.notes.length}` });
const file = this.plugin.app.vault.getAbstractFileByPath(note.path);
@ -252,7 +252,7 @@ export class BatchReviewModal extends Modal {
void this.processNextManual();
}
} else {
new Notice("MCQ controller not available, falling back to manual review.");
new Notice("Controller not available, falling back to manual review.");
this.open();
void this.processNextManual();
}
@ -283,17 +283,17 @@ export class BatchReviewModal extends Modal {
}
const buttonsContainer = contentEl.createDiv("batch-review-buttons");
const blackoutButton = buttonsContainer.createEl("button", { text: "0: Complete blackout", cls: "review-button review-button-complete-blackout" });
const blackoutButton = buttonsContainer.createEl("button", { text: "0: complete blackout", cls: "review-button review-button-complete-blackout" });
blackoutButton.addEventListener("click", () => void this.recordManualResult(note.path, ReviewResponse.CompleteBlackout));
const incorrectButton = buttonsContainer.createEl("button", { text: "1: Incorrect response", cls: "review-button review-button-incorrect" });
const incorrectButton = buttonsContainer.createEl("button", { text: "1: incorrect response", cls: "review-button review-button-incorrect" });
incorrectButton.addEventListener("click", () => void this.recordManualResult(note.path, ReviewResponse.IncorrectResponse));
const incorrectFamiliarButton = buttonsContainer.createEl("button", { text: "2: Incorrect but familiar", cls: "review-button review-button-incorrect-familiar" });
const incorrectFamiliarButton = buttonsContainer.createEl("button", { text: "2: incorrect but familiar", cls: "review-button review-button-incorrect-familiar" });
incorrectFamiliarButton.addEventListener("click", () => void this.recordManualResult(note.path, ReviewResponse.IncorrectButFamiliar));
const correctDifficultyButton = buttonsContainer.createEl("button", { text: "3: Correct with difficulty", cls: "review-button review-button-correct-difficulty" });
const correctDifficultyButton = buttonsContainer.createEl("button", { text: "3: correct with difficulty", cls: "review-button review-button-correct-difficulty" });
correctDifficultyButton.addEventListener("click", () => void this.recordManualResult(note.path, ReviewResponse.CorrectWithDifficulty));
const correctHesitationButton = buttonsContainer.createEl("button", { text: "4: Correct with hesitation", cls: "review-button review-button-correct-hesitation" });
const correctHesitationButton = buttonsContainer.createEl("button", { text: "4: correct with hesitation", cls: "review-button review-button-correct-hesitation" });
correctHesitationButton.addEventListener("click", () => void this.recordManualResult(note.path, ReviewResponse.CorrectWithHesitation));
const perfectRecallButton = buttonsContainer.createEl("button", { text: "5: Perfect recall", cls: "review-button review-button-perfect-recall" });
const perfectRecallButton = buttonsContainer.createEl("button", { text: "5: perfect recall", cls: "review-button review-button-perfect-recall" });
perfectRecallButton.addEventListener("click", () => void this.recordManualResult(note.path, ReviewResponse.PerfectRecall));
const skipButton = buttonsContainer.createEl("button", { text: "Skip", cls: "review-button review-button-skip" });
skipButton.addEventListener("click", () => {
@ -320,7 +320,7 @@ export class BatchReviewModal extends Modal {
showSummary(): void {
const { contentEl } = this;
contentEl.empty();
new Setting(contentEl).setName('MCQ review complete').setHeading().setClass('mcq-review-complete-header');
new Setting(contentEl).setName('Review complete').setHeading().setClass('mcq-review-complete-header');
const statsEl = contentEl.createDiv("batch-review-summary-stats");
const totalNotes = this.results.length;
const successfulNotes = this.results.filter(r => r.success).length;

View file

@ -213,7 +213,7 @@ export class ConsolidatedMCQModal extends Modal {
// Verify we have a valid question
if (!question || !question.choices || question.choices.length < 2) {
new Notice('Error: Invalid question data. Moving to next question.');
new Notice('Invalid question data. Moving to next question.');
// Skip to next question
this.currentQuestionIndex++;
@ -522,7 +522,7 @@ export class ConsolidatedMCQModal extends Modal {
contentEl.empty();
// Display results header with stylized heading
new Setting(contentEl).setName('MCQ review complete').setHeading().setClass('mcq-review-complete-header');
new Setting(contentEl).setName('Review complete').setHeading().setClass('mcq-review-complete-header');
// Display overall score with enhanced styling
const totalCorrectOverall = this.answers.filter(a => a.correct && a.attempts <= 1).length;
@ -538,16 +538,16 @@ export class ConsolidatedMCQModal extends Modal {
const performanceIndicator = scoreEl.createDiv('mcq-performance-indicator');
if (scorePercentOverall >= 90) {
performanceIndicator.setText('🎓 Excellent performance!');
performanceIndicator.setText('Excellent performance');
performanceIndicator.addClass('excellent');
} else if (scorePercentOverall >= 70) {
performanceIndicator.setText('👍 Good work!');
performanceIndicator.setText('Good work');
performanceIndicator.addClass('good');
} else if (scorePercentOverall >= 50) {
performanceIndicator.setText('🔄 Keep practicing');
performanceIndicator.setText('Keep practicing');
performanceIndicator.addClass('needs-improvement');
} else {
performanceIndicator.setText('📚 More review recommended');
performanceIndicator.setText('More review recommended');
performanceIndicator.addClass('review-recommended');
}

18
ui/constants.ts Normal file
View file

@ -0,0 +1,18 @@
export const SM2 = 'SM-2';
export const FSRS = 'FSRS';
export const POMODORO = 'Pomodoro';
export const MCQ = 'MCQ';
export const MCQS = 'MCQs';
export const API = 'API';
export const SPACEFORGE = 'Spaceforge';
export const UI_URL = 'URL';
export const UI_JSON = 'JSON';
export const UI = 'UI';
export const CSV = 'CSV';
export const CLAUDE = 'Claude';
export const GEMINI = 'Gemini';
export const OLLAMA = 'Ollama';
export const OPENAI = 'OpenAI';
export const TOGETHER_AI = 'Together AI';
export const OPENROUTER = 'OpenRouter';
export const WPM = 'WPM';

View file

@ -75,7 +75,7 @@ export class MCQModal extends Modal {
new Notice('Could not find note file to regenerate MCQs.');
}
} else {
new Notice('MCQ generation service not available.');
new Notice('Generation service not available.');
}
})();
});
@ -205,7 +205,7 @@ export class MCQModal extends Modal {
}
const question = this.mcqSet.questions[questionIndex];
if (!question || !question.choices || question.choices.length < 2) {
new Notice('Error: Invalid question data. Moving to next question.');
new Notice('Invalid question data. Moving to next question.');
this.session.currentQuestionIndex++;
if (this.session.currentQuestionIndex < this.mcqSet.questions.length) {
this.displayCurrentQuestion(containerEl);
@ -443,7 +443,7 @@ export class MCQModal extends Modal {
});
} catch {
new Setting(contentEl).setName('Error completing session').setHeading();
contentEl.createEl('p', { text: 'There was an error completing the MCQ session. Please try again.' });
contentEl.createEl('p', { text: 'There was an error completing the session. Please try again.' });
const errorCloseBtn = contentEl.createEl('button', { cls: 'mcq-close-btn', text: 'Close' });
errorCloseBtn.addEventListener('click', () => this.close());
}

View file

@ -1,4 +1,5 @@
import { App, Modal, Notice, TFile, setIcon, Setting } from 'obsidian';
import { SM2, FSRS } from './constants';
import SpaceforgePlugin from '../main';
import { ReviewResponse, FsrsRating } from '../models/review-schedule'; // Added FsrsRating
import { State as FsrsState } from 'ts-fsrs'; // For displaying FSRS state name
@ -100,7 +101,7 @@ export class ReviewModal extends Modal {
void initializedMcqController.startMCQReview(this.path);
this.close();
} else {
new Notice("MCQ feature could not be initialized. Please check settings.");
new Notice("Feature could not be initialized. Please check settings.");
}
}
});
@ -150,13 +151,13 @@ export class ReviewModal extends Modal {
if (schedule.lastReviewDate) infoText.createEl("p", { text: `Last reviewed: ${new Date(schedule.lastReviewDate).toLocaleDateString()}` });
if (schedule.schedulingAlgorithm === 'fsrs' && schedule.fsrsData) {
infoText.createEl("p", { text: `Algorithm: FSRS`, cls: "review-phase-fsrs" });
infoText.createEl("p", { text: `Stability: ${schedule.fsrsData.stability.toFixed(2)}` });
infoText.createEl("p", { text: `Difficulty: ${schedule.fsrsData.difficulty.toFixed(2)}` });
infoText.createEl("p", { text: `State: ${FsrsState[schedule.fsrsData.state]}` }); // Display FSRS state name
infoText.createEl("p", { text: `Algorithm: ${FSRS}`, cls: "review-phase-fsrs" });
infoText.createEl("p", { text: `${'Stability'}: ${schedule.fsrsData.stability.toFixed(2)}` });
infoText.createEl("p", { text: `${'Difficulty'}: ${schedule.fsrsData.difficulty.toFixed(2)}` });
infoText.createEl("p", { text: `${'State'}: ${FsrsState[schedule.fsrsData.state]}` }); // Display FSRS state name
infoText.createEl("p", { text: `Interval: ${schedule.fsrsData.scheduled_days} days` });
} else { // SM-2 or fallback
infoText.createEl("p", { text: `Algorithm: SM-2`, cls: "review-phase-sm2" });
infoText.createEl("p", { text: `Algorithm: ${SM2}`, cls: "review-phase-sm2" });
let phaseText: string; let phaseClass: string;
if (schedule.scheduleCategory === 'initial') {
const totalInitialSteps = this.plugin.settings.initialScheduleCustomIntervals.length;

View file

@ -1,9 +1,9 @@
import { App, Notice, PluginSettingTab, Setting, setIcon } from 'obsidian';
import { ConfirmationModal } from './confirmation-modal';
import SpaceforgePlugin from '../main';
// Import ApiProvider, DEFAULT_SETTINGS, SpaceforgeSettings, and MCQQuestionAmountMode
import { ApiProvider, DEFAULT_SETTINGS, MCQQuestionAmountMode, MCQDifficulty } from '../models/settings';
import { SpaceforgePluginData, DEFAULT_PLUGIN_STATE_DATA } from '../models/plugin-data'; // Import data structures
import { SpaceforgePluginData, DEFAULT_PLUGIN_STATE_DATA } from '../models/plugin-data';
import { SM2, FSRS, POMODORO, MCQS, API, SPACEFORGE, CLAUDE, GEMINI, OLLAMA, TOGETHER_AI, WPM } from './constants';
/**
* Settings tab for the plugin
@ -128,7 +128,7 @@ export class SpaceforgeSettingTab extends PluginSettingTab {
a.click();
document.body.removeChild(a);
new Notice('All plugin data exported successfully');
new Notice('All data exported successfully');
});
// Import all data button
@ -176,7 +176,7 @@ export class SpaceforgeSettingTab extends PluginSettingTab {
this.display(); // Refresh settings display
new Notice('All plugin data imported successfully. Plugin may require a reload for all changes to take effect.');
new Notice('All data imported successfully. Plugin may require a reload for all changes to take effect.');
} catch (error) {
new Notice(`Failed to import data: ${error.message} `);
}
@ -217,7 +217,7 @@ export class SpaceforgeSettingTab extends PluginSettingTab {
this.display(); // Refresh settings display
new Notice('All plugin data reset to defaults.');
new Notice('All data reset to defaults.');
}
).open();
});
@ -354,7 +354,7 @@ export class SpaceforgeSettingTab extends PluginSettingTab {
// Save the cleared data
await this.plugin.savePluginData();
new Notice('All plugin data cleared successfully.');
new Notice('All data cleared successfully.');
// Then refresh UI elements
this.display(); // Refresh settings UI
@ -383,8 +383,8 @@ export class SpaceforgeSettingTab extends PluginSettingTab {
.setName('Default scheduling algorithm')
.setDesc('Choose the default algorithm for newly created notes.')
.addDropdown(dropdown => dropdown
.addOption('sm2', 'SM-2')
.addOption('fsrs', 'FSRS')
.addOption('sm2', SM2)
.addOption('fsrs', FSRS)
.setValue(this.plugin.settings.defaultSchedulingAlgorithm)
.onChange(async (value: 'sm2' | 'fsrs') => {
this.plugin.settings.defaultSchedulingAlgorithm = value;
@ -400,13 +400,13 @@ export class SpaceforgeSettingTab extends PluginSettingTab {
const sm2ParamsContainer = spacedRepSection.createEl('details', { cls: 'sf-settings-collapsible-subsection' });
const sm2Summary = sm2ParamsContainer.createEl('summary');
// Changed to h3
sm2Summary.setText('SM-2 parameters');
sm2Summary.setText(`${SM2} parameters`);
sm2ParamsContainer.open = false; // Initially closed
new Setting(sm2ParamsContainer)
.setName('SM-2: base ease factor')
.setDesc('Initial ease factor for new SM-2 notes (2.5 is SM-2 default). Higher ease increases interval growth. Value shown is internal format (250 = 2.5).')
.setName(`Base ease factor (${SM2})`)
.setDesc(`Initial ease factor for new ${SM2} notes (2.5 is ${SM2} default). Higher ease increases interval growth. Value shown is internal format (250 = 2.5).`)
.addSlider(slider => slider
.setLimits(130, 500, 10)
.setValue(this.plugin.settings.baseEase)
@ -417,8 +417,8 @@ export class SpaceforgeSettingTab extends PluginSettingTab {
}));
new Setting(sm2ParamsContainer)
.setName('SM-2: use initial learning schedule')
.setDesc('For new SM-2 notes, use a fixed set of initial intervals before applying the full algorithm.')
.setName(`Use initial learning schedule (${SM2})`)
.setDesc(`For new ${SM2} notes, use a fixed set of initial intervals before applying the full algorithm.`)
.addToggle(toggle => toggle
.setValue(this.plugin.settings.useInitialSchedule)
.onChange(async (value: boolean) => {
@ -429,8 +429,8 @@ export class SpaceforgeSettingTab extends PluginSettingTab {
if (this.plugin.settings.useInitialSchedule) {
new Setting(sm2ParamsContainer)
.setName('SM-2: custom initial intervals (days)')
.setDesc('Comma-separated list for initial SM-2 reviews (e.g., 0,1,3,7). Must start with 0.')
.setName(`Custom initial intervals (${SM2})`)
.setDesc(`Comma-separated list for initial ${SM2} reviews (e.g., 0,1,3,7). Must start with 0.`)
.addText(text => text
.setValue(this.plugin.settings.initialScheduleCustomIntervals.join(', '))
.onChange(async (value: string) => {
@ -439,15 +439,15 @@ export class SpaceforgeSettingTab extends PluginSettingTab {
this.plugin.settings.initialScheduleCustomIntervals = intervals;
await this.plugin.savePluginData();
} else {
new Notice("Custom initial SM-2 intervals must start with 0 and be valid numbers.", 5000);
new Notice(`Custom initial ${SM2} intervals must start with 0 and be valid numbers.`, 5000);
text.setValue(this.plugin.settings.initialScheduleCustomIntervals.join(', '));
}
}));
}
new Setting(sm2ParamsContainer)
.setName('SM-2: maximum interval (days)')
.setDesc('Longest possible interval between SM-2 reviews.')
.setName(`Maximum interval (${SM2})`)
.setDesc(`Longest possible interval between ${SM2} reviews.`)
.addText(text => text
.setValue(this.plugin.settings.maximumInterval.toString())
.onChange(async (value: string) => {
@ -459,8 +459,8 @@ export class SpaceforgeSettingTab extends PluginSettingTab {
}));
new Setting(sm2ParamsContainer)
.setName('SM-2: load balancing')
.setDesc('Add slight randomness to SM-2 intervals to prevent reviews clumping on the same day.')
.setName(`Load balancing (${SM2})`)
.setDesc(`Add slight randomness to ${SM2} intervals to prevent reviews clumping on the same day.`)
.addToggle(toggle => toggle
.setValue(this.plugin.settings.loadBalance)
.onChange(async (value: boolean) => {
@ -472,7 +472,7 @@ export class SpaceforgeSettingTab extends PluginSettingTab {
const fsrsParamsContainer = spacedRepSection.createEl('details', { cls: 'sf-settings-collapsible-subsection' });
const fsrsSummary = fsrsParamsContainer.createEl('summary');
// Changed to h3
fsrsSummary.setText('FSRS parameters');
fsrsSummary.setText(`${FSRS} parameters`);
fsrsParamsContainer.open = false; // Initially closed
new Setting(fsrsParamsContainer)
@ -487,13 +487,13 @@ export class SpaceforgeSettingTab extends PluginSettingTab {
await this.plugin.savePluginData();
this.plugin.reviewScheduleService.updateAlgorithmServicesForSettingsChange();
} else {
new Notice("FSRS request retention must be between 0.7 and 0.99.");
new Notice(`${FSRS} request retention must be between 0.7 and 0.99.`);
}
}));
new Setting(fsrsParamsContainer)
.setName('Maximum interval (days)')
.setDesc('Longest possible interval FSRS will schedule.')
.setDesc(`Longest possible interval ${FSRS} will schedule.`)
.addText(text => text
.setValue(this.plugin.settings.fsrsParameters?.maximum_interval?.toString() ?? DEFAULT_SETTINGS.fsrsParameters.maximum_interval?.toString() ?? '36500')
.onChange(async (value) => {
@ -503,7 +503,7 @@ export class SpaceforgeSettingTab extends PluginSettingTab {
await this.plugin.savePluginData();
this.plugin.reviewScheduleService.updateAlgorithmServicesForSettingsChange();
} else {
new Notice("FSRS maximum interval must be a positive number.");
new Notice(`${FSRS} maximum interval must be a positive number.`);
}
}));
@ -523,12 +523,12 @@ export class SpaceforgeSettingTab extends PluginSettingTab {
await this.plugin.savePluginData();
this.plugin.reviewScheduleService.updateAlgorithmServicesForSettingsChange();
} else {
new Notice("FSRS learning steps must be valid comma-separated numbers > 0, or empty.");
new Notice(`${FSRS} learning steps must be valid comma-separated numbers > 0, or empty.`);
}
}));
new Setting(fsrsParamsContainer)
.setName('Enable fuzz')
.setDesc('Add slight randomness to FSRS intervals (recommended).')
.setDesc(`Add slight randomness to ${FSRS} intervals (recommended).`)
.addToggle(toggle => toggle
.setValue(this.plugin.settings.fsrsParameters?.enable_fuzz ?? DEFAULT_SETTINGS.fsrsParameters.enable_fuzz ?? false)
.onChange(async (value) => {
@ -537,8 +537,8 @@ export class SpaceforgeSettingTab extends PluginSettingTab {
this.plugin.reviewScheduleService.updateAlgorithmServicesForSettingsChange();
}));
new Setting(fsrsParamsContainer)
.setName('Enable short term scheduling')
.setDesc('Use FSRS short-term memory model (affects initial learning steps).')
.setName('Enable short-term scheduling')
.setDesc(`Use ${FSRS} short-term memory model (affects initial learning steps).`)
.addToggle(toggle => toggle
.setValue(this.plugin.settings.fsrsParameters?.enable_short_term ?? DEFAULT_SETTINGS.fsrsParameters.enable_short_term ?? false)
.onChange(async (value) => {
@ -548,7 +548,7 @@ export class SpaceforgeSettingTab extends PluginSettingTab {
}));
new Setting(fsrsParamsContainer)
.setName('Weights (w)')
.setDesc('FSRS algorithm parameters (17 numbers). Edit with caution. Default weights are generally good.')
.setDesc(`${FSRS} algorithm parameters (17 numbers). Edit with caution. Default weights are generally good.`)
.addTextArea(text => {
text.inputEl.rows = 3;
text.inputEl.addClass('sf-full-width-textarea');
@ -560,7 +560,7 @@ export class SpaceforgeSettingTab extends PluginSettingTab {
await this.plugin.savePluginData();
this.plugin.reviewScheduleService.updateAlgorithmServicesForSettingsChange();
} else {
new Notice("FSRS weights must be a comma-separated list of 17 valid numbers.");
new Notice(`${FSRS} weights must be a comma-separated list of 17 valid numbers.`);
}
});
});
@ -573,42 +573,42 @@ export class SpaceforgeSettingTab extends PluginSettingTab {
conversionContainer.open = false; // Initially closed
new Setting(conversionContainer)
.setName('Convert all SM-2 cards to FSRS')
.setDesc('Migrate all existing SM-2 cards to use the FSRS algorithm. This will reset their learning state for FSRS.')
.setName(`Convert all ${SM2} cards to ${FSRS}`)
.setDesc(`Migrate all existing ${SM2} cards to use the ${FSRS} algorithm. This will reset their learning state for ${FSRS}.`)
.addButton(button => button
.setButtonText('Convert SM-2 to FSRS')
.setButtonText(`Convert ${SM2} to ${FSRS}`)
.setCta() // Call to action style
.onClick(() => {
new ConfirmationModal(
this.app,
'Convert SM-2 to FSRS',
'Are you sure you want to convert ALL SM-2 cards to FSRS? Their FSRS learning state will be reset. This action cannot be easily undone.',
`Convert ${SM2} to ${FSRS}`,
`Are you sure you want to convert ALL ${SM2} cards to ${FSRS}? Their ${FSRS} learning state will be reset. This action cannot be easily undone.`,
async () => {
new Notice('Converting SM-2 cards to FSRS... This may take a moment.');
new Notice(`Converting ${SM2} cards to ${FSRS}... This may take a moment.`);
this.plugin.reviewScheduleService.convertAllSm2ToFsrs();
await this.plugin.savePluginData();
new Notice('All SM-2 cards have been converted to FSRS.');
new Notice(`All ${SM2} cards have been converted to ${FSRS}.`);
this.display(); // Refresh settings tab
}
).open();
}));
new Setting(conversionContainer)
.setName('Convert all FSRS cards to SM-2')
.setDesc('Migrate all existing FSRS cards to use the SM-2 algorithm. Their SM-2 learning state will be initialized with defaults.')
.setName(`Convert all ${FSRS} cards to ${SM2}`)
.setDesc(`Migrate all existing ${FSRS} cards to use the ${SM2} algorithm. Their ${SM2} learning state will be initialized with defaults.`)
.addButton(button => button
.setButtonText('Convert FSRS to SM-2')
.setButtonText(`Convert ${FSRS} to ${SM2}`)
.setCta()
.onClick(() => {
new ConfirmationModal(
this.app,
'Convert FSRS to SM-2',
'Are you sure you want to convert ALL FSRS cards to SM-2? Their SM-2 learning state will be reset to defaults. This action cannot be easily undone.',
`Convert ${FSRS} to ${SM2}`,
`Are you sure you want to convert ALL ${FSRS} cards to ${SM2}? Their ${SM2} learning state will be reset to defaults. This action cannot be easily undone.`,
async () => {
new Notice('Converting FSRS cards to SM-2... This may take a moment.');
new Notice(`Converting ${FSRS} cards to ${SM2}... This may take a moment.`);
this.plugin.reviewScheduleService.convertAllFsrsToSm2();
await this.plugin.savePluginData();
new Notice('All FSRS cards have been converted to SM-2.');
new Notice(`All ${FSRS} cards have been converted to ${SM2}.`);
this.display(); // Refresh settings tab
}
).open();
@ -674,7 +674,7 @@ export class SpaceforgeSettingTab extends PluginSettingTab {
}));
new Setting(interfaceSection)
.setName('Reading speed (WPM)')
.setName(`Reading speed (${WPM})`)
.setDesc('Words per minute for estimating review time')
.addSlider(slider => slider
.setLimits(100, 500, 10)
@ -687,7 +687,7 @@ export class SpaceforgeSettingTab extends PluginSettingTab {
interfaceSection.createEl('div', {
cls: 'sf-setting-explain',
text: 'Average adults read 200-250 WPM for regular content, 100-150 WPM for technical content'
text: `Average adults read 200-250 ${WPM} for regular content, 100-150 ${WPM} for technical content`
});
new Setting(interfaceSection)
@ -756,7 +756,7 @@ export class SpaceforgeSettingTab extends PluginSettingTab {
const mcqSection = createCollapsible('Multiple choice questions', 'newspaper', false); // Closed by default
new Setting(mcqSection)
.setName('Enable MCQ feature')
.setName('Enable multiple choice feature')
.setDesc('Use AI-generated multiple-choice questions to test your knowledge')
.addToggle(toggle => toggle
.setValue(this.plugin.settings.enableMCQ)
@ -821,7 +821,7 @@ export class SpaceforgeSettingTab extends PluginSettingTab {
.setClass("sf-settings-subsection-provider-header");
const apiKeyContainer = mcqSection.createEl('div', { cls: 'sf-setting-highlight' });
new Setting(apiKeyContainer)
.setName('OpenRouter API key')
.setName('API key')
.setDesc('Required for generating MCQs via OpenRouter.')
.addText(text => text
.setPlaceholder('Enter your OpenRouter API key')
@ -834,7 +834,7 @@ export class SpaceforgeSettingTab extends PluginSettingTab {
apiKeyContainer.createEl('div').setText('Get your API key at https://openrouter.ai/keys');
new Setting(mcqSection)
.setName('OpenRouter model')
.setName('Model')
.setDesc('Model identifier from OpenRouter (e.g., openai/gpt-4.1-mini)')
.addText(text => text
.setPlaceholder('Enter OpenRouter model identifier')
@ -850,7 +850,7 @@ export class SpaceforgeSettingTab extends PluginSettingTab {
.setHeading()
.setClass("sf-settings-subsection-provider-header");
new Setting(mcqSection)
.setName('OpenAI API key')
.setName('API key')
.setDesc('Your OpenAI API key.')
.addText(text => text
.setPlaceholder('Enter your OpenAI API key (sk-...)')
@ -860,7 +860,7 @@ export class SpaceforgeSettingTab extends PluginSettingTab {
await this.plugin.savePluginData();
}));
new Setting(mcqSection)
.setName('OpenAI model')
.setName('Model')
.setDesc('Model name (e.g., gpt-3.5-turbo, gpt-4)')
.addText(text => text
.setPlaceholder('Enter OpenAI model name')
@ -875,20 +875,20 @@ export class SpaceforgeSettingTab extends PluginSettingTab {
.setHeading()
.setClass("sf-settings-subsection-provider-header");
new Setting(mcqSection)
.setName('Ollama API URL')
.setName('API URL')
.setDesc('URL of your running Ollama instance (e.g., http://localhost:11434)')
.addText(text => text
.setPlaceholder('http://localhost:11434')
.setPlaceholder(`http:${'//'}localhost:11434`)
.setValue(this.plugin.settings.ollamaApiUrl)
.onChange(async (value: string) => {
this.plugin.settings.ollamaApiUrl = value;
await this.plugin.savePluginData();
}));
new Setting(mcqSection)
.setName('Ollama model')
.setDesc('Name of the Ollama model to use (e.g., llama3, mistral)')
.setName('Model')
.setDesc(`Name of the ${OLLAMA} model to use (e.g., llama3, mistral)`)
.addText(text => text
.setPlaceholder('Enter Ollama model name')
.setPlaceholder(`Enter ${OLLAMA} model name`)
.setValue(this.plugin.settings.ollamaModel)
.onChange(async (value: string) => {
this.plugin.settings.ollamaModel = value;
@ -900,20 +900,20 @@ export class SpaceforgeSettingTab extends PluginSettingTab {
.setHeading()
.setClass("sf-settings-subsection-provider-header");
new Setting(mcqSection)
.setName('Gemini API key')
.setDesc('Your Google AI Gemini API key.')
.setName('API key')
.setDesc(`Your Google AI ${GEMINI} ${API} key.`)
.addText(text => text
.setPlaceholder('Enter your Gemini API key')
.setPlaceholder(`Enter your ${GEMINI} ${API} key`)
.setValue(this.plugin.settings.geminiApiKey)
.onChange(async (value: string) => {
this.plugin.settings.geminiApiKey = value;
await this.plugin.savePluginData();
}));
new Setting(mcqSection)
.setName('Gemini model')
.setName('Model')
.setDesc('Model name (e.g., gemini-pro)')
.addText(text => text
.setPlaceholder('Enter Gemini model name')
.setPlaceholder(`Enter ${GEMINI} model name`)
.setValue(this.plugin.settings.geminiModel)
.onChange(async (value: string) => {
this.plugin.settings.geminiModel = value;
@ -925,20 +925,20 @@ export class SpaceforgeSettingTab extends PluginSettingTab {
.setHeading()
.setClass("sf-settings-subsection-provider-header");
new Setting(mcqSection)
.setName('Claude API key')
.setDesc('Your Anthropic Claude API key.')
.setName('API key')
.setDesc(`Your Anthropic ${CLAUDE} ${API} key.`)
.addText(text => text
.setPlaceholder('Enter your Claude API key')
.setPlaceholder(`Enter your ${CLAUDE} ${API} key`)
.setValue(this.plugin.settings.claudeApiKey)
.onChange(async (value: string) => {
this.plugin.settings.claudeApiKey = value;
await this.plugin.savePluginData();
}));
new Setting(mcqSection)
.setName('Claude model')
.setName('Model')
.setDesc('Model name (e.g., claude-3-opus-20240229, claude-3-sonnet-20240229)')
.addText(text => text
.setPlaceholder('Enter Claude model name')
.setPlaceholder(`Enter ${CLAUDE} model name`)
.setValue(this.plugin.settings.claudeModel)
.onChange(async (value: string) => {
this.plugin.settings.claudeModel = value;
@ -950,20 +950,20 @@ export class SpaceforgeSettingTab extends PluginSettingTab {
.setHeading()
.setClass("sf-settings-subsection-provider-header");
new Setting(mcqSection)
.setName('Together AI API key')
.setDesc('Your Together AI API key.')
.setName('API key')
.setDesc(`Your ${TOGETHER_AI} ${API} key.`)
.addText(text => text
.setPlaceholder('Enter your Together AI API key')
.setPlaceholder(`Enter your ${TOGETHER_AI} ${API} key`)
.setValue(this.plugin.settings.togetherApiKey)
.onChange(async (value: string) => {
this.plugin.settings.togetherApiKey = value;
await this.plugin.savePluginData();
}));
new Setting(mcqSection)
.setName('Together AI model')
.setName('Model')
.setDesc('Model identifier from Together AI (e.g., meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8)')
.addText(text => text
.setPlaceholder('Enter Together AI model identifier')
.setPlaceholder(`Enter ${TOGETHER_AI} model identifier`)
.setValue(this.plugin.settings.togetherModel)
.onChange(async (value: string) => {
this.plugin.settings.togetherModel = value;
@ -1056,7 +1056,7 @@ export class SpaceforgeSettingTab extends PluginSettingTab {
const promptTypeContainer = mcqFormattingGrid.createEl('div');
new Setting(promptTypeContainer)
.setName('Prompt type')
.setDesc('Format for MCQ generation')
.setDesc('Format for question generation')
.addDropdown(dropdown => dropdown
.addOption('basic', 'Basic')
.addOption('detailed', 'Detailed')
@ -1069,7 +1069,7 @@ export class SpaceforgeSettingTab extends PluginSettingTab {
// Second item in grid
const difficultyContainer = mcqFormattingGrid.createEl('div');
new Setting(difficultyContainer)
.setName('MCQ difficulty')
.setName('Difficulty')
.setDesc('Complexity level')
.addDropdown(dropdown => dropdown
.addOption(MCQDifficulty.Basic, 'Basic recall')
@ -1177,8 +1177,8 @@ export class SpaceforgeSettingTab extends PluginSettingTab {
if (this.plugin.settings.enableQuestionRegenerationOnRating) {
new Setting(mcqSection)
.setName('Min SM-2 rating for MCQ regeneration')
.setDesc('For SM-2: Regenerate MCQs if review rating (0-5) is this value or higher. (0:Blackout, 5:Perfect)')
.setName(`Minimum rating for regeneration (${SM2})`)
.setDesc(`For ${SM2}: Regenerate ${MCQS} if review rating (0-5) is this value or higher. (0:Blackout, 5:Perfect)`)
.addSlider(slider => slider
.setLimits(0, 5, 1)
.setValue(this.plugin.settings.minSm2RatingForQuestionRegeneration)
@ -1189,8 +1189,8 @@ export class SpaceforgeSettingTab extends PluginSettingTab {
}));
new Setting(mcqSection)
.setName('Min FSRS rating for MCQ regeneration')
.setDesc('For FSRS: Regenerate MCQs if review rating (1-4) is this value or higher. (1:Again, 4:Easy)')
.setName(`Minimum rating for regeneration (${FSRS})`)
.setDesc(`For ${FSRS}: Regenerate ${MCQS} if review rating (1-4) is this value or higher. (1:Again, 4:Easy)`)
.addSlider(slider => slider
.setLimits(1, 4, 1) // FSRS ratings are 1-4
.setValue(this.plugin.settings.minFsrsRatingForQuestionRegeneration)
@ -1205,16 +1205,16 @@ export class SpaceforgeSettingTab extends PluginSettingTab {
// If MCQ is disabled, show a message about enabling it
const mcqDisabledMessage = mcqSection.createEl('div', { cls: 'sf-info-box' });
mcqDisabledMessage.createEl('p', {
text: 'Multiple choice questions are currently disabled. Enable it to configure durations and notifications.'
text: `${MCQS} are currently disabled. Enable it to configure durations and notifications.`
});
}
// ========= POMODORO TIMER SECTION =========
const pomodoroSection = createCollapsible('Pomodoro timer', 'timer', false); // Added timer icon
const pomodoroSection = createCollapsible(`${POMODORO} timer`, 'timer', false); // Added timer icon
new Setting(pomodoroSection)
.setName('Enable pomodoro timer')
.setDesc('Show the Pomodoro timer in the sidebar.')
.setName(`Enable ${POMODORO} timer`)
.setDesc(`Show the ${POMODORO} timer in the sidebar.`)
.addToggle(toggle => toggle
.setValue(this.plugin.settings.pomodoroEnabled)
.onChange(async (value: boolean) => {
@ -1310,7 +1310,7 @@ export class SpaceforgeSettingTab extends PluginSettingTab {
} else {
const pomodoroDisabledMessage = pomodoroSection.createEl('div', { cls: 'sf-info-box' });
pomodoroDisabledMessage.createEl('p', {
text: 'Pomodoro timer is currently disabled. Enable it to configure durations and notifications.'
text: `${POMODORO} timer is currently disabled. Enable it to configure durations and notifications.`
});
}
@ -1331,7 +1331,7 @@ export class SpaceforgeSettingTab extends PluginSettingTab {
.setDesc(locationDesc)
.addExtraButton(button => button
.setIcon('info')
.setTooltip('Current location of your Spaceforge data')
.setTooltip(`Current location of your ${SPACEFORGE} data`)
.onClick(() => {
const message = this.plugin.settings.useCustomDataPath && this.plugin.settings.customDataPath
? `Your data is stored at: ${this.plugin.settings.customDataPath.endsWith('/data.json') ? this.plugin.settings.customDataPath : `${this.plugin.settings.customDataPath}/data.json`}`
@ -1385,17 +1385,17 @@ export class SpaceforgeSettingTab extends PluginSettingTab {
container.empty(); // Clear previous content
if (algorithm === 'sm2') {
new Setting(container).setName('About the modified SM-2 algorithm').setHeading();
new Setting(container).setName(`About modified ${SM2} algorithm`).setHeading();
container.createEl('p', {
text: 'Spaceforge uses a modified version of the SuperMemo SM-2 algorithm (1991) which schedules reviews based on how well you recall information. ' +
text: `Spaceforge uses a modified version of the SuperMemo ${SM2} algorithm (1991) which schedules reviews based on how well you recall information. ` +
'When you rate your recall quality from 0-5, the algorithm adjusts the interval and difficulty (ease factor) accordingly.'
});
container.createEl('p', {
text: 'Our implementation includes specific handling for overdue or skipped items to prevent them from accumulating in a backlog:'
});
const sm2List = container.createEl('ul');
sm2List.createEl('li', { text: 'Overdue items: Automatically set to review tomorrow with a quality rating of 0.' });
sm2List.createEl('li', { text: 'Postponed items: Explicitly moved to tomorrow with a one-step quality penalty.' });
sm2List.createEl('li', { text: `${'Overdue'} items: Automatically set to review tomorrow with a quality rating of 0.` });
sm2List.createEl('li', { text: `${'Postponed'} items: Explicitly moved to tomorrow with a one-step quality penalty.` });
sm2List.createEl('li', { text: 'Both cases reset the repetition count to 1 and update the ease factor.' });
const ratingsTable = container.createEl('table', { cls: 'sf-ratings-table' }); // Added a class for potential styling
@ -1426,19 +1426,19 @@ export class SpaceforgeSettingTab extends PluginSettingTab {
row4.createEl('td', { text: 'Perfect recall' });
row4.createEl('td', { text: 'Largest increase' });
} else if (algorithm === 'fsrs') {
new Setting(container).setName('About the FSRS algorithm').setHeading();
new Setting(container).setName(`About ${FSRS} algorithm`).setHeading();
container.createEl('p', {
text: 'FSRS (Free Spaced Repetition Scheduler) is a modern, evidence-based algorithm that models memory retention to optimize review schedules. ' +
'It calculates card difficulty and stability dynamically based on your review history and aims for a target retention rate.'
text: `${FSRS} (Free Spaced Repetition Scheduler) is a modern, evidence-based algorithm that models memory retention to optimize review schedules. ` +
`It calculates card difficulty and stability dynamically based on your review history and aims for a target retention rate.`
});
container.createEl('p', {
text: 'Key concepts in FSRS:'
text: `Key concepts in ${FSRS}:`
});
const fsrsList = container.createEl('ul');
fsrsList.createEl('li', { text: 'Difficulty: How hard a card is to remember.' });
fsrsList.createEl('li', { text: 'Stability: How long a card is likely to be remembered.' });
fsrsList.createEl('li', { text: 'Retention: The probability of recalling a card at the time of review.' });
fsrsList.createEl('li', { text: 'Learning steps: Initial short intervals for new cards (configurable).' });
fsrsList.createEl('li', { text: `${'Difficulty'}: How hard a card is to remember.` });
fsrsList.createEl('li', { text: `${'Stability'}: How long a card is likely to be remembered.` });
fsrsList.createEl('li', { text: `${'Retention'}: The probability of recalling a card at the time of review.` });
fsrsList.createEl('li', { text: `${'Learning'} steps: Initial short intervals for new cards (configurable).` });
const ratingsTable = container.createEl('table', { cls: 'sf-ratings-table' });
const thead = ratingsTable.createTHead();
@ -1449,22 +1449,22 @@ export class SpaceforgeSettingTab extends PluginSettingTab {
headerRow.createEl('th', { text: 'Effect on stability/difficulty' });
const row1 = tbody.insertRow();
row1.createEl('td', { text: '1 (Again)' });
row1.createEl('td', { text: `${'1 (Again)'}` });
row1.createEl('td', { text: 'Forgot the card' });
row1.createEl('td', { text: 'Decreases stability, may increase difficulty' });
const row2 = tbody.insertRow();
row2.createEl('td', { text: '2 (Hard)' });
row2.createEl('td', { text: `${'2 (Hard)'}` });
row2.createEl('td', { text: 'Recalled with significant difficulty' });
row2.createEl('td', { text: 'Smaller increase in stability' });
const row3 = tbody.insertRow();
row3.createEl('td', { text: '3 (Good)' });
row3.createEl('td', { text: `${'3 (Good)'}` });
row3.createEl('td', { text: 'Recalled correctly' });
row3.createEl('td', { text: 'Standard increase in stability' });
const row4 = tbody.insertRow();
row4.createEl('td', { text: '4 (Easy)' });
row4.createEl('td', { text: `${'4 (Easy)'}` });
row4.createEl('td', { text: 'Recalled easily' });
row4.createEl('td', { text: 'Largest increase in stability' });
}

View file

@ -248,7 +248,7 @@ export class ReviewSidebarView extends ItemView {
if (this.listViewRenderer) { // Check if renderer is initialized
await this.listViewRenderer.render(container);
} else {
container.setText("Error: Could not render list view. Renderer not ready.");
container.setText("Error: could not render list view. Renderer not ready.");
}
}
@ -267,7 +267,7 @@ export class ReviewSidebarView extends ItemView {
if (this.calendarView) { // Should always exist due to ensureBaseStructure
await this.calendarView.render();
} else {
container.setText("Error: Could not render calendar view. CalendarView not ready.");
container.setText("Error: could not render calendar view. CalendarView not ready.");
return; // Avoid further errors if calendarView is somehow null
}

View file

@ -55,7 +55,7 @@ export class PomodoroUIManager {
// new Notice("Pomodoro durations updated."); // Removed notification
return true;
} else {
new Notice("Invalid Pomodoro durations. Settings not saved. Please enter positive numbers.");
new Notice("Invalid durations. Settings not saved. Please enter positive numbers.");
// Re-populate with current valid settings to prevent saving invalid on next close if not corrected
if (this.pomodoroQuickWorkInput) this.pomodoroQuickWorkInput.value = String(this.plugin.settings.pomodoroWorkDuration);
if (this.pomodoroQuickShortInput) this.pomodoroQuickShortInput.value = String(this.plugin.settings.pomodoroShortBreakDuration);
@ -339,7 +339,7 @@ export class PomodoroUIManager {
this.pomodoroUserOverrideHoursInput.value = String(this.plugin.pluginState.pomodoroUserOverrideHours);
const hoursLabel = overrideInputsContainer.createSpan("pomodoro-override-label-small");
hoursLabel.setText("h");
hoursLabel.setText("H");
this.pomodoroUserOverrideMinutesInput = overrideInputsContainer.createEl("input", { type: "number", cls: "pomodoro-override-minutes" });
this.pomodoroUserOverrideMinutesInput.setAttr("min", "0");
@ -347,7 +347,7 @@ export class PomodoroUIManager {
this.pomodoroUserOverrideMinutesInput.value = String(this.plugin.pluginState.pomodoroUserOverrideMinutes);
const minutesLabel = overrideInputsContainer.createSpan("pomodoro-override-label-small");
minutesLabel.setText("m");
minutesLabel.setText("M");
// Add to estimation toggle
const toggleContainer = overrideContainer.createDiv("pomodoro-override-toggle-container");