This commit is contained in:
Alastair Grant 2025-02-13 18:51:03 +01:00
parent 7301625050
commit bd5865eb52
4 changed files with 69 additions and 40 deletions

20
main.ts
View file

@ -180,14 +180,16 @@ export default class MyPlugin extends Plugin {
state: { questions }
});
this.app.workspace.revealLeaf(leaf);
// For extra safety, explicitly update the view instance if available.
const view = leaf.view as any;
if (view) {
console.log("openQuestionDocument: view instance found, updating questions.");
view.questions = questions;
view.render();
} else {
console.log("openQuestionDocument: view instance not available.");
}
// Wait a short time for the view to initialize, then update the view instance.
setTimeout(() => {
const view = leaf.view as any;
if (view) {
console.log("openQuestionDocument: view instance found, updating questions.");
view.questions = questions;
view.render();
} else {
console.log("openQuestionDocument: view instance not available.");
}
}, 100);
}
}

View file

@ -2,10 +2,11 @@
This file implements a full-page view for displaying generated test questions.
It renders an interactive document where each question is displayed with an input field for the user's answer,
and includes a "Mark Test" button to submit the answers.
Debug logs are added to verify that the questions are correctly passed into the view.
The view expects an array of GeneratedTest objects but displays only the question text.
*/
import { App, ItemView, Notice, WorkspaceLeaf } from "obsidian";
import type { GeneratedTest } from "../models/types";
export const QUESTION_VIEW_TYPE = "question-document-view";
@ -13,18 +14,18 @@ export const QUESTION_VIEW_TYPE = "question-document-view";
* A full-page view for displaying test questions.
*/
export default class QuestionDocumentView extends ItemView {
questions: string[];
generatedTests: GeneratedTest[];
/**
* Constructs the QuestionDocumentView.
* @param leaf - The workspace leaf to attach the view.
* @param app - The Obsidian application instance.
* @param questions - An array of test question strings.
* @param generatedTests - An array of generated test objects.
*/
constructor(leaf: WorkspaceLeaf, app: App, questions: string[]) {
constructor(leaf: WorkspaceLeaf, app: App, generatedTests: GeneratedTest[]) {
super(leaf);
this.questions = questions;
console.log("QuestionDocumentView constructor: initial questions =", this.questions);
this.generatedTests = generatedTests;
console.log("QuestionDocumentView constructor: initial generatedTests =", this.generatedTests);
}
/**
@ -47,13 +48,13 @@ export default class QuestionDocumentView extends ItemView {
* Called when the view is opened; retrieves state and triggers rendering.
*/
async onOpen(): Promise<void> {
const state = this.getState() as { questions?: string[] } | undefined;
const state = this.getState() as { questions?: GeneratedTest[] } | undefined;
console.log("QuestionDocumentView onOpen: retrieved state =", state);
if (state && state.questions) {
this.questions = state.questions;
this.generatedTests = state.questions;
}
console.log("QuestionDocumentView onOpen: questions length =", this.questions.length);
if (this.questions.length === 0) {
console.log("QuestionDocumentView onOpen: generatedTests length =", this.generatedTests.length);
if (this.generatedTests.length === 0) {
new Notice("No test questions available. Please generate tests first.");
}
this.render();
@ -73,22 +74,23 @@ export default class QuestionDocumentView extends ItemView {
render(): void {
const container = this.containerEl;
container.empty();
console.log("QuestionDocumentView render: rendering questions, count =", this.questions.length);
console.log("QuestionDocumentView render: rendering generatedTests, count =", this.generatedTests.length);
const titleEl = container.createEl("h1", { text: "Generated Test Questions" });
const formEl = container.createEl("form");
formEl.style.display = "block";
formEl.style.width = "100%";
if (this.questions.length === 0) {
if (this.generatedTests.length === 0) {
const emptyEl = container.createEl("p", { text: "No questions to display." });
console.log("QuestionDocumentView render: no questions found.");
} else {
this.questions.forEach((question, index) => {
this.generatedTests.forEach((item, index) => {
const questionDiv = formEl.createEl("div", { cls: "question-item" });
questionDiv.style.marginBottom = "1em";
const label = questionDiv.createEl("label", { text: `Q${index + 1}: ${question}` });
// Display only the question text
const label = questionDiv.createEl("label", { text: `Q${index + 1}: ${item.question}` });
label.style.display = "block";
label.style.fontWeight = "bold";
@ -97,6 +99,8 @@ export default class QuestionDocumentView extends ItemView {
input.style.width = "100%";
input.style.marginTop = "0.5em";
(input as HTMLElement).dataset.questionIndex = index.toString();
// Optionally, store the suggested answer (not displayed)
(input as HTMLElement).dataset.suggestedAnswer = item.suggestedAnswer;
});
}
@ -104,13 +108,14 @@ export default class QuestionDocumentView extends ItemView {
markButton.type = "button";
markButton.style.marginTop = "1em";
markButton.addEventListener("click", () => {
const answers: { [key: number]: string } = {};
const answers: { [key: number]: { userAnswer: string; suggestedAnswer: string } } = {};
const inputs = formEl.querySelectorAll("input[type='text']") as NodeListOf<HTMLInputElement>;
inputs.forEach((input) => {
const idxStr = input.dataset.questionIndex;
const suggested = input.dataset.suggestedAnswer || "";
if (idxStr !== undefined) {
const idx = parseInt(idxStr, 10);
answers[idx] = input.value;
answers[idx] = { userAnswer: input.value, suggestedAnswer: suggested };
}
});
new Notice("Test answers submitted!");

View file

@ -15,6 +15,7 @@ export interface LLMResponse {
index: number;
message: {
role: string;
// Expected to be a JSON string that parses to an array of GeneratedTest objects.
content: string;
};
}[];
@ -26,11 +27,11 @@ export interface LLMResponse {
}
/**
* Represents a generated test question (and an optional answer) from the LLM.
* Represents a generated test question and its suggested answer (which is stored but not shown to the user).
*/
export interface GeneratedTest {
question: string;
answer?: string;
suggestedAnswer: string;
}
/**
@ -49,4 +50,4 @@ export interface IndexedNote {
filePath: string;
content: string;
testStatus: TestStatus;
}
}

View file

@ -1,29 +1,29 @@
/*
This file handles communication with OpenAI to generate test questions.
It exports a function generateTestQuestions that sends a prompt (built using formatter.ts)
to OpenAI using the provided API key and returns an array of test questions.
It now expects a JSON-formatted response and returns an array of GeneratedTest objects.
If the JSON is incomplete or wrapped in markdown fences, the code attempts to clean the output.
*/
import { formatNotesForLLM } from "./formatter";
import type { IndexedNote, LLMResponse } from "../models/types";
import type { IndexedNote, LLMResponse, GeneratedTest } from "../models/types";
const API_URL = "https://api.openai.com/v1/chat/completions";
/**
* Sends formatted note data to OpenAI and retrieves test questions.
* Sends formatted note data to OpenAI and retrieves test questions as JSON.
* @param indexedNotes - The indexed notes to generate questions from.
* @param apiKey - The OpenAI API key.
* @returns A promise that resolves to an array of generated test questions.
* @returns A promise that resolves to an array of GeneratedTest objects.
*/
export async function generateTestQuestions(indexedNotes: IndexedNote[], apiKey: string): Promise<string[]> {
export async function generateTestQuestions(indexedNotes: IndexedNote[], apiKey: string): Promise<GeneratedTest[]> {
if (!apiKey) {
throw new Error("Missing OpenAI API key! Please set it in the plugin settings.");
}
const prompt = formatNotesForLLM(indexedNotes);
const requestBody = {
model: "gpt-3.5-turbo-0125",
model: "gpt-4-turbo",
messages: [
{ role: "system", content: "You are a helpful AI that generates test questions from study notes. Your questions should be answered with written answers." },
{ role: "system", content: "You are a helpful AI that generates test questions from study notes. Respond with a JSON array of objects, each having a 'question' and a 'suggestedAnswer'. Do not include any additional text." },
{ role: "user", content: prompt }
],
temperature: 0.7,
@ -45,11 +45,32 @@ export async function generateTestQuestions(indexedNotes: IndexedNote[], apiKey:
const responseData: LLMResponse = await response.json();
const output = responseData.choices?.[0]?.message?.content;
if (!output) {
throw new Error("Invalid response from OpenAI");
}
// Split response into separate questions (assuming they are newline-separated)
return output.split("\n").filter(q => q.trim().length > 0);
}
console.log("Raw LLM output:", output);
// Attempt to clean the output:
let jsonString = output.trim();
// Remove markdown fences if present
if (jsonString.startsWith("```json")) {
jsonString = jsonString.slice(7).trim();
}
if (jsonString.endsWith("```")) {
jsonString = jsonString.slice(0, -3).trim();
}
// If the JSON might be truncated, try to extract up to the last closing brace
const lastBrace = jsonString.lastIndexOf("}");
if (lastBrace !== -1) {
jsonString = jsonString.slice(0, lastBrace + 1);
}
try {
const parsed: GeneratedTest[] = JSON.parse(jsonString);
return parsed;
} catch (err) {
console.error("Error parsing JSON from LLM response:", err, "Cleaned JSON string:", jsonString);
throw new Error("Failed to parse JSON response from OpenAI");
}
}