further developed

This commit is contained in:
Alastair Grant 2025-03-03 18:23:47 +01:00
parent 0385c70f06
commit 714bf262df
2 changed files with 232 additions and 14 deletions

103
main.ts
View file

@ -2,6 +2,7 @@ import { Notice, Plugin } from "obsidian";
import TestDashboardView, { VIEW_TYPE as DASHBOARD_VIEW_TYPE } from "./src/ui/DashboardView";
import QuestionDocumentView, { QUESTION_VIEW_TYPE } from "./src/ui/QuestionView";
import SettingsTab from "./src/ui/SettingsTab";
import { GeneratedTest } from "src/models/types";
export interface TestStatus {
testsReady: boolean;
@ -27,10 +28,12 @@ const DEFAULT_SETTINGS: MyPluginSettings = {
export interface TestDocumentState {
description: string;
questions: { question: string }[];
questions: GeneratedTest[];
answers: { [key: number]: string };
score?: number;
}
// Add markResults array to store feedback for each question
markResults?: Array<{ marks: number; maxMarks: number; feedback: string } | null>;
}
interface MyPluginData {
settings: MyPluginSettings;
@ -597,6 +600,58 @@ export default class MyPlugin extends Plugin {
to { opacity: 1; transform: translateY(0); }
}
.mark-all-container {
position: fixed;
bottom: 20px;
right: 20px;
z-index: 10;
}
.mark-all-button {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
transition: all 0.2s ease;
border-radius: 6px;
}
.mark-all-button:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
.mark-all-button:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 24px;
background-color: var(--background-primary);
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
.loading-text {
margin-top: 16px;
font-size: 16px;
font-weight: 500;
color: var(--text-normal);
}
.mark-all-button .spinner {
width: 14px;
height: 14px;
}
`;
document.head.appendChild(styleElement);
@ -690,25 +745,47 @@ export default class MyPlugin extends Plugin {
*/
public openQuestionDoc(filePath: string): void {
if (!this.testDocuments[filePath]) {
new Notice("No tests found for this note. Generate tests first.");
return;
new Notice("No tests found for this note. Generate tests first.");
return;
}
const response = this.testDocuments[filePath];
this.app.workspace.detachLeavesOfType(QUESTION_VIEW_TYPE);
const leaf = this.app.workspace.getLeaf("tab");
leaf.setViewState({ type: QUESTION_VIEW_TYPE, active: true });
this.app.workspace.revealLeaf(leaf);
setTimeout(() => {
const view = leaf.view as QuestionDocumentView;
if (view) {
view.filePath = filePath;
view.description = response.description;
view.generatedTests = response.questions;
view.answers = response.answers || {};
view.render();
const view = leaf.view as QuestionDocumentView;
if (view) {
view.filePath = filePath;
view.description = response.description;
view.generatedTests = response.questions;
view.answers = response.answers || {};
// Also pass the existing mark results if available
if (response.markResults) {
view.markResults = response.markResults;
}
// If there's a score, set the score summary
if (typeof response.score === "number") {
const markResults = response.markResults || [];
let totalEarnedMarks = 0;
let totalPossibleMarks = 0;
markResults.forEach(result => {
if (result) {
totalEarnedMarks += result.marks;
totalPossibleMarks += result.maxMarks;
}
});
view.scoreSummary = `You scored ${totalEarnedMarks} / ${totalPossibleMarks} marks (${response.score.toFixed(1)}%)`;
}
view.render();
}
}, 200);
}
}

View file

@ -1,6 +1,6 @@
import { App, ItemView, Notice, WorkspaceLeaf } from "obsidian";
import type { IndexedNote } from "../models/types";
import { generateTestQuestions, ContextLengthExceededError } from "../services/llm";
import { generateTestQuestions, ContextLengthExceededError, markTestAnswers } from "../services/llm";
import type MyPlugin from "../../main";
export const VIEW_TYPE = "rag-test-view";
@ -199,6 +199,27 @@ export default class TestDashboardView extends ItemView {
// Initialize the Create Tests button state
this.updateCreateBtn(createBtn);
// Add the "Mark All Tests" button to the bottom right
const markAllContainer = container.createEl("div", {
cls: "mark-all-container"
});
const markAllBtn = markAllContainer.createEl("button", {
cls: "dashboard-button primary mark-all-button",
text: "Mark All Tests"
});
// Disable the button if there are no partial tests to mark
const hasPartialTests = Object.entries(this.plugin.testDocuments).some(([path, doc]) => {
// Check if test has answers but no score yet
return doc.answers &&
Object.values(doc.answers).some(answer => answer && (answer as string).trim().length > 0) &&
typeof doc.score !== "number";
});
markAllBtn.disabled = !hasPartialTests;
markAllBtn.onclick = () => this.markAllTests();
}
/**
@ -567,4 +588,124 @@ export default class TestDashboardView extends ItemView {
new Notice("❌ Some tests could not be generated. Check console for details.");
}
}
/**
* Marks all tests that have answers but haven't been fully marked yet
*/
async markAllTests() {
const ragPlugin = this.plugin;
if (!ragPlugin) {
new Notice("❌ RAG Test Plugin not found.");
return;
}
const apiKey = ragPlugin.settings.apiKey;
if (!apiKey) {
new Notice("❌ OpenAI API Key is missing! Please set it in the plugin settings.");
return;
}
// Find all tests with answers that haven't been marked yet
const testsToMark: string[] = [];
Object.entries(ragPlugin.testDocuments).forEach(([path, doc]) => {
// Check if there are answers but no score yet
const hasAnswers = doc.answers &&
Object.values(doc.answers).some(answer => answer && (answer as string).trim().length > 0);
if (hasAnswers && typeof doc.score !== "number") {
testsToMark.push(path);
}
});
if (testsToMark.length === 0) {
new Notice("No tests to mark.");
return;
}
// Create a loading overlay
const loadingOverlay = this.containerEl.createDiv({ cls: "spinner-overlay" });
const loadingContainer = loadingOverlay.createDiv({ cls: "loading-container" });
loadingContainer.createDiv({ cls: "spinner" });
loadingContainer.createEl("p", {
text: `Marking ${testsToMark.length} tests... This may take a few moments.`,
cls: "loading-text"
});
try {
// Process tests concurrently
const markingPromises = testsToMark.map(async (filePath) => {
try {
const indexedNote = ragPlugin.indexedNotes.find(n => n.filePath === filePath);
if (!indexedNote) {
return { filePath, success: false, error: "Note content not found" };
}
const docState = ragPlugin.testDocuments[filePath];
const noteContent = indexedNote.content;
// Prepare question-answer pairs
const qnaPairs = docState.questions.map((test, idx) => ({
question: test.question,
answer: docState.answers[idx] || ""
}));
// Skip if no actual answers
if (!qnaPairs.some(pair => pair.answer.trim())) {
return { filePath, success: false, error: "No answers to mark" };
}
// Mark the answers
const feedbackArray = await markTestAnswers(noteContent, qnaPairs, apiKey);
// Calculate the score
let totalPossibleMarks = 0;
let totalEarnedMarks = 0;
feedbackArray.forEach(item => {
totalPossibleMarks += item.maxMarks;
totalEarnedMarks += item.marks;
});
const percentage = totalPossibleMarks
? ((totalEarnedMarks / totalPossibleMarks) * 100)
: 0;
// Update the document state
ragPlugin.testDocuments[filePath].score = percentage;
return { filePath, success: true, score: percentage };
} catch (error) {
console.error(`Error marking test ${filePath}:`, error);
return { filePath, success: false, error: error.message || "Unknown error" };
}
});
// Wait for all marking operations to complete
const results = await Promise.all(markingPromises);
// Save all changes at once
await ragPlugin.saveSettings();
// Update the dashboard
this.render();
// Show results to user
const successful = results.filter(r => r.success).length;
const failed = results.length - successful;
if (failed > 0) {
new Notice(`✅ Marked ${successful} tests successfully. ❌ ${failed} tests failed.`);
console.error("Failed tests:", results.filter(r => !r.success));
} else {
new Notice(`✅ Successfully marked all ${successful} tests!`);
}
} catch (error) {
console.error("Error in markAllTests:", error);
new Notice("❌ Error marking tests. Check console for details.");
} finally {
// Remove loading overlay
loadingOverlay.remove();
}
}
}