refactor: replace fetch with Obsidian requestUrl for API calls

- Switches all LLM services (Claude, Gemini, Ollama, OpenAI, OpenRouter, Together) from native fetch to Obsidian’s requestUrl for better CORS handling and plugin compatibility
- Updates UI components to use new Obsidian API patterns (createDiv with options)
- Minor clean-ups in MCQ, Pomodoro, and review services
This commit is contained in:
Dralk 2025-08-02 01:06:54 +03:00
parent 79d83e4eb4
commit dbd37e8537
20 changed files with 371 additions and 546 deletions

View file

@ -1,4 +1,4 @@
import { Notice } from 'obsidian';
import { Notice, requestUrl } from 'obsidian';
import SpaceforgePlugin from '../main';
import { MCQQuestion, MCQSet } from '../models/mcq';
import { IMCQGenerationService } from './mcq-generation-service';
@ -89,7 +89,8 @@ export class ClaudeService implements IMCQGenerationService {
console.log(`Making API request to Claude using model: ${model} with difficulty: ${difficulty}`);
try {
const response = await fetch('https://api.anthropic.com/v1/messages', {
const response = await requestUrl({
url: 'https://api.anthropic.com/v1/messages',
method: 'POST',
headers: {
'Content-Type': 'application/json',
@ -106,13 +107,13 @@ export class ClaudeService implements IMCQGenerationService {
})
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({ message: response.statusText }));
if (response.status !== 200) {
const errorData = response.json || { message: response.text };
console.error('Claude API error:', response.status, errorData);
throw new Error(`API request failed (${response.status}): ${errorData.error?.message || errorData.message || 'Unknown error'}`);
}
const data = await response.json();
const data = response.json;
if (!data.content || !data.content.length || !data.content[0].text) {
console.error('Invalid API response format from Claude:', data);
throw new Error('Invalid API response format from Claude - missing content');

View file

@ -1,4 +1,4 @@
import { Notice } from 'obsidian';
import { Notice, requestUrl } from 'obsidian';
import SpaceforgePlugin from '../main';
import { MCQQuestion, MCQSet } from '../models/mcq';
import { IMCQGenerationService } from './mcq-generation-service';
@ -92,7 +92,8 @@ export class GeminiService implements IMCQGenerationService {
const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${apiKey}`;
try {
const response = await fetch(apiUrl, {
const response = await requestUrl({
url: apiUrl,
method: 'POST',
headers: {
'Content-Type': 'application/json',
@ -108,14 +109,14 @@ export class GeminiService implements IMCQGenerationService {
})
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({ message: response.statusText }));
if (response.status !== 200) {
const errorData = response.json;
console.error('Gemini API error:', response.status, errorData);
const errorMessage = errorData?.error?.message || errorData?.message || 'Unknown error';
throw new Error(`API request failed (${response.status}): ${errorMessage}`);
}
const data = await response.json();
const data = response.json;
// Gemini response structure: data.candidates[0].content.parts[0].text
if (!data.candidates || !data.candidates.length || !data.candidates[0].content || !data.candidates[0].content.parts || !data.candidates[0].content.parts.length || !data.candidates[0].content.parts[0].text) {
console.error('Invalid API response format from Gemini:', data);

View file

@ -1,4 +1,4 @@
import { Notice } from 'obsidian';
import { Notice, requestUrl } from 'obsidian';
import SpaceforgePlugin from '../main';
import { MCQQuestion, MCQSet } from '../models/mcq';
import { IMCQGenerationService } from './mcq-generation-service';
@ -89,7 +89,8 @@ export class OllamaService implements IMCQGenerationService {
console.log(`Making API request to Ollama at ${apiUrl} using model: ${model} with difficulty: ${difficulty}`);
try {
const response = await fetch(`${apiUrl}/api/chat`, { // Common Ollama chat endpoint
const response = await requestUrl({
url: `${apiUrl}/api/chat`,
method: 'POST',
headers: {
'Content-Type': 'application/json',
@ -104,13 +105,13 @@ export class OllamaService implements IMCQGenerationService {
})
});
if (!response.ok) {
const errorText = await response.text();
if (response.status !== 200) {
const errorText = response.text;
console.error('Ollama API error:', response.status, errorText);
throw new Error(`API request failed (${response.status}): ${errorText}`);
}
const data = await response.json();
const data = response.json;
// Ollama's non-streaming chat response structure is typically { model, created_at, message: { role, content }, done }
if (!data.message || !data.message.content) {
console.error('Invalid API response format from Ollama:', data);

View file

@ -1,4 +1,4 @@
import { Notice } from 'obsidian';
import { Notice, requestUrl } from 'obsidian';
import SpaceforgePlugin from '../main';
import { MCQQuestion, MCQSet } from '../models/mcq';
import { IMCQGenerationService } from './mcq-generation-service';
@ -89,7 +89,8 @@ export class OpenAIService implements IMCQGenerationService {
console.log(`Making API request to OpenAI using model: ${model} with difficulty: ${difficulty}`);
try {
const response = await fetch('https://api.openai.com/v1/chat/completions', {
const response = await requestUrl({
url: 'https://api.openai.com/v1/chat/completions',
method: 'POST',
headers: {
'Content-Type': 'application/json',
@ -104,13 +105,13 @@ export class OpenAIService implements IMCQGenerationService {
})
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({ message: response.statusText }));
if (response.status !== 200) {
const errorData = response.json || { message: response.text };
console.error('OpenAI API error:', response.status, errorData);
throw new Error(`API request failed (${response.status}): ${errorData.error?.message || errorData.message || 'Unknown error'}`);
}
const data = await response.json();
const data = response.json;
if (!data.choices || !data.choices.length || !data.choices[0].message || !data.choices[0].message.content) {
console.error('Invalid API response format from OpenAI:', data);
throw new Error('Invalid API response format from OpenAI - missing content');

View file

@ -1,4 +1,4 @@
import { Notice } from 'obsidian'; // Removed TFile as it wasn't used
import { Notice, requestUrl } from 'obsidian'; // Removed TFile as it wasn't used
import SpaceforgePlugin from '../main';
import { MCQQuestion, MCQSet } from '../models/mcq';
import { IMCQGenerationService } from './mcq-generation-service';
@ -147,7 +147,8 @@ For example:
? settings.mcqBasicSystemPrompt
: settings.mcqAdvancedSystemPrompt;
const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {
const response = await requestUrl({
url: 'https://openrouter.ai/api/v1/chat/completions',
method: 'POST',
headers: {
'Content-Type': 'application/json',
@ -169,20 +170,19 @@ For example:
]
})
});
if (!response.ok) {
const errorText = await response.text();
console.error('OpenRouter API error:', errorText);
throw new Error(`API request failed (${response.status}): ${errorText}`);
if (response.status !== 200) {
console.error('OpenRouter API error:', response.text);
throw new Error(`API request failed (${response.status}): ${response.text}`);
}
const data = await response.json();
const data = response.json;
if (!data.choices || !data.choices.length || !data.choices[0].message) {
console.error('Invalid API response format from OpenRouter:', data);
throw new Error('Invalid API response format from OpenRouter - missing choices');
}
return data.choices[0].message.content;
} catch (error) {
console.error('Error in OpenRouter API request:', error);

View file

@ -1,4 +1,4 @@
import { Notice } from 'obsidian';
import { Notice, requestUrl } from 'obsidian';
import SpaceforgePlugin from '../main';
import { MCQQuestion, MCQSet } from '../models/mcq';
import { IMCQGenerationService } from './mcq-generation-service';
@ -89,7 +89,8 @@ export class TogetherService implements IMCQGenerationService {
console.log(`Making API request to Together AI using model: ${model} with difficulty: ${difficulty}`);
try {
const response = await fetch('https://api.together.xyz/v1/chat/completions', {
const response = await requestUrl({
url: 'https://api.together.xyz/v1/chat/completions',
method: 'POST',
headers: {
'Content-Type': 'application/json',
@ -105,13 +106,13 @@ export class TogetherService implements IMCQGenerationService {
})
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({ message: response.statusText }));
if (response.status !== 200) {
const errorData = response.json || { message: response.text };
console.error('Together AI API error:', response.status, errorData);
throw new Error(`API request failed (${response.status}): ${errorData.error?.message || errorData.message || 'Unknown error'}`);
}
const data = await response.json();
const data = response.json;
if (!data.choices || !data.choices.length || !data.choices[0].message || !data.choices[0].message.content) {
console.error('Invalid API response format from Together AI:', data);
throw new Error('Invalid API response format from Together AI - missing content');

412
main.js
View file

@ -1234,6 +1234,7 @@ var ReviewModal = class extends import_obsidian2.Modal {
}
}
const infoText = contentEl.createDiv("review-info-text");
infoText.empty();
if (schedule) {
const file = this.app.vault.getAbstractFileByPath(this.path);
const fileName = file instanceof import_obsidian2.TFile ? file.basename : this.path;
@ -1277,7 +1278,6 @@ var ReviewModal = class extends import_obsidian2.Modal {
onClose() {
const { contentEl } = this;
contentEl.empty();
console.log("Review modal closed for path: " + this.path);
}
};
@ -1929,11 +1929,7 @@ var ConsolidatedMCQModal = class extends import_obsidian5.Modal {
const progressPercent = Math.round((this.currentQuestionIndex + 1) / this.allQuestions.length * 100);
contentEl.setAttribute("data-progress", progressPercent.toString());
progressEl.setText(`Question ${this.currentQuestionIndex + 1} of ${this.allQuestions.length}`);
const progressCounter = contentEl.createDiv();
progressCounter.style.textAlign = "center";
progressCounter.style.marginBottom = "12px";
progressCounter.style.fontSize = "0.9em";
progressCounter.style.color = "var(--mcq-text-muted)";
const progressCounter = contentEl.createDiv("mcq-progress-counter");
progressCounter.setText(`${this.allQuestions.length} questions from ${this.mcqSets.length} notes`);
const noteInfoEl = contentEl.createDiv("mcq-note-info");
noteInfoEl.setText(`Question from: ${this.allQuestions[this.currentQuestionIndex].fileName}`);
@ -2178,10 +2174,7 @@ var ConsolidatedMCQModal = class extends import_obsidian5.Modal {
this.onComplete(results);
const { contentEl } = this;
contentEl.empty();
const headerEl = contentEl.createEl("h2", { text: "MCQ Review Complete" });
headerEl.style.color = "var(--mcq-primary)";
headerEl.style.textAlign = "center";
headerEl.style.marginBottom = "24px";
const headerEl = contentEl.createEl("h2", { text: "MCQ Review Complete", cls: "mcq-review-complete-header" });
const totalCorrectOverall = this.answers.filter((a) => a.correct && a.attempts <= 1).length;
const totalQuestionsOverall = this.allQuestions.length;
const overallScore = totalQuestionsOverall > 0 ? totalCorrectOverall / totalQuestionsOverall : 0;
@ -2189,30 +2182,24 @@ var ConsolidatedMCQModal = class extends import_obsidian5.Modal {
const scoreEl = contentEl.createDiv("mcq-score");
const scoreTextEl = scoreEl.createDiv("mcq-score-text");
scoreTextEl.setText(`Overall Score: ${scorePercentOverall}%`);
const performanceIndicator = scoreEl.createDiv();
performanceIndicator.style.marginTop = "8px";
performanceIndicator.style.fontSize = "1.1em";
const performanceIndicator = scoreEl.createDiv("mcq-performance-indicator");
if (scorePercentOverall >= 90) {
performanceIndicator.setText("\u{1F393} Excellent Performance!");
performanceIndicator.style.color = "var(--mcq-correct)";
performanceIndicator.addClass("excellent");
} else if (scorePercentOverall >= 70) {
performanceIndicator.setText("\u{1F44D} Good Work!");
performanceIndicator.style.color = "var(--mcq-primary)";
performanceIndicator.addClass("good");
} else if (scorePercentOverall >= 50) {
performanceIndicator.setText("\u{1F504} Keep Practicing");
performanceIndicator.style.color = "var(--mcq-warning)";
performanceIndicator.addClass("needs-improvement");
} else {
performanceIndicator.setText("\u{1F4DA} More Review Recommended");
performanceIndicator.style.color = "var(--mcq-text-muted)";
performanceIndicator.addClass("review-recommended");
}
const statsEl = scoreEl.createDiv();
statsEl.style.marginTop = "12px";
statsEl.style.fontSize = "0.9em";
statsEl.style.color = "var(--mcq-text-muted)";
const statsEl = scoreEl.createDiv("mcq-stats-summary");
statsEl.setText(`${totalCorrectOverall} correct out of ${totalQuestionsOverall} questions`);
const noteScoresEl = contentEl.createDiv("mcq-note-scores");
const scoreHeading = noteScoresEl.createEl("h3", { text: "Scores by Note" });
scoreHeading.style.marginTop = "20px";
const scoreHeading = noteScoresEl.createEl("h3", { text: "Scores by Note", cls: "mcq-note-scores-heading" });
const sortedNotes = Object.keys(noteScores).sort((a, b2) => noteScores[b2].score - noteScores[a].score);
for (const notePath of sortedNotes) {
const noteScore = noteScores[notePath];
@ -2229,55 +2216,32 @@ var ConsolidatedMCQModal = class extends import_obsidian5.Modal {
cls: "mcq-note-score-value"
});
if (noteScore.score >= 0.7) {
scoreTextValueEl.style.backgroundColor = "rgba(76, 175, 80, 0.1)";
scoreTextValueEl.style.color = "var(--mcq-correct)";
scoreTextValueEl.style.border = "1px solid var(--mcq-correct)";
scoreTextValueEl.addClass("high-score");
} else if (noteScore.score >= 0.5) {
scoreTextValueEl.style.backgroundColor = "rgba(255, 152, 0, 0.1)";
scoreTextValueEl.style.color = "var(--mcq-warning)";
scoreTextValueEl.style.border = "1px solid var(--mcq-warning)";
scoreTextValueEl.addClass("medium-score");
} else {
scoreTextValueEl.style.backgroundColor = "rgba(244, 67, 54, 0.1)";
scoreTextValueEl.style.color = "var(--mcq-incorrect)";
scoreTextValueEl.style.border = "1px solid var(--mcq-incorrect)";
scoreTextValueEl.addClass("low-score");
}
const progressBar = noteScoreEl.createDiv();
progressBar.style.height = "4px";
progressBar.style.backgroundColor = "var(--background-modifier-border)";
progressBar.style.borderRadius = "2px";
progressBar.style.marginTop = "6px";
progressBar.style.position = "relative";
progressBar.style.overflow = "hidden";
const progressFill = progressBar.createDiv();
progressFill.style.position = "absolute";
progressFill.style.left = "0";
progressFill.style.top = "0";
progressFill.style.height = "100%";
const progressBar = noteScoreEl.createDiv("mcq-progress-bar");
const progressFill = progressBar.createDiv("mcq-progress-fill");
progressFill.style.width = `${scorePercent}%`;
progressFill.style.transition = "width 0.5s ease";
if (noteScore.score >= 0.7) {
progressFill.style.backgroundColor = "var(--mcq-correct)";
progressFill.addClass("high-score");
} else if (noteScore.score >= 0.5) {
progressFill.style.backgroundColor = "var(--mcq-warning)";
progressFill.addClass("medium-score");
} else {
progressFill.style.backgroundColor = "var(--mcq-incorrect)";
progressFill.addClass("low-score");
}
}
const breakdownContainer = contentEl.createDiv("mcq-detailed-breakdown");
breakdownContainer.style.marginTop = "24px";
breakdownContainer.createEl("h3", { text: "Detailed Question Breakdown" });
this.allQuestions.forEach((question, index) => {
const questionEl = breakdownContainer.createDiv("mcq-breakdown-item");
questionEl.style.marginBottom = "12px";
questionEl.style.padding = "8px";
questionEl.style.border = "1px solid var(--background-modifier-border)";
questionEl.style.borderRadius = "4px";
const questionHeader = questionEl.createDiv();
questionHeader.createSpan({ text: `Q${index + 1} (from ${question.fileName}): `, cls: "mcq-breakdown-q-header" });
questionHeader.createSpan({ text: question.question });
const userAnswer = this.answers.find((a) => a.questionIndex === index);
const userAnswerTextEl = questionEl.createDiv();
userAnswerTextEl.style.marginLeft = "10px";
const userAnswerTextEl = questionEl.createDiv("mcq-user-answer-text");
let userAnswerDisplay = "Not answered";
if (userAnswer && userAnswer.selectedAnswerIndex !== -1 && userAnswer.selectedAnswerIndex < question.choices.length) {
userAnswerDisplay = question.choices[userAnswer.selectedAnswerIndex];
@ -2288,29 +2252,23 @@ var ConsolidatedMCQModal = class extends import_obsidian5.Modal {
const correctnessText = userAnswer.correct ? " (Correct)" : " (Incorrect)";
userAnswerTextEl.createSpan({ text: "Your answer: " });
const userAnswerSpan = userAnswerTextEl.createSpan({ text: userAnswerDisplay });
const correctnessSpan = userAnswerTextEl.createSpan({ text: correctnessText });
correctnessSpan.style.fontWeight = "bold";
const correctnessSpan = userAnswerTextEl.createSpan({ text: correctnessText, cls: "mcq-correctness-indicator" });
if (userAnswer.correct) {
userAnswerSpan.style.color = "var(--mcq-correct)";
correctnessSpan.style.color = "var(--mcq-correct)";
userAnswerSpan.addClass("correct");
correctnessSpan.addClass("correct");
} else {
userAnswerSpan.style.color = "var(--mcq-incorrect)";
correctnessSpan.style.color = "var(--mcq-incorrect)";
userAnswerSpan.addClass("incorrect");
correctnessSpan.addClass("incorrect");
}
} else {
userAnswerTextEl.createSpan({ text: "Your answer: " + userAnswerDisplay });
userAnswerTextEl.style.fontStyle = "italic";
}
const correctAnswerEl = questionEl.createDiv();
correctAnswerEl.style.marginLeft = "10px";
const correctAnswerEl = questionEl.createDiv("mcq-correct-answer");
correctAnswerEl.createSpan({ text: "Correct answer: " });
const correctAnswerSpan = correctAnswerEl.createSpan({ text: question.choices[question.correctAnswerIndex] });
correctAnswerSpan.style.color = "var(--mcq-correct-answer-text)";
correctAnswerSpan.style.fontWeight = "bold";
correctAnswerEl.createSpan({ text: question.choices[question.correctAnswerIndex], cls: "mcq-correct-answer-text" });
});
const closeBtn = contentEl.createEl("button", "mcq-close-btn");
closeBtn.setText("Close");
closeBtn.style.marginTop = "24px";
const closeBtn = contentEl.createEl("button", { cls: "mcq-close-btn", text: "Close" });
closeBtn.addEventListener("click", () => {
this.close();
});
@ -2469,7 +2427,6 @@ var BatchReviewModal = class extends import_obsidian6.Modal {
this.allMCQSets,
(results) => {
this.results = results;
console.log("Consolidated MCQ review complete with results:", results);
this.recordAllReviews(results).then(() => {
this.open();
this.showSummary();
@ -2477,7 +2434,6 @@ var BatchReviewModal = class extends import_obsidian6.Modal {
}
);
consolidatedModal.open();
console.log(`Opened consolidated MCQ modal with ${this.allMCQSets.length} MCQ sets`);
} catch (error) {
console.error("Error showing consolidated MCQ UI:", error);
new import_obsidian6.Notice("Error showing MCQ review. Falling back to manual review.");
@ -3041,23 +2997,10 @@ var MCQModal = class extends import_obsidian8.Modal {
const existingAnswer = this.session.answers.find((a) => a.questionIndex === questionIndex);
if (existingAnswer && existingAnswer.attempts >= 2) {
const skipContainer = newQuestionContainer.createDiv("mcq-skip-container");
skipContainer.style.marginBottom = "12px";
skipContainer.style.textAlign = "right";
const skipButton = skipContainer.createEl("button", { text: "Show Answer & Continue", cls: "mcq-skip-button" });
skipButton.style.backgroundColor = "var(--text-muted)";
skipButton.style.color = "white";
skipButton.style.padding = "4px 8px";
skipButton.style.borderRadius = "4px";
skipButton.style.cursor = "pointer";
skipButton.style.border = "none";
skipButton.addEventListener("click", () => {
const correctIndex = question.correctAnswerIndex;
const correctAnswerDisplay = newQuestionContainer.createDiv("mcq-correct-answer-display");
correctAnswerDisplay.style.backgroundColor = "rgba(76, 175, 80, 0.1)";
correctAnswerDisplay.style.padding = "10px";
correctAnswerDisplay.style.borderRadius = "4px";
correctAnswerDisplay.style.marginBottom = "10px";
correctAnswerDisplay.style.border = "1px solid #4caf50";
const correctLabel = correctAnswerDisplay.createDiv();
correctLabel.style.fontWeight = "bold";
correctLabel.style.marginBottom = "4px";
@ -3070,16 +3013,18 @@ var MCQModal = class extends import_obsidian8.Modal {
existingAnswer.correct = false;
existingAnswer.attempts += 1;
} else {
this.session.answers.push({ questionIndex, selectedAnswerIndex: -1, correct: false, timeToAnswer: (Date.now() - this.questionStartTime) / 1e3, attempts: 3 });
this.session.answers.push({
questionIndex,
selectedAnswerIndex: -1,
correct: false,
timeToAnswer: (Date.now() - this.questionStartTime) / 1e3,
attempts: 3
});
}
const continueBtn = correctAnswerDisplay.createEl("button", { text: "Continue to Next Question", cls: "mcq-continue-button" });
continueBtn.style.marginTop = "10px";
continueBtn.style.backgroundColor = "var(--interactive-accent)";
continueBtn.style.color = "white";
continueBtn.style.padding = "6px 12px";
continueBtn.style.borderRadius = "4px";
continueBtn.style.cursor = "pointer";
continueBtn.style.border = "none";
const continueBtn = correctAnswerDisplay.createEl("button", {
text: "Continue to Next Question",
cls: "mcq-continue-button"
});
continueBtn.addEventListener("click", () => {
this.session.currentQuestionIndex++;
this.questionStartTime = Date.now();
@ -3108,10 +3053,6 @@ var MCQModal = class extends import_obsidian8.Modal {
const textSpan = choiceBtn.createSpan("mcq-choice-text");
textSpan.setText(choice || "(Empty choice)");
const shortcutHint = choiceBtn.createSpan("mcq-shortcut-hint");
shortcutHint.style.fontSize = "0.8em";
shortcutHint.style.color = "var(--mcq-text-muted)";
shortcutHint.style.marginLeft = "auto";
shortcutHint.style.paddingLeft = "10px";
shortcutHint.setText(`${String.fromCharCode(65 + index)} or ${index + 1}`);
choiceBtn.addEventListener("click", () => this.handleAnswer(index));
});
@ -3133,7 +3074,6 @@ var MCQModal = class extends import_obsidian8.Modal {
answer.timeToAnswer = timeToAnswer;
answer.attempts += 1;
if (answer.attempts >= 2 && !answer.correct) {
console.log(`Question ${questionIndex + 1} has ${answer.attempts} attempts, marking as zero points`);
answer.selectedAnswerIndex = -1;
}
} else {
@ -3176,7 +3116,7 @@ var MCQModal = class extends import_obsidian8.Modal {
this.session.completedAt = Date.now();
this.plugin.mcqService.saveMCQSession(this.session);
this.plugin.savePluginData();
contentEl.createEl("h2").setText("Review Complete");
contentEl.createEl("h2", { text: "Review Complete" });
const scoreEl = contentEl.createDiv("mcq-score");
const scoreTextEl = scoreEl.createDiv("mcq-score-text");
const scorePercentage = this.session.score;
@ -3226,14 +3166,11 @@ var MCQModal = class extends import_obsidian8.Modal {
if (ratingDetails) {
const detailsEl = scoreEl.createDiv("mcq-score-details");
detailsEl.setText(ratingDetails);
detailsEl.style.fontSize = "0.9em";
detailsEl.style.color = "var(--text-muted)";
detailsEl.style.marginTop = "4px";
}
const resultsEl = contentEl.createDiv("mcq-results");
resultsEl.createEl("h3").setText("Question Results");
resultsEl.createEl("h3", { text: "Question Results" });
if (this.session.answers.length === 0) {
resultsEl.createDiv("mcq-no-answers").setText("No questions were answered in this session.");
resultsEl.createDiv({ cls: "mcq-no-answers", text: "No questions were answered in this session." });
} else {
this.session.answers.forEach((answer) => {
try {
@ -3241,39 +3178,38 @@ var MCQModal = class extends import_obsidian8.Modal {
if (!question || !question.choices)
return;
const resultItem = resultsEl.createDiv("mcq-result-item");
resultItem.createDiv("mcq-result-question").setText(question.question || "Question text missing");
resultItem.createDiv({ cls: "mcq-result-question", text: question.question || "Question text missing" });
if (answer.attempts > 1) {
if (answer.selectedAnswerIndex !== -1) {
const yourAnswer = resultItem.createDiv("mcq-result-your-answer");
yourAnswer.createSpan("mcq-result-label").setText("Your final answer (correct after multiple attempts): ");
yourAnswer.createSpan("mcq-result-correct").setText(question.choices[answer.selectedAnswerIndex] || "(invalid choice)");
yourAnswer.createSpan({ cls: "mcq-result-label", text: "Your final answer (correct after multiple attempts): " });
yourAnswer.createSpan({ cls: "mcq-result-correct", text: question.choices[answer.selectedAnswerIndex] || "(invalid choice)" });
} else {
const yourAnswer = resultItem.createDiv("mcq-result-your-answer");
yourAnswer.createSpan("mcq-result-label").setText("Your answer: ");
yourAnswer.createSpan("mcq-result-incorrect").setText('(Incorrect - used "Show Answer" option)');
yourAnswer.createSpan({ cls: "mcq-result-label", text: "Your answer: " });
yourAnswer.createSpan({ cls: "mcq-result-incorrect", text: '(Incorrect - used "Show Answer" option)' });
}
const correctAnswer = resultItem.createDiv("mcq-result-correct-answer");
correctAnswer.createSpan("mcq-result-label").setText("Correct answer: ");
correctAnswer.createSpan("mcq-result-correct").setText(question.choices[question.correctAnswerIndex] || "(invalid choice)");
correctAnswer.createSpan({ cls: "mcq-result-label", text: "Correct answer: " });
correctAnswer.createSpan({ cls: "mcq-result-correct", text: question.choices[question.correctAnswerIndex] || "(invalid choice)" });
} else {
const yourAnswer = resultItem.createDiv("mcq-result-your-answer");
yourAnswer.createSpan("mcq-result-label").setText("Your answer: ");
yourAnswer.createSpan(answer.correct ? "mcq-result-correct" : "mcq-result-incorrect").setText(question.choices[answer.selectedAnswerIndex] || "(invalid choice)");
yourAnswer.createSpan({ cls: "mcq-result-label", text: "Your answer: " });
yourAnswer.createSpan({ cls: answer.correct ? "mcq-result-correct" : "mcq-result-incorrect", text: question.choices[answer.selectedAnswerIndex] || "(invalid choice)" });
if (!answer.correct) {
const correctAnswer = resultItem.createDiv("mcq-result-correct-answer");
correctAnswer.createSpan("mcq-result-label").setText("Correct answer: ");
correctAnswer.createSpan("mcq-result-correct").setText(question.choices[question.correctAnswerIndex] || "(invalid choice)");
correctAnswer.createSpan({ cls: "mcq-result-label", text: "Correct answer: " });
correctAnswer.createSpan({ cls: "mcq-result-correct", text: question.choices[question.correctAnswerIndex] || "(invalid choice)" });
}
}
resultItem.createDiv("mcq-result-attempts").setText(`Attempts: ${answer.attempts}`);
resultItem.createDiv("mcq-result-time").setText(`Time: ${Math.round(answer.timeToAnswer)} seconds`);
resultItem.createDiv({ cls: "mcq-result-attempts", text: `Attempts: ${answer.attempts}` });
resultItem.createDiv({ cls: "mcq-result-time", text: `Time: ${Math.round(answer.timeToAnswer)} seconds` });
} catch (error) {
console.error("Error displaying answer result:", error);
}
});
}
const closeBtn = contentEl.createEl("button", { cls: "mcq-close-btn" });
closeBtn.setText("Close");
const closeBtn = contentEl.createEl("button", { cls: "mcq-close-btn", text: "Close" });
closeBtn.addEventListener("click", () => {
if (this.onCompleteCallback) {
this.onCompleteCallback(this.notePath, this.session.score, true);
@ -3282,10 +3218,9 @@ var MCQModal = class extends import_obsidian8.Modal {
});
} catch (error) {
console.error("Error completing MCQ session:", error);
contentEl.createEl("h2").setText("Error Completing Session");
contentEl.createEl("p").setText("There was an error completing the MCQ session. Please try again.");
const errorCloseBtn = contentEl.createEl("button", { cls: "mcq-close-btn" });
errorCloseBtn.setText("Close");
contentEl.createEl("h2", { text: "Error Completing Session" });
contentEl.createEl("p", { text: "There was an error completing the MCQ session. Please try again." });
const errorCloseBtn = contentEl.createEl("button", { cls: "mcq-close-btn", text: "Close" });
errorCloseBtn.addEventListener("click", () => this.close());
}
}
@ -3987,9 +3922,7 @@ var LinkAnalyzer = class {
*/
static findStartingNode(nodes) {
var _a, _b, _c, _d;
console.log("LinkAnalyzer: Starting node selection process...");
if (Object.keys(nodes).length === 0) {
console.log("LinkAnalyzer: No nodes found, returning empty array.");
return [];
}
const folderNodes = {};
@ -4003,35 +3936,25 @@ var LinkAnalyzer = class {
for (const folderPath in folderNodes) {
const folderName = ((_a = folderPath.split("/").pop()) == null ? void 0 : _a.toLowerCase()) || "";
const filesInFolder = folderNodes[folderPath];
console.log(`LinkAnalyzer: Checking for exact match with folder name "${folderName}" in folder "${folderPath}"...`);
for (const path of filesInFolder) {
const fileName = ((_b = path.split("/").pop()) == null ? void 0 : _b.toLowerCase()) || "";
const fileNameWithoutExt = fileName.replace(/\.md$/, "");
if (fileNameWithoutExt === folderName) {
console.log(`LinkAnalyzer: Selected main file with exact folder name match: ${path}`);
const fileName = ((_b = path.split("/").pop()) == null ? void 0 : _b.toLowerCase().replace(/\.md$/, "")) || "";
if (fileName === folderName) {
return [path];
}
}
console.log(`LinkAnalyzer: Checking for partial match with folder name "${folderName}"...`);
for (const path of filesInFolder) {
const fileName = ((_c = path.split("/").pop()) == null ? void 0 : _c.toLowerCase()) || "";
const fileNameWithoutExt = fileName.replace(/\.md$/, "");
if (fileNameWithoutExt.includes(folderName) || folderName.includes(fileNameWithoutExt)) {
console.log(`LinkAnalyzer: Selected main file with partial folder name match: ${path}`);
const fileName = ((_c = path.split("/").pop()) == null ? void 0 : _c.toLowerCase().replace(/\.md$/, "")) || "";
if (fileName.includes(folderName) || folderName.includes(fileName)) {
return [path];
}
}
console.log("LinkAnalyzer: Checking for common index or main file patterns...");
for (const path of filesInFolder) {
const fileName = ((_d = path.split("/").pop()) == null ? void 0 : _d.toLowerCase()) || "";
const fileNameWithoutExt = fileName.replace(/\.md$/, "");
if (fileNameWithoutExt === "index" || fileNameWithoutExt === "main" || fileNameWithoutExt.includes("index") || fileNameWithoutExt.includes("readme") || fileNameWithoutExt.includes("main")) {
console.log(`LinkAnalyzer: Selected main file by standard name pattern: ${path}`);
const fileName = ((_d = path.split("/").pop()) == null ? void 0 : _d.toLowerCase().replace(/\.md$/, "")) || "";
if (fileName === "index" || fileName === "main" || fileName.includes("index") || fileName.includes("readme") || fileName.includes("main")) {
return [path];
}
}
}
console.log("LinkAnalyzer: Checking for files with the most outgoing links...");
const sortedNodes = Object.values(nodes).sort((a, b2) => {
const regularLinkDiff = b2.regularLinks.length - a.regularLinks.length;
if (regularLinkDiff !== 0) {
@ -4040,10 +3963,8 @@ var LinkAnalyzer = class {
return b2.outgoingLinks.length - a.outgoingLinks.length;
});
if (sortedNodes.length > 0 && (sortedNodes[0].regularLinks.length > 0 || sortedNodes[0].outgoingLinks.length > 0)) {
console.log(`LinkAnalyzer: Selected main file by link count: ${sortedNodes[0].path} with ${sortedNodes[0].regularLinks.length} regular links`);
return [sortedNodes[0].path];
}
console.log(`LinkAnalyzer: No clear main file identified, using first file as root node`);
return Object.keys(nodes).length > 0 ? [Object.keys(nodes)[0]] : [];
}
/**
@ -4072,10 +3993,8 @@ var LinkAnalyzer = class {
fileFolders.set(path, folderPath);
}
const mainFolder = fileFolders.get(startNodePath) || "";
console.log(`LinkAnalyzer: Starting ordered traversal from: ${startNodePath} in main folder: ${mainFolder || "vault root"}`);
const traverse = (currentNodePath, currentDepth = 0, currentMainFolder) => {
if (visited.has(currentNodePath)) {
console.log(`${" ".repeat(currentDepth)}LinkAnalyzer: Already visited: ${currentNodePath}`);
return;
}
const node = nodes[currentNodePath];
@ -4083,26 +4002,21 @@ var LinkAnalyzer = class {
console.warn(`${" ".repeat(currentDepth)}LinkAnalyzer: Node not found for path: ${currentNodePath}`);
return;
}
console.log(`${" ".repeat(currentDepth)}LinkAnalyzer: Traversing: ${currentNodePath}`);
visited.add(currentNodePath);
traversalOrder.push(currentNodePath);
const currentNodeFolder = fileFolders.get(currentNodePath) || "";
const linksToFollow = node.regularLinks.length > 0 ? node.regularLinks : node.outgoingLinks;
for (const linkedPath of linksToFollow) {
if (!nodes[linkedPath]) {
console.log(`${" ".repeat(currentDepth + 1)}LinkAnalyzer: Skipping unknown linked path: ${linkedPath}`);
continue;
}
const linkedNodeFolder = fileFolders.get(linkedPath) || "";
if (nodes[linkedPath] && linkedNodeFolder.startsWith(currentMainFolder)) {
if (!visited.has(linkedPath)) {
console.log(`${" ".repeat(currentDepth + 1)}LinkAnalyzer: Following link within main hierarchy to: ${linkedPath}`);
traverse(linkedPath, currentDepth + 1, currentMainFolder);
} else {
console.log(`${" ".repeat(currentDepth + 1)}LinkAnalyzer: Link to already visited node in main hierarchy: ${linkedPath}`);
}
} else {
console.log(`${" ".repeat(currentDepth + 1)}LinkAnalyzer: Ignoring link outside main hierarchy or unknown node: ${linkedPath} (from ${currentNodeFolder} to ${linkedNodeFolder})`);
}
}
};
@ -4111,7 +4025,6 @@ var LinkAnalyzer = class {
} else {
console.warn(`LinkAnalyzer: Start node ${startNodePath} not found in nodes. Traversal order might be empty or incomplete.`);
}
console.log(`LinkAnalyzer: Final traversal order from ${startNodePath} (in folder '${mainFolder || "vault root"}') has ${traversalOrder.length} files: ${traversalOrder.join(" -> ")}`);
return traversalOrder;
}
/**
@ -4180,7 +4093,6 @@ var LinkAnalyzer = class {
seenLinks.add(resolvedPath);
}
}
console.log(`LinkAnalyzer: Analyzed links in ${filePath}, found ${resolvedLinks.length} unique links in order`);
return resolvedLinks;
} catch (error) {
console.error(`LinkAnalyzer: Error analyzing links in ${filePath}:`, error);
@ -4285,7 +4197,6 @@ var ContextMenuHandler = class {
this.plugin.registerEvent(
this.plugin.app.workspace.on("file-menu", this.handleFileMenuEvent.bind(this))
);
console.log("Unified context menu handler registered for 'file-menu'.");
}
/**
* Handles the 'file-menu' event for TAbstractFile (could be TFile or TFolder).
@ -4295,10 +4206,8 @@ var ContextMenuHandler = class {
*/
handleFileMenuEvent(menu, abstractFile) {
if (abstractFile instanceof import_obsidian11.TFolder) {
console.log("Folder right-clicked:", abstractFile.path);
this.addFolderMenuItems(menu, abstractFile);
} else if (abstractFile instanceof import_obsidian11.TFile && abstractFile.extension === "md") {
console.log("Markdown file right-clicked:", abstractFile.path);
this.addFileMenuItems(menu, abstractFile);
}
}
@ -4350,15 +4259,12 @@ var ContextMenuHandler = class {
* @param folder Target folder
*/
addFolderMenuItems(menu, folder) {
console.log("Adding menu items for folder:", folder.path);
try {
menu.addItem((item) => {
item.setTitle("Add folder to review").setIcon("calendar-plus").onClick(() => {
console.log("Context menu: Adding folder to review:", folder.path);
this.addFolderToReview(folder);
});
});
console.log("Menu items added successfully for folder:", folder.path);
} catch (error) {
console.error("Error adding folder menu items:", error);
}
@ -4369,7 +4275,6 @@ var ContextMenuHandler = class {
* @param folder Target folder
*/
async addFolderToReview(folder) {
console.log("Processing folder:", folder.path);
new import_obsidian11.Notice(`Analyzing folder structure for "${folder.name}"...`);
try {
const allFiles = this.plugin.app.vault.getMarkdownFiles().filter((file) => {
@ -4392,7 +4297,6 @@ var ContextMenuHandler = class {
const folderName = folder.name.toLowerCase();
if (fileName === folderName) {
mainFilePath = file.path;
console.log(`Found main file with exact folder name match: ${mainFilePath}`);
break;
}
}
@ -4402,7 +4306,6 @@ var ContextMenuHandler = class {
const folderName = folder.name.toLowerCase();
if (fileName.includes(folderName) || folderName.includes(fileName) || fileName === "index" || fileName === "main" || fileName.includes("index") || fileName.includes("main")) {
mainFilePath = file.path;
console.log(`Found main file by partial name match: ${mainFilePath}`);
break;
}
}
@ -4411,7 +4314,6 @@ var ContextMenuHandler = class {
const activeFile = this.plugin.app.workspace.getActiveFile();
if (activeFile && activeFile.extension === "md" && allFiles.some((f) => f.path === activeFile.path)) {
mainFilePath = activeFile.path;
console.log(`Using active note as main file: ${mainFilePath}`);
}
}
let traversalOrder = [];
@ -4443,17 +4345,14 @@ var ContextMenuHandler = class {
}
};
if (mainFilePath) {
console.log(`Starting traversal from identified main file: ${mainFilePath}`);
await processLinksRecursively(mainFilePath);
}
const sortedAllFiles = [...allFiles].sort((a, b2) => a.path.localeCompare(b2.path));
for (const file of sortedAllFiles) {
if (!visited.has(file.path)) {
console.log(`Processing new branch or unlinked file: ${file.path}`);
await processLinksRecursively(file.path);
}
}
console.log(`Final traversal order determined with ${traversalOrder.length} files.`);
const count = await this.plugin.reviewScheduleService.scheduleNotesInOrder(traversalOrder);
if (count > 0) {
await this.plugin.savePluginData();
@ -5062,11 +4961,9 @@ var NoteItemRenderer = class {
if (note.scheduleCategory === "initial") {
const totalInitialSteps = this.plugin.settings.initialScheduleCustomIntervals.length;
const currentStepDisplay = note.reviewCount < totalInitialSteps ? note.reviewCount + 1 : totalInitialSteps;
phaseEl.innerHTML = `
<div title="Initial">Initial</div>
<div title="${currentStepDisplay}/${totalInitialSteps}">${currentStepDisplay}/${totalInitialSteps}</div>
<div class="phase-time" title="${formattedTime}">${formattedTime}</div>
`;
phaseEl.createDiv({ title: "Initial", text: "Initial" });
phaseEl.createDiv({ title: `${currentStepDisplay}/${totalInitialSteps}`, text: `${currentStepDisplay}/${totalInitialSteps}` });
const phaseTimeEl = phaseEl.createDiv({ cls: "phase-time", title: formattedTime, text: formattedTime });
phaseEl.addClass("review-phase-initial");
} else {
phaseEl.setText(note.scheduleCategory === "graduated" ? "Graduated" : "Spaced");
@ -6204,6 +6101,8 @@ var ReviewSidebarView = class extends import_obsidian16.ItemView {
this.lastSelectedNotePath = null;
this.lastScrollPosition = 0;
this.expandedUpcomingDayKey = null;
// State for upcoming section
this.resizeObserver = null;
this.listViewRenderer = null;
// Persistent UI elements
this.mainContainer = null;
@ -6238,7 +6137,7 @@ var ReviewSidebarView = class extends import_obsidian16.ItemView {
this.plugin.events.off("sidebar-update", this.refresh.bind(this));
}
ensureBaseStructure() {
const contentEl = this.containerEl || this.contentEl;
const contentEl = this.containerEl;
if (!contentEl)
return;
if (!this.mainContainer) {
@ -6503,7 +6402,7 @@ var ReviewSidebarView = class extends import_obsidian16.ItemView {
sidebarViewType: this.plugin.settings.sidebarViewType
};
}
async setViewState(state, e) {
async setViewState(state) {
var _a, _b, _c;
if (!state)
return;
@ -7006,7 +6905,6 @@ var SpaceforgeSettingTab = class extends import_obsidian17.PluginSettingTab {
openRouterModel: this.plugin.settings.openRouterModel
};
window.localStorage.setItem("spaceforge-api-settings", JSON.stringify(apiSettings));
console.log("Updated API settings backup with MCQ state");
} catch (e) {
console.error("Error updating API settings backup:", e);
}
@ -7030,7 +6928,7 @@ var SpaceforgeSettingTab = class extends import_obsidian17.PluginSettingTab {
this.plugin.settings.openRouterApiKey = value;
await this.plugin.savePluginData();
}));
apiKeyContainer.createEl("div", { text: "Get your API key at https://openrouter.ai/keys" });
apiKeyContainer.createEl("div").setText("Get your API key at https://openrouter.ai/keys");
new import_obsidian17.Setting(mcqSection).setName("OpenRouter Model").setDesc("Model identifier from OpenRouter (e.g., openai/gpt-4.1-mini)").addText((text) => text.setPlaceholder("Enter OpenRouter model identifier").setValue(this.plugin.settings.openRouterModel).onChange(async (value) => {
this.plugin.settings.openRouterModel = value;
await this.plugin.savePluginData();
@ -7120,7 +7018,7 @@ var SpaceforgeSettingTab = class extends import_obsidian17.PluginSettingTab {
await this.plugin.savePluginData();
}));
const difficultyContainer = mcqFormattingGrid.createEl("div");
new import_obsidian17.Setting(difficultyContainer).setName("MCQ difficulty").setDesc("Complexity level").addDropdown((dropdown) => dropdown.addOption("basic", "Basic recall").addOption("advanced", "Advanced understanding").setValue(this.plugin.settings.mcqDifficulty).onChange(async (value) => {
new import_obsidian17.Setting(difficultyContainer).setName("MCQ difficulty").setDesc("Complexity level").addDropdown((dropdown) => dropdown.addOption("basic" /* Basic */, "Basic recall").addOption("advanced" /* Advanced */, "Advanced understanding").setValue(this.plugin.settings.mcqDifficulty).onChange(async (value) => {
this.plugin.settings.mcqDifficulty = value;
await this.plugin.savePluginData();
}));
@ -7259,15 +7157,28 @@ var SpaceforgeSettingTab = class extends import_obsidian17.PluginSettingTab {
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" });
ratingsTable.innerHTML = `
<thead><tr><th>Rating (0-5)</th><th>Description</th><th>Effect on Interval</th></tr></thead>
<tbody>
<tr><td>0-2</td><td>Incorrect / struggled</td><td>Resets, shortest interval</td></tr>
<tr><td>3</td><td>Correct with difficulty</td><td>Small increase</td></tr>
<tr><td>4</td><td>Correct with hesitation</td><td>Moderate increase</td></tr>
<tr><td>5</td><td>Perfect recall</td><td>Largest increase</td></tr>
</tbody>
`;
const thead = ratingsTable.createTHead();
const tbody = ratingsTable.createTBody();
const headerRow = thead.insertRow();
headerRow.createEl("th", { text: "Rating (0-5)" });
headerRow.createEl("th", { text: "Description" });
headerRow.createEl("th", { text: "Effect on Interval" });
const row1 = tbody.insertRow();
row1.createEl("td", { text: "0-2" });
row1.createEl("td", { text: "Incorrect / struggled" });
row1.createEl("td", { text: "Resets, shortest interval" });
const row2 = tbody.insertRow();
row2.createEl("td", { text: "3" });
row2.createEl("td", { text: "Correct with difficulty" });
row2.createEl("td", { text: "Small increase" });
const row3 = tbody.insertRow();
row3.createEl("td", { text: "4" });
row3.createEl("td", { text: "Correct with hesitation" });
row3.createEl("td", { text: "Moderate increase" });
const row4 = tbody.insertRow();
row4.createEl("td", { text: "5" });
row4.createEl("td", { text: "Perfect recall" });
row4.createEl("td", { text: "Largest increase" });
} else if (algorithm === "fsrs") {
container.createEl("h4", { text: "About the FSRS Algorithm" });
container.createEl("p", {
@ -7282,15 +7193,28 @@ var SpaceforgeSettingTab = class extends import_obsidian17.PluginSettingTab {
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" });
ratingsTable.innerHTML = `
<thead><tr><th>Rating (1-4)</th><th>Description</th><th>Effect on Stability/Difficulty</th></tr></thead>
<tbody>
<tr><td>1 (Again)</td><td>Forgot the card</td><td>Decreases stability, may increase difficulty</td></tr>
<tr><td>2 (Hard)</td><td>Recalled with significant difficulty</td><td>Smaller increase in stability</td></tr>
<tr><td>3 (Good)</td><td>Recalled correctly</td><td>Standard increase in stability</td></tr>
<tr><td>4 (Easy)</td><td>Recalled very easily</td><td>Largest increase in stability, may decrease difficulty</td></tr>
</tbody>
`;
const thead = ratingsTable.createTHead();
const tbody = ratingsTable.createTBody();
const headerRow = thead.insertRow();
headerRow.createEl("th", { text: "Rating (1-4)" });
headerRow.createEl("th", { text: "Description" });
headerRow.createEl("th", { text: "Effect on Stability/Difficulty" });
const row1 = tbody.insertRow();
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: "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: "Recalled correctly" });
row3.createEl("td", { text: "Standard increase in stability" });
const row4 = tbody.insertRow();
row4.createEl("td", { text: "4 (Easy)" });
row4.createEl("td", { text: "Recalled very easily" });
row4.createEl("td", { text: "Largest increase in stability, may decrease difficulty" });
container.createEl("p", { text: "FSRS parameters (weights, retention, etc.) can be tuned, but the defaults are generally effective." });
}
}
@ -7405,7 +7329,8 @@ ${noteContent}`;
try {
console.log(`Making API request to OpenRouter using model: ${model} with difficulty: ${difficulty}`);
const systemPrompt = difficulty === "basic" ? settings.mcqBasicSystemPrompt : settings.mcqAdvancedSystemPrompt;
const response = await fetch("https://openrouter.ai/api/v1/chat/completions", {
const response = await (0, import_obsidian18.requestUrl)({
url: "https://openrouter.ai/api/v1/chat/completions",
method: "POST",
headers: {
"Content-Type": "application/json",
@ -7429,12 +7354,11 @@ ${noteContent}`;
]
})
});
if (!response.ok) {
const errorText = await response.text();
console.error("OpenRouter API error:", errorText);
throw new Error(`API request failed (${response.status}): ${errorText}`);
if (response.status !== 200) {
console.error("OpenRouter API error:", response.text);
throw new Error(`API request failed (${response.status}): ${response.text}`);
}
const data = await response.json();
const data = response.json;
if (!data.choices || !data.choices.length || !data.choices[0].message) {
console.error("Invalid API response format from OpenRouter:", data);
throw new Error("Invalid API response format from OpenRouter - missing choices");
@ -7618,7 +7542,8 @@ ${noteContent}`;
const systemPrompt = difficulty === "basic" ? settings.mcqBasicSystemPrompt : settings.mcqAdvancedSystemPrompt;
console.log(`Making API request to OpenAI using model: ${model} with difficulty: ${difficulty}`);
try {
const response = await fetch("https://api.openai.com/v1/chat/completions", {
const response = await (0, import_obsidian19.requestUrl)({
url: "https://api.openai.com/v1/chat/completions",
method: "POST",
headers: {
"Content-Type": "application/json",
@ -7632,12 +7557,12 @@ ${noteContent}`;
]
})
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({ message: response.statusText }));
if (response.status !== 200) {
const errorData = response.json || { message: response.text };
console.error("OpenAI API error:", response.status, errorData);
throw new Error(`API request failed (${response.status}): ${((_a = errorData.error) == null ? void 0 : _a.message) || errorData.message || "Unknown error"}`);
}
const data = await response.json();
const data = response.json;
if (!data.choices || !data.choices.length || !data.choices[0].message || !data.choices[0].message.content) {
console.error("Invalid API response format from OpenAI:", data);
throw new Error("Invalid API response format from OpenAI - missing content");
@ -7794,8 +7719,8 @@ ${noteContent}`;
const systemPrompt = difficulty === "basic" ? settings.mcqBasicSystemPrompt : settings.mcqAdvancedSystemPrompt;
console.log(`Making API request to Ollama at ${apiUrl} using model: ${model} with difficulty: ${difficulty}`);
try {
const response = await fetch(`${apiUrl}/api/chat`, {
// Common Ollama chat endpoint
const response = await (0, import_obsidian20.requestUrl)({
url: `${apiUrl}/api/chat`,
method: "POST",
headers: {
"Content-Type": "application/json"
@ -7810,12 +7735,12 @@ ${noteContent}`;
// Ensure we get the full response, not a stream
})
});
if (!response.ok) {
const errorText = await response.text();
if (response.status !== 200) {
const errorText = response.text;
console.error("Ollama API error:", response.status, errorText);
throw new Error(`API request failed (${response.status}): ${errorText}`);
}
const data = await response.json();
const data = response.json;
if (!data.message || !data.message.content) {
console.error("Invalid API response format from Ollama:", data);
throw new Error("Invalid API response format from Ollama - missing message content");
@ -7968,7 +7893,8 @@ ${noteContent}`;
console.log(`Making API request to Gemini using model: ${model}`);
const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${apiKey}`;
try {
const response = await fetch(apiUrl, {
const response = await (0, import_obsidian21.requestUrl)({
url: apiUrl,
method: "POST",
headers: {
"Content-Type": "application/json"
@ -7983,13 +7909,13 @@ ${noteContent}`;
// }
})
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({ message: response.statusText }));
if (response.status !== 200) {
const errorData = response.json;
console.error("Gemini API error:", response.status, errorData);
const errorMessage = ((_a = errorData == null ? void 0 : errorData.error) == null ? void 0 : _a.message) || (errorData == null ? void 0 : errorData.message) || "Unknown error";
throw new Error(`API request failed (${response.status}): ${errorMessage}`);
}
const data = await response.json();
const data = response.json;
if (!data.candidates || !data.candidates.length || !data.candidates[0].content || !data.candidates[0].content.parts || !data.candidates[0].content.parts.length || !data.candidates[0].content.parts[0].text) {
console.error("Invalid API response format from Gemini:", data);
throw new Error("Invalid API response format from Gemini - missing content");
@ -8147,7 +8073,8 @@ ${noteContent}`;
const systemPrompt = difficulty === "basic" ? settings.mcqBasicSystemPrompt : settings.mcqAdvancedSystemPrompt;
console.log(`Making API request to Claude using model: ${model} with difficulty: ${difficulty}`);
try {
const response = await fetch("https://api.anthropic.com/v1/messages", {
const response = await (0, import_obsidian22.requestUrl)({
url: "https://api.anthropic.com/v1/messages",
method: "POST",
headers: {
"Content-Type": "application/json",
@ -8164,12 +8091,12 @@ ${noteContent}`;
]
})
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({ message: response.statusText }));
if (response.status !== 200) {
const errorData = response.json || { message: response.text };
console.error("Claude API error:", response.status, errorData);
throw new Error(`API request failed (${response.status}): ${((_a = errorData.error) == null ? void 0 : _a.message) || errorData.message || "Unknown error"}`);
}
const data = await response.json();
const data = response.json;
if (!data.content || !data.content.length || !data.content[0].text) {
console.error("Invalid API response format from Claude:", data);
throw new Error("Invalid API response format from Claude - missing content");
@ -8353,7 +8280,8 @@ ${noteContent}`;
const systemPrompt = difficulty === "basic" ? settings.mcqBasicSystemPrompt : settings.mcqAdvancedSystemPrompt;
console.log(`Making API request to Together AI using model: ${model} with difficulty: ${difficulty}`);
try {
const response = await fetch("https://api.together.xyz/v1/chat/completions", {
const response = await (0, import_obsidian23.requestUrl)({
url: "https://api.together.xyz/v1/chat/completions",
method: "POST",
headers: {
"Content-Type": "application/json",
@ -8369,12 +8297,12 @@ ${noteContent}`;
]
})
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({ message: response.statusText }));
if (response.status !== 200) {
const errorData = response.json || { message: response.text };
console.error("Together AI API error:", response.status, errorData);
throw new Error(`API request failed (${response.status}): ${((_a = errorData.error) == null ? void 0 : _a.message) || errorData.message || "Unknown error"}`);
}
const data = await response.json();
const data = response.json;
if (!data.choices || !data.choices.length || !data.choices[0].message || !data.choices[0].message.content) {
console.error("Invalid API response format from Together AI:", data);
throw new Error("Invalid API response format from Together AI - missing content");
@ -8921,14 +8849,7 @@ var ReviewScheduleService = class {
*/
calculateSM2Schedule(currentInterval, currentEase, qualityRating, repetitionCount = 0, daysLate = 0, isSkipped = false) {
if (isSkipped || daysLate > 0) {
console.log(`[Modified SM-2] Processing ${isSkipped ? "skipped" : "overdue"} item:
- Days late: ${daysLate}
- Original quality: ${qualityRating}
- Current interval: ${currentInterval}
- Current ease: ${currentEase / 100}`);
const q_eff = isSkipped ? Math.max(0, qualityRating - 1) : 0;
console.log(`[Modified SM-2] Applied penalty:
- Effective quality: ${q_eff}`);
let ease2 = currentEase / 100;
ease2 = ease2 + (0.1 - (5 - q_eff) * (0.08 + (5 - q_eff) * 0.02));
ease2 = Math.max(1.3, ease2);
@ -8940,10 +8861,6 @@ var ReviewScheduleService = class {
repetitionCount: 1
// Reset repetition count to 1
};
console.log(`[Modified SM-2] Result for ${isSkipped ? "skipped" : "overdue"} item:
- New interval: ${result.interval} day(s)
- New ease: ${result.ease / 100}
- New repetition count: ${result.repetitionCount}`);
return result;
}
let ease = currentEase / 100;
@ -9031,7 +8948,6 @@ var ReviewScheduleService = class {
const schedule = this.schedules[path];
if (!schedule)
return;
console.log(`Skipping note ${path} with algorithm ${schedule.schedulingAlgorithm}`);
const effectiveReviewDate = currentReviewDate || Date.now();
const reviewDateObj = new Date(effectiveReviewDate);
if (schedule.schedulingAlgorithm === "fsrs") {
@ -9120,7 +9036,6 @@ var ReviewScheduleService = class {
async advanceNote(path) {
const schedule = this.schedules[path];
if (!schedule) {
console.warn(`[ReviewScheduleService] Advance: Schedule not found for path ${path}`);
return false;
}
const todayUTCMidnight = DateUtils.startOfUTCDay(/* @__PURE__ */ new Date());
@ -9184,7 +9099,6 @@ var ReviewScheduleService = class {
const content = await this.plugin.app.vault.read(file);
return EstimationUtils.estimateReviewTime(file, content);
} catch (error) {
console.error("Error estimating review time:", error);
return 60;
}
}
@ -9268,7 +9182,6 @@ var ReviewScheduleService = class {
async updateCustomNoteOrder(order) {
const uniqueValidPaths = Array.from(new Set(order)).filter((path) => this.schedules[path] !== void 0);
this.customNoteOrder = uniqueValidPaths;
console.log(`Updated custom note order with ${this.customNoteOrder.length} unique entries`);
if (this.plugin.events) {
this.plugin.events.emit("sidebar-update");
}
@ -9326,7 +9239,6 @@ var ReviewScheduleService = class {
this.customNoteOrder.push(newPath);
}
}
console.log(`Handled rename from ${oldPath} to ${newPath}. Schedule updated.`);
if (this.plugin.events) {
this.plugin.events.emit("sidebar-update");
}
@ -9359,7 +9271,6 @@ var ReviewScheduleService = class {
}
}
}
console.log(`Converted ${convertedCount} SM-2 cards to FSRS.`);
if (this.plugin.events) {
this.plugin.events.emit("sidebar-update");
}
@ -9389,7 +9300,6 @@ var ReviewScheduleService = class {
}
}
}
console.log(`Converted ${convertedCount} FSRS cards to SM-2.`);
if (this.plugin.events) {
this.plugin.events.emit("sidebar-update");
}
@ -9642,11 +9552,9 @@ var MCQService = class {
this.mcqSets = {};
return null;
}
const sets = Object.values(this.mcqSets).filter((set) => set && set.notePath === notePath).sort((a, b2) => b2.generatedAt - a.generatedAt);
if (sets.length > 0 && sets[0].questions && sets[0].questions.length > 0) {
const sets = Object.values(this.mcqSets).filter((set) => set && set.notePath === notePath && set.questions && set.questions.length > 0).sort((a, b2) => b2.generatedAt - a.generatedAt);
if (sets.length > 0) {
return sets[0];
} else if (sets.length > 0) {
console.warn("Found MCQ set but it contains no valid questions:", sets[0]);
}
return null;
} catch (error) {
@ -9717,9 +9625,7 @@ var MCQService = class {
if (mcqSet) {
mcqSet.needsQuestionRegeneration = true;
this.saveMCQSet(mcqSet);
console.log(`MCQSet for ${notePath} flagged for regeneration.`);
} else {
console.log(`No MCQSet found for ${notePath} to flag for regeneration.`);
}
}
};
@ -9941,7 +9847,6 @@ var PomodoroService = class {
const remainingMs = this.state.pomodoroEndTimeMs - now;
this.state.pomodoroTimeLeftInSeconds = Math.max(0, Math.round(remainingMs / 1e3));
if (remainingMs <= 0) {
console.log("Pomodoro timer ended while inactive. Handling potential transition.");
this.handleTimerEnd(true);
}
} else if (!this.state.pomodoroIsRunning) {
@ -10024,11 +9929,11 @@ var SpaceforgePlugin = class extends import_obsidian26.Plugin {
this.addCommand({
id: "add-selected-file-to-review",
name: "Add Selected File to Review Schedule (File Explorer)",
hotkeys: [{ modifiers: ["Alt", "Shift"], key: "s" }],
callback: () => {
const fileExplorerLeaf = this.app.workspace.getLeavesOfType("file-explorer")[0];
if (fileExplorerLeaf && fileExplorerLeaf.view && fileExplorerLeaf.view.file) {
const selectedFile = fileExplorerLeaf.view.file;
const viewWithFile = fileExplorerLeaf == null ? void 0 : fileExplorerLeaf.view;
if (viewWithFile == null ? void 0 : viewWithFile.file) {
const selectedFile = viewWithFile.file;
if (selectedFile instanceof import_obsidian26.TFile && selectedFile.extension === "md") {
this.reviewScheduleService.scheduleNoteForReview(selectedFile.path).then(() => this.savePluginData());
new import_obsidian26.Notice(`Added "${selectedFile.path}" to review schedule.`);
@ -10136,7 +10041,10 @@ var SpaceforgePlugin = class extends import_obsidian26.Plugin {
styleEl.remove();
let existingData = {};
try {
existingData = await this.loadData() || {};
const loaded = await this.loadData();
if (loaded) {
existingData = loaded;
}
} catch (loadError) {
console.warn("Could not load existing data during unload:", loadError);
}
@ -10164,7 +10072,6 @@ var SpaceforgePlugin = class extends import_obsidian26.Plugin {
}
if (this.pomodoroService)
this.pomodoroService.destroy();
this.app.workspace.detachLeavesOfType("spaceforge-review-schedule");
}
// private async _getEffectiveDataPathFromLocalStorage(): Promise<string | null> {
// const customPath = await this.app.loadLocalStorage('spaceforgeCustomDataPath');
@ -10389,7 +10296,6 @@ var SpaceforgePlugin = class extends import_obsidian26.Plugin {
this.addCommand({
id: "spaceforge-next-review-note",
name: "Next Review Note",
hotkeys: [{ modifiers: ["Alt", "Shift"], key: "/" }],
callback: () => {
this.navigationController.navigateToNextNote();
}
@ -10397,7 +10303,6 @@ var SpaceforgePlugin = class extends import_obsidian26.Plugin {
this.addCommand({
id: "spaceforge-previous-review-note",
name: "Previous Review Note",
hotkeys: [{ modifiers: ["Alt", "Shift"], key: "." }],
callback: () => {
this.navigationController.navigateToPreviousNote();
}
@ -10405,7 +10310,6 @@ var SpaceforgePlugin = class extends import_obsidian26.Plugin {
this.addCommand({
id: "spaceforge-review-current-note",
name: "Review Current Note",
hotkeys: [{ modifiers: ["Alt", "Shift"], key: "," }],
callback: () => {
const activeFile = this.app.workspace.getActiveFile();
if (activeFile && activeFile instanceof import_obsidian26.TFile && activeFile.extension === "md") {
@ -10418,7 +10322,6 @@ var SpaceforgePlugin = class extends import_obsidian26.Plugin {
this.addCommand({
id: "spaceforge-add-current-note-to-review",
name: "Add Current Note to Review Schedule",
hotkeys: [{ modifiers: ["Alt", "Shift"], key: "\\\\" }],
callback: () => {
const activeFile = this.app.workspace.getActiveFile();
if (activeFile && activeFile instanceof import_obsidian26.TFile && activeFile.extension === "md") {
@ -10432,7 +10335,6 @@ var SpaceforgePlugin = class extends import_obsidian26.Plugin {
this.addCommand({
id: "spaceforge-add-current-folder-to-review",
name: "Add Current Note's Folder to Review Schedule",
hotkeys: [{ modifiers: ["Alt", "Shift"], key: "'" }],
callback: async () => {
const activeFile = this.app.workspace.getActiveFile();
if (activeFile && activeFile.parent && activeFile.parent instanceof import_obsidian26.TFolder) {

25
main.ts
View file

@ -120,11 +120,11 @@ export default class SpaceforgePlugin extends Plugin {
this.addCommand({
id: 'add-selected-file-to-review',
name: 'Add Selected File to Review Schedule (File Explorer)',
hotkeys: [{ modifiers: ['Alt', 'Shift'], key: 's' }],
callback: () => {
const fileExplorerLeaf = this.app.workspace.getLeavesOfType('file-explorer')[0];
if (fileExplorerLeaf && fileExplorerLeaf.view && (fileExplorerLeaf.view as any).file) {
const selectedFile = (fileExplorerLeaf.view as any).file;
const viewWithFile = fileExplorerLeaf?.view as { file?: TFile };
if (viewWithFile?.file) {
const selectedFile = viewWithFile.file;
if (selectedFile instanceof TFile && selectedFile.extension === 'md') {
this.reviewScheduleService.scheduleNoteForReview(selectedFile.path)
.then(() => this.savePluginData());
@ -233,8 +233,13 @@ export default class SpaceforgePlugin extends Plugin {
const styleEl = document.getElementById(this.stylesheetId);
if (styleEl) styleEl.remove();
let existingData = {};
try { existingData = await this.loadData() || {}; }
let existingData: Partial<SpaceforgePluginData> = {};
try {
const loaded = await this.loadData();
if (loaded) {
existingData = loaded;
}
}
catch (loadError) { console.warn("Could not load existing data during unload:", loadError); }
try {
@ -258,7 +263,6 @@ export default class SpaceforgePlugin extends Plugin {
} catch (error) { console.error('Error saving plugin data before unload:', error); }
if (this.pomodoroService) this.pomodoroService.destroy();
this.app.workspace.detachLeavesOfType('spaceforge-review-schedule');
}
// private async _getEffectiveDataPathFromLocalStorage(): Promise<string | null> {
@ -364,8 +368,8 @@ export default class SpaceforgePlugin extends Plugin {
this.pluginState = { ...DEFAULT_APP_DATA.pluginState };
if (rawLoadedData?.pluginState) {
this.pluginState = { ...this.pluginState, ...rawLoadedData.pluginState };
} else if (rawLoadedData && !rawLoadedData.pluginState && (rawLoadedData as any).schedules) { // Legacy check
this.pluginState = { ...this.pluginState, ...(rawLoadedData as any) };
} else if (rawLoadedData && !rawLoadedData.pluginState && (rawLoadedData as Partial<PluginStateData>).schedules) { // Legacy check
this.pluginState = { ...this.pluginState, ...(rawLoadedData as Partial<PluginStateData>) };
}
// Ensure pomodoro state is initialized if not present in loaded data
@ -548,7 +552,6 @@ export default class SpaceforgePlugin extends Plugin {
this.addCommand({
id: 'spaceforge-next-review-note',
name: 'Next Review Note',
hotkeys: [{ modifiers: ['Alt', 'Shift'], key: '/' }],
callback: () => {
this.navigationController.navigateToNextNote();
},
@ -557,7 +560,6 @@ export default class SpaceforgePlugin extends Plugin {
this.addCommand({
id: 'spaceforge-previous-review-note',
name: 'Previous Review Note',
hotkeys: [{ modifiers: ['Alt', 'Shift'], key: '.' }],
callback: () => {
this.navigationController.navigateToPreviousNote();
},
@ -566,7 +568,6 @@ export default class SpaceforgePlugin extends Plugin {
this.addCommand({
id: 'spaceforge-review-current-note',
name: 'Review Current Note',
hotkeys: [{ modifiers: ['Alt', 'Shift'], key: ',' }],
callback: () => {
const activeFile = this.app.workspace.getActiveFile();
if (activeFile && activeFile instanceof TFile && activeFile.extension === 'md') {
@ -580,7 +581,6 @@ export default class SpaceforgePlugin extends Plugin {
this.addCommand({
id: 'spaceforge-add-current-note-to-review',
name: 'Add Current Note to Review Schedule',
hotkeys: [{ modifiers: ['Alt', 'Shift'], key: '\\\\' }],
callback: () => {
const activeFile = this.app.workspace.getActiveFile();
if (activeFile && activeFile instanceof TFile && activeFile.extension === 'md') {
@ -596,7 +596,6 @@ export default class SpaceforgePlugin extends Plugin {
this.addCommand({
id: 'spaceforge-add-current-folder-to-review',
name: "Add Current Note's Folder to Review Schedule",
hotkeys: [{ modifiers: ['Alt', 'Shift'], key: "'" }],
callback: async () => {
const activeFile = this.app.workspace.getActiveFile();
if (activeFile && activeFile.parent && activeFile.parent instanceof TFolder) {

View file

@ -61,14 +61,12 @@ export class MCQService {
// Find the latest MCQ set for this note
const sets = Object.values(this.mcqSets)
.filter(set => set && set.notePath === notePath)
.filter(set => set && set.notePath === notePath && set.questions && set.questions.length > 0)
.sort((a, b) => b.generatedAt - a.generatedAt);
// Verify the set is valid before returning
if (sets.length > 0 && sets[0].questions && sets[0].questions.length > 0) {
if (sets.length > 0) {
return sets[0];
} else if (sets.length > 0) {
console.warn('Found MCQ set but it contains no valid questions:', sets[0]);
}
return null;
@ -157,9 +155,7 @@ export class MCQService {
if (mcqSet) {
mcqSet.needsQuestionRegeneration = true;
this.saveMCQSet(mcqSet); // Updates the in-memory reference; persistence handled by caller
console.log(`MCQSet for ${notePath} flagged for regeneration.`);
} else {
console.log(`No MCQSet found for ${notePath} to flag for regeneration.`);
}
}
}

View file

@ -280,7 +280,6 @@ export class PomodoroService {
// Timer should have ended while plugin was inactive/closed
// We could try to simulate the transition, but it gets complex.
// Simplest: Stop the timer, reset to the expected next state's duration.
console.log("Pomodoro timer ended while inactive. Handling potential transition.");
this.handleTimerEnd(true); // Treat as skipped to avoid sound, force transition logic
}
} else if (!this.state.pomodoroIsRunning) {

View file

@ -489,22 +489,12 @@ export class ReviewScheduleService {
// Handle overdue or skipped items according to modified SM-2 algorithm
if (isSkipped || daysLate > 0) {
// Debug log
console.log(`[Modified SM-2] Processing ${isSkipped ? 'skipped' : 'overdue'} item:
- Days late: ${daysLate}
- Original quality: ${qualityRating}
- Current interval: ${currentInterval}
- Current ease: ${currentEase/100}`);
// Determine effective quality rating:
// - If explicitly skipped, reduce quality by 1 (but not below 0)
// - If overdue, set quality to 0
// - If both overdue and explicitly skipped, prioritize the skip logic (user choice)
const q_eff = isSkipped ? Math.max(0, qualityRating - 1) : 0;
console.log(`[Modified SM-2] Applied penalty:
- Effective quality: ${q_eff}`);
// Convert ease from internal format (250 = 2.5) to SM-2 format (2.5)
let ease = currentEase / 100;
@ -522,11 +512,6 @@ export class ReviewScheduleService {
repetitionCount: 1 // Reset repetition count to 1
};
console.log(`[Modified SM-2] Result for ${isSkipped ? 'skipped' : 'overdue'} item:
- New interval: ${result.interval} day(s)
- New ease: ${result.ease/100}
- New repetition count: ${result.repetitionCount}`);
return result;
}
@ -656,7 +641,6 @@ export class ReviewScheduleService {
const schedule = this.schedules[path];
if (!schedule) return;
console.log(`Skipping note ${path} with algorithm ${schedule.schedulingAlgorithm}`);
const effectiveReviewDate = currentReviewDate || Date.now();
const reviewDateObj = new Date(effectiveReviewDate);
@ -755,7 +739,6 @@ export class ReviewScheduleService {
async advanceNote(path: string): Promise<boolean> {
const schedule = this.schedules[path];
if (!schedule) {
console.warn(`[ReviewScheduleService] Advance: Schedule not found for path ${path}`);
return false;
}
@ -854,7 +837,6 @@ export class ReviewScheduleService {
const content = await this.plugin.app.vault.read(file);
return EstimationUtils.estimateReviewTime(file, content);
} catch (error) {
console.error("Error estimating review time:", error);
return 60; // Default 1 minute
}
}
@ -938,7 +920,6 @@ export class ReviewScheduleService {
// Filter out duplicate paths and ensure we only store paths that exist in our schedules
const uniqueValidPaths = Array.from(new Set(order)).filter(path => this.schedules[path] !== undefined);
this.customNoteOrder = uniqueValidPaths;
console.log(`Updated custom note order with ${this.customNoteOrder.length} unique entries`);
// Data saving is now handled by main.ts after this method returns
@ -1021,7 +1002,6 @@ export class ReviewScheduleService {
}
}
console.log(`Handled rename from ${oldPath} to ${newPath}. Schedule updated.`);
// Notify any listeners (for UI updates)
// Data saving will be handled by the caller in main.ts
@ -1062,7 +1042,6 @@ export class ReviewScheduleService {
}
}
}
console.log(`Converted ${convertedCount} SM-2 cards to FSRS.`);
if (this.plugin.events) {
this.plugin.events.emit('sidebar-update'); // Notify UI to refresh
}
@ -1096,9 +1075,8 @@ export class ReviewScheduleService {
}
}
}
console.log(`Converted ${convertedCount} FSRS cards to SM-2.`);
if (this.plugin.events) {
this.plugin.events.emit('sidebar-update'); // Notify UI to refresh
}
}
}
}

View file

@ -164,7 +164,6 @@ export class BatchReviewModal extends Modal {
this.allMCQSets,
(results: Array<{ path: string, success: boolean, response: ReviewResponse, score?: number }>) => {
this.results = results;
console.log("Consolidated MCQ review complete with results:", results);
this.recordAllReviews(results).then(() => {
this.open();
this.showSummary();
@ -172,7 +171,6 @@ export class BatchReviewModal extends Modal {
}
);
consolidatedModal.open();
console.log(`Opened consolidated MCQ modal with ${this.allMCQSets.length} MCQ sets`);
} catch (error) {
console.error("Error showing consolidated MCQ UI:", error);
new Notice("Error showing MCQ review. Falling back to manual review.");

View file

@ -138,13 +138,9 @@ export class ConsolidatedMCQModal extends Modal {
progressEl.setText(`Question ${this.currentQuestionIndex + 1} of ${this.allQuestions.length}`);
// Display a progress counter badge
const progressCounter = contentEl.createDiv();
progressCounter.style.textAlign = 'center';
progressCounter.style.marginBottom = '12px';
progressCounter.style.fontSize = '0.9em';
progressCounter.style.color = 'var(--mcq-text-muted)';
const progressCounter = contentEl.createDiv('mcq-progress-counter');
progressCounter.setText(`${this.allQuestions.length} questions from ${this.mcqSets.length} notes`);
// Display note information with enhanced styling
const noteInfoEl = contentEl.createDiv('mcq-note-info');
noteInfoEl.setText(`Question from: ${this.allQuestions[this.currentQuestionIndex].fileName}`);
@ -527,11 +523,8 @@ export class ConsolidatedMCQModal extends Modal {
contentEl.empty();
// Display results header with stylized heading
const headerEl = contentEl.createEl('h2', { text: 'MCQ Review Complete' });
headerEl.style.color = 'var(--mcq-primary)';
headerEl.style.textAlign = 'center';
headerEl.style.marginBottom = '24px';
const headerEl = contentEl.createEl('h2', { text: 'MCQ Review Complete', cls: 'mcq-review-complete-header' });
// Display overall score with enhanced styling
const totalCorrectOverall = this.answers.filter(a => a.correct && a.attempts <= 1).length;
const totalQuestionsOverall = this.allQuestions.length;
@ -543,35 +536,29 @@ export class ConsolidatedMCQModal extends Modal {
scoreTextEl.setText(`Overall Score: ${scorePercentOverall}%`);
// Add performance indicator based on score
const performanceIndicator = scoreEl.createDiv();
performanceIndicator.style.marginTop = '8px';
performanceIndicator.style.fontSize = '1.1em';
const performanceIndicator = scoreEl.createDiv('mcq-performance-indicator');
if (scorePercentOverall >= 90) {
performanceIndicator.setText('🎓 Excellent Performance!');
performanceIndicator.style.color = 'var(--mcq-correct)';
performanceIndicator.addClass('excellent');
} else if (scorePercentOverall >= 70) {
performanceIndicator.setText('👍 Good Work!');
performanceIndicator.style.color = 'var(--mcq-primary)';
performanceIndicator.addClass('good');
} else if (scorePercentOverall >= 50) {
performanceIndicator.setText('🔄 Keep Practicing');
performanceIndicator.style.color = 'var(--mcq-warning)';
performanceIndicator.addClass('needs-improvement');
} else {
performanceIndicator.setText('📚 More Review Recommended');
performanceIndicator.style.color = 'var(--mcq-text-muted)';
performanceIndicator.addClass('review-recommended');
}
// Add stats summary
const statsEl = scoreEl.createDiv();
statsEl.style.marginTop = '12px';
statsEl.style.fontSize = '0.9em';
statsEl.style.color = 'var(--mcq-text-muted)';
const statsEl = scoreEl.createDiv('mcq-stats-summary');
statsEl.setText(`${totalCorrectOverall} correct out of ${totalQuestionsOverall} questions`);
// Display note scores with enhanced styling
const noteScoresEl = contentEl.createDiv('mcq-note-scores');
const scoreHeading = noteScoresEl.createEl('h3', { text: 'Scores by Note' });
scoreHeading.style.marginTop = '20px'; // Added margin
const scoreHeading = noteScoresEl.createEl('h3', { text: 'Scores by Note', cls: 'mcq-note-scores-heading' });
// Sort notes by score for better visualization (highest first)
const sortedNotes = Object.keys(noteScores).sort((a, b) => noteScores[b].score - noteScores[a].score);
@ -592,75 +579,51 @@ export class ConsolidatedMCQModal extends Modal {
text: `Score: ${scorePercent}% (${noteScore.correctAnswers}/${noteScore.totalQuestions})`,
cls: 'mcq-note-score-value'
});
// Style score badge based on performance
if (noteScore.score >= 0.7) {
scoreTextValueEl.style.backgroundColor = 'rgba(76, 175, 80, 0.1)';
scoreTextValueEl.style.color = 'var(--mcq-correct)';
scoreTextValueEl.style.border = '1px solid var(--mcq-correct)';
scoreTextValueEl.addClass('high-score');
} else if (noteScore.score >= 0.5) {
scoreTextValueEl.style.backgroundColor = 'rgba(255, 152, 0, 0.1)';
scoreTextValueEl.style.color = 'var(--mcq-warning)';
scoreTextValueEl.style.border = '1px solid var(--mcq-warning)';
scoreTextValueEl.addClass('medium-score');
} else {
scoreTextValueEl.style.backgroundColor = 'rgba(244, 67, 54, 0.1)';
scoreTextValueEl.style.color = 'var(--mcq-incorrect)';
scoreTextValueEl.style.border = '1px solid var(--mcq-incorrect)';
scoreTextValueEl.addClass('low-score');
}
// Add a visual progress bar
const progressBar = noteScoreEl.createDiv();
progressBar.style.height = '4px';
progressBar.style.backgroundColor = 'var(--background-modifier-border)';
progressBar.style.borderRadius = '2px';
progressBar.style.marginTop = '6px';
progressBar.style.position = 'relative';
progressBar.style.overflow = 'hidden';
const progressFill = progressBar.createDiv();
progressFill.style.position = 'absolute';
progressFill.style.left = '0';
progressFill.style.top = '0';
progressFill.style.height = '100%';
const progressBar = noteScoreEl.createDiv('mcq-progress-bar');
const progressFill = progressBar.createDiv('mcq-progress-fill');
progressFill.style.width = `${scorePercent}%`;
progressFill.style.transition = 'width 0.5s ease';
if (noteScore.score >= 0.7) {
progressFill.style.backgroundColor = 'var(--mcq-correct)';
progressFill.addClass('high-score');
} else if (noteScore.score >= 0.5) {
progressFill.style.backgroundColor = 'var(--mcq-warning)';
progressFill.addClass('medium-score');
} else {
progressFill.style.backgroundColor = 'var(--mcq-incorrect)';
progressFill.addClass('low-score');
}
}
// --- Detailed Question Breakdown ---
const breakdownContainer = contentEl.createDiv('mcq-detailed-breakdown');
breakdownContainer.style.marginTop = '24px';
breakdownContainer.createEl('h3', { text: 'Detailed Question Breakdown' });
this.allQuestions.forEach((question, index) => {
const questionEl = breakdownContainer.createDiv('mcq-breakdown-item');
questionEl.style.marginBottom = '12px';
questionEl.style.padding = '8px';
questionEl.style.border = '1px solid var(--background-modifier-border)';
questionEl.style.borderRadius = '4px';
const questionHeader = questionEl.createDiv();
questionHeader.createSpan({ text: `Q${index + 1} (from ${question.fileName}): `, cls: 'mcq-breakdown-q-header' });
questionHeader.createSpan({ text: question.question });
const userAnswer = this.answers.find(a => a.questionIndex === index);
const userAnswerTextEl = questionEl.createDiv();
userAnswerTextEl.style.marginLeft = '10px';
const userAnswerTextEl = questionEl.createDiv('mcq-user-answer-text');
let userAnswerDisplay = "Not answered";
if (userAnswer && userAnswer.selectedAnswerIndex !== -1 && userAnswer.selectedAnswerIndex < question.choices.length) {
userAnswerDisplay = question.choices[userAnswer.selectedAnswerIndex];
} else if (userAnswer && userAnswer.selectedAnswerIndex === -1) {
// This case should ideally not happen with the new logic in handleAnswer
// but good to have a fallback.
userAnswerDisplay = "Attempted, but no valid choice recorded";
userAnswerDisplay = "Attempted, but no valid choice recorded";
}
@ -668,33 +631,27 @@ export class ConsolidatedMCQModal extends Modal {
const correctnessText = userAnswer.correct ? ' (Correct)' : ' (Incorrect)';
userAnswerTextEl.createSpan({ text: 'Your answer: ' });
const userAnswerSpan = userAnswerTextEl.createSpan({ text: userAnswerDisplay });
const correctnessSpan = userAnswerTextEl.createSpan({ text: correctnessText });
correctnessSpan.style.fontWeight = 'bold';
const correctnessSpan = userAnswerTextEl.createSpan({ text: correctnessText, cls: 'mcq-correctness-indicator' });
if (userAnswer.correct) {
userAnswerSpan.style.color = 'var(--mcq-correct)';
correctnessSpan.style.color = 'var(--mcq-correct)';
userAnswerSpan.addClass('correct');
correctnessSpan.addClass('correct');
} else {
userAnswerSpan.style.color = 'var(--mcq-incorrect)';
correctnessSpan.style.color = 'var(--mcq-incorrect)';
userAnswerSpan.addClass('incorrect');
correctnessSpan.addClass('incorrect');
}
} else {
userAnswerTextEl.createSpan({ text: 'Your answer: ' + userAnswerDisplay });
userAnswerTextEl.style.fontStyle = 'italic';
userAnswerTextEl.createSpan({ text: 'Your answer: ' + userAnswerDisplay });
userAnswerTextEl.style.fontStyle = 'italic';
}
const correctAnswerEl = questionEl.createDiv();
correctAnswerEl.style.marginLeft = '10px';
const correctAnswerEl = questionEl.createDiv('mcq-correct-answer');
correctAnswerEl.createSpan({ text: 'Correct answer: ' });
const correctAnswerSpan = correctAnswerEl.createSpan({ text: question.choices[question.correctAnswerIndex] });
correctAnswerSpan.style.color = 'var(--mcq-correct-answer-text)'; // A distinct color for the correct answer text itself
correctAnswerSpan.style.fontWeight = 'bold';
correctAnswerEl.createSpan({ text: question.choices[question.correctAnswerIndex], cls: 'mcq-correct-answer-text' });
});
// Create close button
const closeBtn = contentEl.createEl('button', 'mcq-close-btn');
closeBtn.setText('Close');
closeBtn.style.marginTop = '24px'; // Added margin
const closeBtn = contentEl.createEl('button', {cls: 'mcq-close-btn', text: 'Close'});
closeBtn.addEventListener('click', () => {
this.close();
});

View file

@ -28,7 +28,6 @@ export class ContextMenuHandler {
this.plugin.registerEvent(
this.plugin.app.workspace.on("file-menu", this.handleFileMenuEvent.bind(this))
);
console.log("Unified context menu handler registered for 'file-menu'.");
}
/**
@ -40,11 +39,9 @@ export class ContextMenuHandler {
handleFileMenuEvent(menu: Menu, abstractFile: TAbstractFile): void {
if (abstractFile instanceof TFolder) {
// Handle folder context menu
console.log("Folder right-clicked:", abstractFile.path);
this.addFolderMenuItems(menu, abstractFile);
} else if (abstractFile instanceof TFile && abstractFile.extension === "md") {
// Handle markdown file context menu
console.log("Markdown file right-clicked:", abstractFile.path);
this.addFileMenuItems(menu, abstractFile);
}
}
@ -113,17 +110,14 @@ export class ContextMenuHandler {
* @param folder Target folder
*/
private addFolderMenuItems(menu: Menu, folder: TFolder): void {
console.log("Adding menu items for folder:", folder.path);
try {
menu.addItem((item) => {
item.setTitle("Add folder to review")
.setIcon("calendar-plus")
.onClick(() => {
console.log("Context menu: Adding folder to review:", folder.path);
this.addFolderToReview(folder);
});
});
console.log("Menu items added successfully for folder:", folder.path);
} catch (error) {
console.error("Error adding folder menu items:", error);
}
@ -135,7 +129,6 @@ export class ContextMenuHandler {
* @param folder Target folder
*/
async addFolderToReview(folder: TFolder): Promise<void> {
console.log("Processing folder:", folder.path);
// Show immediate notification
new Notice(`Analyzing folder structure for "${folder.name}"...`);
@ -174,7 +167,6 @@ export class ContextMenuHandler {
// Exact match first
if (fileName === folderName) {
mainFilePath = file.path;
console.log(`Found main file with exact folder name match: ${mainFilePath}`);
break;
}
}
@ -192,7 +184,6 @@ export class ContextMenuHandler {
fileName.includes('index') ||
fileName.includes('main')) {
mainFilePath = file.path;
console.log(`Found main file by partial name match: ${mainFilePath}`);
break;
}
}
@ -205,7 +196,6 @@ export class ContextMenuHandler {
activeFile.extension === "md" &&
allFiles.some(f => f.path === activeFile.path)) {
mainFilePath = activeFile.path;
console.log(`Using active note as main file: ${mainFilePath}`);
}
}
@ -218,7 +208,6 @@ export class ContextMenuHandler {
}
visited.add(path);
traversalOrder.push(path);
// console.log(`Added to traversal: ${path}`); // Keep console logs minimal for production
const links = await LinkAnalyzer.analyzeNoteLinks(
this.plugin.app.vault,
@ -238,7 +227,6 @@ export class ContextMenuHandler {
if (!visited.has(linkFile.path)) {
visited.add(linkFile.path);
traversalOrder.push(linkFile.path);
// console.log(`Added external link (no recursion): ${linkFile.path}`);
}
}
}
@ -246,7 +234,6 @@ export class ContextMenuHandler {
// If a mainFilePath was identified, process it first.
if (mainFilePath) {
console.log(`Starting traversal from identified main file: ${mainFilePath}`);
await processLinksRecursively(mainFilePath);
}
@ -256,12 +243,10 @@ export class ContextMenuHandler {
const sortedAllFiles = [...allFiles].sort((a,b) => a.path.localeCompare(b.path));
for (const file of sortedAllFiles) {
if (!visited.has(file.path)) {
console.log(`Processing new branch or unlinked file: ${file.path}`);
await processLinksRecursively(file.path);
}
}
console.log(`Final traversal order determined with ${traversalOrder.length} files.`);
// Schedule notes in the traversal order using the service
const count = await this.plugin.reviewScheduleService.scheduleNotesInOrder(traversalOrder);

View file

@ -223,24 +223,36 @@ export class MCQModal extends Modal {
const existingAnswer = this.session.answers.find(a => a.questionIndex === questionIndex);
if (existingAnswer && existingAnswer.attempts >= 2) {
const skipContainer = newQuestionContainer.createDiv('mcq-skip-container');
skipContainer.style.marginBottom = '12px'; skipContainer.style.textAlign = 'right';
const skipButton = skipContainer.createEl('button', { text: 'Show Answer & Continue', cls: 'mcq-skip-button' });
skipButton.style.backgroundColor = 'var(--text-muted)'; skipButton.style.color = 'white'; skipButton.style.padding = '4px 8px'; skipButton.style.borderRadius = '4px'; skipButton.style.cursor = 'pointer'; skipButton.style.border = 'none';
skipButton.addEventListener('click', () => {
const correctIndex = question.correctAnswerIndex;
const correctAnswerDisplay = newQuestionContainer.createDiv('mcq-correct-answer-display');
correctAnswerDisplay.style.backgroundColor = 'rgba(76, 175, 80, 0.1)'; correctAnswerDisplay.style.padding = '10px'; correctAnswerDisplay.style.borderRadius = '4px'; correctAnswerDisplay.style.marginBottom = '10px'; correctAnswerDisplay.style.border = '1px solid #4caf50';
const correctLabel = correctAnswerDisplay.createDiv(); correctLabel.style.fontWeight = 'bold'; correctLabel.style.marginBottom = '4px'; correctLabel.setText('Correct Answer:');
const correctText = correctAnswerDisplay.createDiv(); correctText.style.color = '#4caf50'; correctText.setText(String.fromCharCode(65 + correctIndex) + ') ' + question.choices[correctIndex]);
const correctLabel = correctAnswerDisplay.createDiv();
correctLabel.style.fontWeight = 'bold';
correctLabel.style.marginBottom = '4px';
correctLabel.setText('Correct Answer:');
const correctText = correctAnswerDisplay.createDiv();
correctText.style.color = '#4caf50';
correctText.setText(String.fromCharCode(65 + correctIndex) + ') ' + question.choices[correctIndex]);
if (existingAnswer) {
existingAnswer.selectedAnswerIndex = -1; existingAnswer.correct = false; existingAnswer.attempts += 1;
existingAnswer.selectedAnswerIndex = -1;
existingAnswer.correct = false;
existingAnswer.attempts += 1;
} else {
this.session.answers.push({ questionIndex, selectedAnswerIndex: -1, correct: false, timeToAnswer: (Date.now() - this.questionStartTime) / 1000, attempts: 3 });
this.session.answers.push({
questionIndex,
selectedAnswerIndex: -1,
correct: false,
timeToAnswer: (Date.now() - this.questionStartTime) / 1000,
attempts: 3
});
}
const continueBtn = correctAnswerDisplay.createEl('button', { text: 'Continue to Next Question', cls: 'mcq-continue-button' });
continueBtn.style.marginTop = '10px'; continueBtn.style.backgroundColor = 'var(--interactive-accent)'; continueBtn.style.color = 'white'; continueBtn.style.padding = '6px 12px'; continueBtn.style.borderRadius = '4px'; continueBtn.style.cursor = 'pointer'; continueBtn.style.border = 'none';
const continueBtn = correctAnswerDisplay.createEl('button', {
text: 'Continue to Next Question',
cls: 'mcq-continue-button'
});
continueBtn.addEventListener('click', () => {
this.session.currentQuestionIndex++;
this.questionStartTime = Date.now();
@ -266,7 +278,6 @@ export class MCQModal extends Modal {
const letterLabel = choiceBtn.createSpan('mcq-choice-letter'); letterLabel.setText(String.fromCharCode(65 + index) + ') ');
const textSpan = choiceBtn.createSpan('mcq-choice-text'); textSpan.setText(choice || '(Empty choice)');
const shortcutHint = choiceBtn.createSpan('mcq-shortcut-hint');
shortcutHint.style.fontSize = '0.8em'; shortcutHint.style.color = 'var(--mcq-text-muted)'; shortcutHint.style.marginLeft = 'auto'; shortcutHint.style.paddingLeft = '10px';
shortcutHint.setText(`${String.fromCharCode(65 + index)} or ${index + 1}`);
choiceBtn.addEventListener('click', () => this.handleAnswer(index));
});
@ -286,7 +297,6 @@ export class MCQModal extends Modal {
if (isCorrect) { answer.selectedAnswerIndex = selectedIndex; answer.correct = true; }
answer.timeToAnswer = timeToAnswer; answer.attempts += 1;
if (answer.attempts >= 2 && !answer.correct) {
console.log(`Question ${questionIndex+1} has ${answer.attempts} attempts, marking as zero points`);
answer.selectedAnswerIndex = -1; // Mark as failed but eventually correct
}
} else {
@ -333,7 +343,7 @@ export class MCQModal extends Modal {
this.plugin.mcqService.saveMCQSession(this.session);
this.plugin.savePluginData(); // Persist
contentEl.createEl('h2').setText('Review Complete'); // Corrected
contentEl.createEl('h2', { text: 'Review Complete' });
const scoreEl = contentEl.createDiv('mcq-score');
const scoreTextEl = scoreEl.createDiv('mcq-score-text');
const scorePercentage = this.session.score; // Score is 0-1
@ -380,56 +390,52 @@ export class MCQModal extends Modal {
if (ratingDetails) {
const detailsEl = scoreEl.createDiv('mcq-score-details');
detailsEl.setText(ratingDetails);
detailsEl.style.fontSize = '0.9em';
detailsEl.style.color = 'var(--text-muted)';
detailsEl.style.marginTop = '4px';
}
// Removed the old score indicator text (🎯 Excellent, etc.)
const resultsEl = contentEl.createDiv('mcq-results');
resultsEl.createEl('h3').setText('Question Results');
resultsEl.createEl('h3', { text: 'Question Results' });
if (this.session.answers.length === 0) {
resultsEl.createDiv('mcq-no-answers').setText('No questions were answered in this session.');
resultsEl.createDiv({ cls: 'mcq-no-answers', text: 'No questions were answered in this session.' });
} else {
this.session.answers.forEach(answer => {
try {
const question = this.mcqSet.questions[answer.questionIndex];
if (!question || !question.choices) return;
const resultItem = resultsEl.createDiv('mcq-result-item');
resultItem.createDiv('mcq-result-question').setText(question.question || 'Question text missing');
resultItem.createDiv({ cls: 'mcq-result-question', text: question.question || 'Question text missing' });
if (answer.attempts > 1) {
if (answer.selectedAnswerIndex !== -1) {
const yourAnswer = resultItem.createDiv('mcq-result-your-answer');
yourAnswer.createSpan('mcq-result-label').setText('Your final answer (correct after multiple attempts): ');
yourAnswer.createSpan('mcq-result-correct').setText(question.choices[answer.selectedAnswerIndex] || '(invalid choice)');
yourAnswer.createSpan({ cls: 'mcq-result-label', text: 'Your final answer (correct after multiple attempts): ' });
yourAnswer.createSpan({ cls: 'mcq-result-correct', text: question.choices[answer.selectedAnswerIndex] || '(invalid choice)' });
} else {
const yourAnswer = resultItem.createDiv('mcq-result-your-answer');
yourAnswer.createSpan('mcq-result-label').setText('Your answer: ');
yourAnswer.createSpan('mcq-result-incorrect').setText('(Incorrect - used "Show Answer" option)');
yourAnswer.createSpan({ cls: 'mcq-result-label', text: 'Your answer: ' });
yourAnswer.createSpan({ cls: 'mcq-result-incorrect', text: '(Incorrect - used "Show Answer" option)' });
}
const correctAnswer = resultItem.createDiv('mcq-result-correct-answer');
correctAnswer.createSpan('mcq-result-label').setText('Correct answer: ');
correctAnswer.createSpan('mcq-result-correct').setText(question.choices[question.correctAnswerIndex] || '(invalid choice)');
correctAnswer.createSpan({ cls: 'mcq-result-label', text: 'Correct answer: ' });
correctAnswer.createSpan({ cls: 'mcq-result-correct', text: question.choices[question.correctAnswerIndex] || '(invalid choice)' });
} else {
const yourAnswer = resultItem.createDiv('mcq-result-your-answer');
yourAnswer.createSpan('mcq-result-label').setText('Your answer: ');
yourAnswer.createSpan(answer.correct ? 'mcq-result-correct' : 'mcq-result-incorrect').setText(question.choices[answer.selectedAnswerIndex] || '(invalid choice)');
yourAnswer.createSpan({ cls: 'mcq-result-label', text: 'Your answer: ' });
yourAnswer.createSpan({ cls: answer.correct ? 'mcq-result-correct' : 'mcq-result-incorrect', text: question.choices[answer.selectedAnswerIndex] || '(invalid choice)' });
if (!answer.correct) {
const correctAnswer = resultItem.createDiv('mcq-result-correct-answer');
correctAnswer.createSpan('mcq-result-label').setText('Correct answer: ');
correctAnswer.createSpan('mcq-result-correct').setText(question.choices[question.correctAnswerIndex] || '(invalid choice)');
correctAnswer.createSpan({ cls: 'mcq-result-label', text: 'Correct answer: ' });
correctAnswer.createSpan({ cls: 'mcq-result-correct', text: question.choices[question.correctAnswerIndex] || '(invalid choice)' });
}
}
resultItem.createDiv('mcq-result-attempts').setText(`Attempts: ${answer.attempts}`);
resultItem.createDiv('mcq-result-time').setText(`Time: ${Math.round(answer.timeToAnswer)} seconds`);
resultItem.createDiv({ cls: 'mcq-result-attempts', text: `Attempts: ${answer.attempts}` });
resultItem.createDiv({ cls: 'mcq-result-time', text: `Time: ${Math.round(answer.timeToAnswer)} seconds` });
// FSRS/SM-2 data per question removed from here
} catch (error) { console.error('Error displaying answer result:', error); }
});
}
const closeBtn = contentEl.createEl('button', { cls: 'mcq-close-btn' });
closeBtn.setText('Close');
const closeBtn = contentEl.createEl('button', { cls: 'mcq-close-btn', text: 'Close' });
closeBtn.addEventListener('click', () => {
if (this.onCompleteCallback) {
// Pass the actual score and completion status
@ -439,10 +445,9 @@ export class MCQModal extends Modal {
});
} catch (error) {
console.error('Error completing MCQ session:', error);
contentEl.createEl('h2').setText('Error Completing Session'); // Corrected
contentEl.createEl('p').setText('There was an error completing the MCQ session. Please try again.'); // Corrected
const errorCloseBtn = contentEl.createEl('button', { cls: 'mcq-close-btn' });
errorCloseBtn.setText('Close'); // Set text separately
contentEl.createEl('h2', { text: 'Error Completing Session' });
contentEl.createEl('p', { text: 'There was an error completing the MCQ session. Please try again.' });
const errorCloseBtn = contentEl.createEl('button', { cls: 'mcq-close-btn', text: 'Close' });
errorCloseBtn.addEventListener('click', () => this.close());
}
}
@ -500,4 +505,4 @@ function getSm2RatingText(rating: number): string {
case 5: return "Perfect Recall";
default: return "Unknown";
}
}
}

View file

@ -129,6 +129,7 @@ export class ReviewModal extends Modal {
// Note Info Section
const infoText = contentEl.createDiv("review-info-text");
infoText.empty();
// const schedule = this.plugin.reviewScheduleService.schedules[this.path]; // Already fetched
if (schedule) {
@ -171,6 +172,5 @@ export class ReviewModal extends Modal {
onClose(): void {
const { contentEl } = this;
contentEl.empty();
console.log("Review modal closed for path: " + this.path);
}
}

View file

@ -1,7 +1,7 @@
import { App, Notice, PluginSettingTab, Setting, setIcon, TextAreaComponent } from 'obsidian'; // Added TextAreaComponent
import SpaceforgePlugin from '../main';
// Import ApiProvider, DEFAULT_SETTINGS, SpaceforgeSettings, and MCQQuestionAmountMode
import { ApiProvider, DEFAULT_SETTINGS, SpaceforgeSettings, MCQQuestionAmountMode } from '../models/settings';
import { ApiProvider, DEFAULT_SETTINGS, SpaceforgeSettings, MCQQuestionAmountMode, MCQDifficulty } from '../models/settings';
import { SpaceforgePluginData, DEFAULT_APP_DATA, DEFAULT_PLUGIN_STATE_DATA } from '../models/plugin-data'; // Import data structures
/**
@ -603,7 +603,6 @@ export class SpaceforgeSettingTab extends PluginSettingTab {
openRouterModel: this.plugin.settings.openRouterModel
};
window.localStorage.setItem('spaceforge-api-settings', JSON.stringify(apiSettings));
console.log("Updated API settings backup with MCQ state");
} catch (e) {
console.error("Error updating API settings backup:", e);
}
@ -660,7 +659,7 @@ export class SpaceforgeSettingTab extends PluginSettingTab {
await this.plugin.savePluginData();
}));
// Removed sf-setting-explain class
apiKeyContainer.createEl('div', { text: 'Get your API key at https://openrouter.ai/keys' });
apiKeyContainer.createEl('div').setText('Get your API key at https://openrouter.ai/keys');
new Setting(mcqSection)
.setName('OpenRouter Model')
@ -901,11 +900,11 @@ export class SpaceforgeSettingTab extends PluginSettingTab {
.setName('MCQ difficulty')
.setDesc('Complexity level')
.addDropdown(dropdown => dropdown
.addOption('basic', 'Basic recall')
.addOption('advanced', 'Advanced understanding')
.addOption(MCQDifficulty.Basic, 'Basic recall')
.addOption(MCQDifficulty.Advanced, 'Advanced understanding')
.setValue(this.plugin.settings.mcqDifficulty)
.onChange(async (value: string) => {
this.plugin.settings.mcqDifficulty = value as any;
.onChange(async (value: MCQDifficulty) => {
this.plugin.settings.mcqDifficulty = value;
await this.plugin.savePluginData();
}));
@ -1157,15 +1156,32 @@ export class SpaceforgeSettingTab extends PluginSettingTab {
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
ratingsTable.innerHTML = `
<thead><tr><th>Rating (0-5)</th><th>Description</th><th>Effect on Interval</th></tr></thead>
<tbody>
<tr><td>0-2</td><td>Incorrect / struggled</td><td>Resets, shortest interval</td></tr>
<tr><td>3</td><td>Correct with difficulty</td><td>Small increase</td></tr>
<tr><td>4</td><td>Correct with hesitation</td><td>Moderate increase</td></tr>
<tr><td>5</td><td>Perfect recall</td><td>Largest increase</td></tr>
</tbody>
`;
const thead = ratingsTable.createTHead();
const tbody = ratingsTable.createTBody();
const headerRow = thead.insertRow();
headerRow.createEl('th', { text: 'Rating (0-5)' });
headerRow.createEl('th', { text: 'Description' });
headerRow.createEl('th', { text: 'Effect on Interval' });
const row1 = tbody.insertRow();
row1.createEl('td', { text: '0-2' });
row1.createEl('td', { text: 'Incorrect / struggled' });
row1.createEl('td', { text: 'Resets, shortest interval' });
const row2 = tbody.insertRow();
row2.createEl('td', { text: '3' });
row2.createEl('td', { text: 'Correct with difficulty' });
row2.createEl('td', { text: 'Small increase' });
const row3 = tbody.insertRow();
row3.createEl('td', { text: '4' });
row3.createEl('td', { text: 'Correct with hesitation' });
row3.createEl('td', { text: 'Moderate increase' });
const row4 = tbody.insertRow();
row4.createEl('td', { text: '5' });
row4.createEl('td', { text: 'Perfect recall' });
row4.createEl('td', { text: 'Largest increase' });
} else if (algorithm === 'fsrs') {
container.createEl('h4', { text: 'About the FSRS Algorithm' });
container.createEl('p', {
@ -1182,15 +1198,32 @@ export class SpaceforgeSettingTab extends PluginSettingTab {
fsrsList.createEl('li', { text: 'Learning Steps: Initial short intervals for new cards (configurable).' });
const ratingsTable = container.createEl('table', { cls: 'sf-ratings-table' });
ratingsTable.innerHTML = `
<thead><tr><th>Rating (1-4)</th><th>Description</th><th>Effect on Stability/Difficulty</th></tr></thead>
<tbody>
<tr><td>1 (Again)</td><td>Forgot the card</td><td>Decreases stability, may increase difficulty</td></tr>
<tr><td>2 (Hard)</td><td>Recalled with significant difficulty</td><td>Smaller increase in stability</td></tr>
<tr><td>3 (Good)</td><td>Recalled correctly</td><td>Standard increase in stability</td></tr>
<tr><td>4 (Easy)</td><td>Recalled very easily</td><td>Largest increase in stability, may decrease difficulty</td></tr>
</tbody>
`;
const thead = ratingsTable.createTHead();
const tbody = ratingsTable.createTBody();
const headerRow = thead.insertRow();
headerRow.createEl('th', { text: 'Rating (1-4)' });
headerRow.createEl('th', { text: 'Description' });
headerRow.createEl('th', { text: 'Effect on Stability/Difficulty' });
const row1 = tbody.insertRow();
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: '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: 'Recalled correctly' });
row3.createEl('td', { text: 'Standard increase in stability' });
const row4 = tbody.insertRow();
row4.createEl('td', { text: '4 (Easy)' });
row4.createEl('td', { text: 'Recalled very easily' });
row4.createEl('td', { text: 'Largest increase in stability, may decrease difficulty' });
container.createEl('p', {text: 'FSRS parameters (weights, retention, etc.) can be tuned, but the defaults are generally effective.'});
}
}

View file

@ -30,6 +30,7 @@ export class ReviewSidebarView extends ItemView {
private lastSelectedNotePath: string | null = null;
private lastScrollPosition: number = 0;
private expandedUpcomingDayKey: string | null = null; // State for upcoming section
private resizeObserver: ResizeObserver | null = null;
// Sub-components for rendering different parts
private pomodoroUIManager: PomodoroUIManager;
@ -67,15 +68,15 @@ export class ReviewSidebarView extends ItemView {
}
async onClose(): Promise<void> {
if ((this as any).resizeObserver) {
(this as any).resizeObserver.disconnect();
if (this.resizeObserver) {
this.resizeObserver.disconnect();
}
this.plugin.events.off('sidebar-update', this.refresh.bind(this));
// No need to explicitly unregister pomodoro-update as the manager handles its own logic
}
private ensureBaseStructure(): void {
const contentEl = this.containerEl || (this as any).contentEl;
const contentEl = this.containerEl;
if (!contentEl) return;
if (!this.mainContainer) {
@ -280,7 +281,7 @@ export class ReviewSidebarView extends ItemView {
if (this.containerEl) {
resizeObserver.observe(this.containerEl);
(this as any).resizeObserver = resizeObserver;
this.resizeObserver = resizeObserver;
this.updateCalendarContainerClass();
}
}
@ -389,7 +390,7 @@ export class ReviewSidebarView extends ItemView {
};
}
async setViewState(state: ReviewSidebarViewState, e?: any): Promise<void> {
async setViewState(state: ReviewSidebarViewState): Promise<void> {
if (!state) return;
// Restore non-UI-dependent state first

View file

@ -58,11 +58,9 @@ export class NoteItemRenderer {
if (note.scheduleCategory === 'initial') {
const totalInitialSteps = this.plugin.settings.initialScheduleCustomIntervals.length;
const currentStepDisplay = note.reviewCount < totalInitialSteps ? note.reviewCount + 1 : totalInitialSteps;
phaseEl.innerHTML = `
<div title="Initial">Initial</div>
<div title="${currentStepDisplay}/${totalInitialSteps}">${currentStepDisplay}/${totalInitialSteps}</div>
<div class="phase-time" title="${formattedTime}">${formattedTime}</div>
`;
phaseEl.createDiv({ title: "Initial", text: "Initial" });
phaseEl.createDiv({ title: `${currentStepDisplay}/${totalInitialSteps}`, text: `${currentStepDisplay}/${totalInitialSteps}` });
const phaseTimeEl = phaseEl.createDiv({ cls: "phase-time", title: formattedTime, text: formattedTime });
phaseEl.addClass("review-phase-initial");
} else {
phaseEl.setText(note.scheduleCategory === 'graduated' ? "Graduated" : "Spaced");

View file

@ -250,10 +250,8 @@ export class LinkAnalyzer {
* @returns Array with the path of the best starting node
*/
private static findStartingNode(nodes: Record<string, FileNode>): string[] {
console.log("LinkAnalyzer: Starting node selection process...");
// If we have no nodes, return an empty array
if (Object.keys(nodes).length === 0) {
console.log("LinkAnalyzer: No nodes found, returning empty array.");
return [];
}
@ -274,42 +272,25 @@ export class LinkAnalyzer {
const filesInFolder = folderNodes[folderPath];
// HIGHEST PRIORITY: Exact match with folder name
console.log(`LinkAnalyzer: Checking for exact match with folder name "${folderName}" in folder "${folderPath}"...`);
for (const path of filesInFolder) {
const fileName = path.split('/').pop()?.toLowerCase() || '';
const fileNameWithoutExt = fileName.replace(/\.md$/, '');
if (fileNameWithoutExt === folderName) {
console.log(`LinkAnalyzer: Selected main file with exact folder name match: ${path}`);
const fileName = path.split('/').pop()?.toLowerCase().replace(/\.md$/, '') || '';
if (fileName === folderName) {
return [path];
}
}
// SECOND PRIORITY: Partial match with folder name
console.log(`LinkAnalyzer: Checking for partial match with folder name "${folderName}"...`);
for (const path of filesInFolder) {
const fileName = path.split('/').pop()?.toLowerCase() || '';
const fileNameWithoutExt = fileName.replace(/\.md$/, '');
if (fileNameWithoutExt.includes(folderName) || folderName.includes(fileNameWithoutExt)) {
console.log(`LinkAnalyzer: Selected main file with partial folder name match: ${path}`);
const fileName = path.split('/').pop()?.toLowerCase().replace(/\.md$/, '') || '';
if (fileName.includes(folderName) || folderName.includes(fileName)) {
return [path];
}
}
// THIRD PRIORITY: Common index or main file patterns
console.log("LinkAnalyzer: Checking for common index or main file patterns...");
for (const path of filesInFolder) {
const fileName = path.split('/').pop()?.toLowerCase() || '';
const fileNameWithoutExt = fileName.replace(/\.md$/, '');
if (fileNameWithoutExt === 'index' ||
fileNameWithoutExt === 'main' ||
fileNameWithoutExt.includes('index') ||
fileNameWithoutExt.includes('readme') ||
fileNameWithoutExt.includes('main')) {
console.log(`LinkAnalyzer: Selected main file by standard name pattern: ${path}`);
const fileName = path.split('/').pop()?.toLowerCase().replace(/\.md$/, '') || '';
if (fileName === 'index' || fileName === 'main' || fileName.includes('index') || fileName.includes('readme') || fileName.includes('main')) {
return [path];
}
}
@ -317,7 +298,6 @@ export class LinkAnalyzer {
// FOURTH PRIORITY: Look for files with the most outgoing links
// Prefer regular links over embeds
console.log("LinkAnalyzer: Checking for files with the most outgoing links...");
const sortedNodes = Object.values(nodes)
.sort((a, b) => {
// First, prioritize by regular link count (non-embed links)
@ -333,12 +313,10 @@ export class LinkAnalyzer {
if (sortedNodes.length > 0 &&
(sortedNodes[0].regularLinks.length > 0 || sortedNodes[0].outgoingLinks.length > 0)) {
// Return the node with the most links
console.log(`LinkAnalyzer: Selected main file by link count: ${sortedNodes[0].path} with ${sortedNodes[0].regularLinks.length} regular links`);
return [sortedNodes[0].path];
}
// FALLBACK: Use all files as root nodes if none of the above criteria match
console.log(`LinkAnalyzer: No clear main file identified, using first file as root node`);
return Object.keys(nodes).length > 0 ? [Object.keys(nodes)[0]] : [];
}
@ -376,12 +354,10 @@ export class LinkAnalyzer {
}
const mainFolder = fileFolders.get(startNodePath) || "";
console.log(`LinkAnalyzer: Starting ordered traversal from: ${startNodePath} in main folder: ${mainFolder || "vault root"}`);
// Helper function for depth-first traversal, respecting link order and folder constraints
const traverse = (currentNodePath: string, currentDepth: number = 0, currentMainFolder: string) => {
if (visited.has(currentNodePath)) {
console.log(`${" ".repeat(currentDepth)}LinkAnalyzer: Already visited: ${currentNodePath}`);
return;
}
@ -391,7 +367,6 @@ export class LinkAnalyzer {
return;
}
console.log(`${" ".repeat(currentDepth)}LinkAnalyzer: Traversing: ${currentNodePath}`);
visited.add(currentNodePath);
traversalOrder.push(currentNodePath);
@ -402,7 +377,6 @@ export class LinkAnalyzer {
for (const linkedPath of linksToFollow) {
if (!nodes[linkedPath]) { // Ensure the linked path is a known node in the current analysis context
console.log(`${" ".repeat(currentDepth + 1)}LinkAnalyzer: Skipping unknown linked path: ${linkedPath}`);
continue;
}
@ -411,14 +385,11 @@ export class LinkAnalyzer {
// MODIFIED RULE: Traverse if the linked note is within the scope of the main analysis folder.
if (nodes[linkedPath] && linkedNodeFolder.startsWith(currentMainFolder)) { // Ensure it's a known node and within the main hierarchy
if (!visited.has(linkedPath)) { // Avoid re-traversing already processed branches
console.log(`${" ".repeat(currentDepth + 1)}LinkAnalyzer: Following link within main hierarchy to: ${linkedPath}`);
traverse(linkedPath, currentDepth + 1, currentMainFolder);
} else {
console.log(`${" ".repeat(currentDepth + 1)}LinkAnalyzer: Link to already visited node in main hierarchy: ${linkedPath}`);
}
} else {
// This link goes outside the main folder being analyzed or is not a known node.
console.log(`${" ".repeat(currentDepth + 1)}LinkAnalyzer: Ignoring link outside main hierarchy or unknown node: ${linkedPath} (from ${currentNodeFolder} to ${linkedNodeFolder})`);
}
}
};
@ -435,7 +406,6 @@ export class LinkAnalyzer {
// unlinked files from the mainFolder.
// Log the traversal order summary
console.log(`LinkAnalyzer: Final traversal order from ${startNodePath} (in folder '${mainFolder || "vault root"}') has ${traversalOrder.length} files: ${traversalOrder.join(' -> ')}`);
return traversalOrder;
}
@ -538,7 +508,6 @@ export class LinkAnalyzer {
}
}
console.log(`LinkAnalyzer: Analyzed links in ${filePath}, found ${resolvedLinks.length} unique links in order`);
return resolvedLinks;
} catch (error) {