From bd5865eb52430b697b0c48b07dc702fe980d9454 Mon Sep 17 00:00:00 2001 From: Alastair Grant <85125580+aldo-g@users.noreply.github.com> Date: Thu, 13 Feb 2025 18:51:03 +0100 Subject: [PATCH] better --- main.ts | 20 ++++++------ src/components/QuestionDocumentView.ts | 37 ++++++++++++--------- src/models/types.ts | 7 ++-- src/services/llm.ts | 45 +++++++++++++++++++------- 4 files changed, 69 insertions(+), 40 deletions(-) diff --git a/main.ts b/main.ts index f0ad34f..4291ec2 100644 --- a/main.ts +++ b/main.ts @@ -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); } } \ No newline at end of file diff --git a/src/components/QuestionDocumentView.ts b/src/components/QuestionDocumentView.ts index b7e29c3..0e56fd5 100644 --- a/src/components/QuestionDocumentView.ts +++ b/src/components/QuestionDocumentView.ts @@ -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 { - 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; 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!"); diff --git a/src/models/types.ts b/src/models/types.ts index 452f86e..cae055b 100644 --- a/src/models/types.ts +++ b/src/models/types.ts @@ -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; -} +} \ No newline at end of file diff --git a/src/services/llm.ts b/src/services/llm.ts index 64a3fd1..5c8afba 100644 --- a/src/services/llm.ts +++ b/src/services/llm.ts @@ -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 { +export async function generateTestQuestions(indexedNotes: IndexedNote[], apiKey: string): Promise { 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"); + } +} \ No newline at end of file