mirror of
https://github.com/aldo-g/obsidian-llm-test.git
synced 2026-07-22 05:42:19 +00:00
better
This commit is contained in:
parent
7301625050
commit
bd5865eb52
4 changed files with 69 additions and 40 deletions
20
main.ts
20
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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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!");
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue