From 714bf262df5bc82c10d32a115d20554ac9142a84 Mon Sep 17 00:00:00 2001 From: Alastair Grant <85125580+aldo-g@users.noreply.github.com> Date: Mon, 3 Mar 2025 18:23:47 +0100 Subject: [PATCH] further developed --- main.ts | 103 +++++++++++++++++++++++++---- src/ui/DashboardView.ts | 143 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 232 insertions(+), 14 deletions(-) diff --git a/main.ts b/main.ts index 0272384..5184d14 100644 --- a/main.ts +++ b/main.ts @@ -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); } } \ No newline at end of file diff --git a/src/ui/DashboardView.ts b/src/ui/DashboardView.ts index 7dbb627..cc1f6e6 100644 --- a/src/ui/DashboardView.ts +++ b/src/ui/DashboardView.ts @@ -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(); + } + } } \ No newline at end of file