mirror of
https://github.com/dralkh/spaceforge.git
synced 2026-07-22 06:45:03 +00:00
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:
parent
79d83e4eb4
commit
dbd37e8537
20 changed files with 371 additions and 546 deletions
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
412
main.js
|
|
@ -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
25
main.ts
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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.");
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.'});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue