mirror of
https://github.com/aldo-g/obsidian-llm-test.git
synced 2026-07-22 05:42:19 +00:00
further developed
This commit is contained in:
parent
0385c70f06
commit
714bf262df
2 changed files with 232 additions and 14 deletions
103
main.ts
103
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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue