improving ui more

This commit is contained in:
Alastair Grant 2025-02-14 15:26:42 +01:00
parent 92dd59c827
commit 69f5078b04
2 changed files with 49 additions and 42 deletions

View file

@ -1,10 +1,9 @@
/*
This file implements a custom sidebar view (dashboard) for the Obsidian RAG Test Plugin.
It displays a list of all indexed notes with checkboxes for selection and a "Create Tests" button.
When "Create Tests" is clicked, an OpenAI API call is made for each selected note (using that note's full content)
to generate test questions. While tests are being generated, a spinner is shown next to the respective file name.
Once the tests are generated for a file, the spinner is replaced by a clickable icon which, when clicked,
opens a full-screen view displaying the generated tests.
Each row initially shows a greyed-out document icon. When tests are being generated for that note,
the icon is replaced by a spinner. Once tests are generated, the spinner is replaced by an active,
colored document icon that opens the full-screen test questions view when clicked.
*/
import { App, ItemView, Notice, WorkspaceLeaf } from "obsidian";
@ -62,13 +61,13 @@ export default class TestDashboardView extends ItemView {
}
/**
* Renders the dashboard: a list of notes with selection checkboxes and a "Create Tests" button.
* Renders the dashboard: a list of notes with checkboxes, file names, and a status icon.
*/
async render(): Promise<void> {
const container = this.containerEl;
container.empty();
// Header with "Create Tests" button
// Header with "Create Tests" button.
const headerEl = container.createEl("div", { cls: "test-view-header" });
const createTestsButton = headerEl.createEl("button", { text: "Create Tests" });
createTestsButton.disabled = true;
@ -76,22 +75,27 @@ export default class TestDashboardView extends ItemView {
await this.createTests();
});
// Container for the list of notes
// Container for the list of notes.
const listEl = container.createEl("ul");
this.pluginData.forEach((note) => {
const itemEl = listEl.createEl("li");
const checkbox = itemEl.createEl("input", { type: "checkbox" });
checkbox.dataset.filePath = note.filePath;
// Display the file name.
itemEl.createEl("span", { text: ` ${note.filePath}` });
// Create spinner element (hidden by default)
const spinnerEl = itemEl.createEl("div", { cls: "spinner" });
spinnerEl.style.display = "none";
spinnerEl.style.marginLeft = "0.5em";
// Create a status icon container.
const statusIconEl = itemEl.createEl("span", { cls: "status-icon" });
statusIconEl.style.marginLeft = "0.5em";
// Initially, display a greyed-out document icon.
statusIconEl.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="gray" class="bi bi-file-text" viewBox="0 0 16 16">
<path d="M4 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V5.5L9.5 0H4z"/>
<path d="M9.5 0v4a1 1 0 0 0 1 1h4"/>
<path d="M4.5 7a.5.5 0 0 1 .5.5v.5h5v-1h-5V7.5a.5.5 0 0 1 .5-.5z"/>
</svg>`;
checkbox.addEventListener("change", () => {
this.updateCreateTestsButtonState(createTestsButton, listEl);
});
});
this.updateCreateTestsButtonState(createTestsButton, listEl);
}
@ -113,12 +117,13 @@ export default class TestDashboardView extends ItemView {
/**
* For each selected note, calls the LLM API to generate test questions,
* and replaces the spinner with an icon that, when clicked, opens the full-screen view for that file's tests.
* updates the row's status icon to a spinner during generation, and then replaces it
* with a clickable document icon (active) that opens the full-screen view.
*/
async createTests(): Promise<void> {
const container = this.containerEl;
const checkboxes = container.querySelectorAll('input[type="checkbox"]');
// Retrieve API key from plugin settings stored in the main plugin instance.
// Retrieve API key from plugin settings.
const plugin = (this.app as any).plugins.plugins["obsidian-rag-test-plugin"] as any;
const apiKey = plugin?.settings?.apiKey;
if (!apiKey) {
@ -133,41 +138,43 @@ export default class TestDashboardView extends ItemView {
if (!note) continue;
console.log(`Generating tests for: ${filePath}`);
const listItem = input.parentElement;
const spinnerEl = listItem?.querySelector(".spinner");
if (spinnerEl) {
spinnerEl.style.display = "inline-block";
const statusIconEl = listItem?.querySelector(".status-icon");
// Change icon to spinner.
if (statusIconEl) {
statusIconEl.innerHTML = `<div class="spinner"></div>`;
}
try {
// Generate tests for this note.
const response = await generateTestQuestions([note], apiKey);
console.log(`Generated tests for ${filePath}:`, response);
// Remove spinner and add a view icon.
if (spinnerEl) {
spinnerEl.style.display = "none";
// Create a button with a document icon.
const iconBtn = document.createElement("button");
iconBtn.classList.add("view-tests-icon");
iconBtn.title = "View Generated Tests";
// Example inline SVG for a document icon (adjust as needed).
iconBtn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-file-text" viewBox="0 0 16 16">
<path d="M4 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V5.5L9.5 0H4z"/>
<path d="M9.5 0v4a1 1 0 0 0 1 1h4"/>
<path fill-rule="evenodd" d="M4.5 7a.5.5 0 0 1 .5.5v.5h5v-1h-5V7.5a.5.5 0 0 1 .5-.5z"/>
</svg>`;
// Append the icon button to the list item.
listItem?.appendChild(iconBtn);
// When clicked, open a full-screen view for this file's tests.
iconBtn.addEventListener("click", () => {
(plugin as any).openQuestionDocument(response);
});
// After generation, update icon to active document icon.
if (statusIconEl) {
statusIconEl.innerHTML = `<button class="view-tests-icon" title="View Generated Tests">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-file-text" viewBox="0 0 16 16">
<path d="M4 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V5.5L9.5 0H4z"/>
<path d="M9.5 0v4a1 1 0 0 0 1 1h4"/>
<path d="M4.5 7a.5.5 0 0 1 .5.5v.5h5v-1h-5V7.5a.5.5 0 0 1 .5-.5z"/>
</svg>
</button>`;
// Remove grey color by changing fill attribute via CSS class.
const iconBtn = statusIconEl.querySelector("button.view-tests-icon") as HTMLButtonElement;
if (iconBtn) {
iconBtn.addEventListener("click", () => {
(plugin as any).openQuestionDocument(response);
});
}
}
} catch (error) {
console.error(`Error generating tests for ${filePath}:`, error);
if (spinnerEl) {
spinnerEl.style.display = "none";
}
if (statusIconEl) {
// Revert icon to greyed-out document icon.
statusIconEl.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="gray" class="bi bi-file-text" viewBox="0 0 16 16">
<path d="M4 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V5.5L9.5 0H4z"/>
<path d="M9.5 0v4a1 1 0 0 0 1 1h4"/>
<path d="M4.5 7a.5.5 0 0 1 .5.5v.5h5v-1h-5V7.5a.5.5 0 0 1 .5-.5z"/>
</svg>`;
}
}
}
}
}
}

View file

@ -8,6 +8,7 @@ If your plugin does not need CSS, delete this file.
*/
.spinner {
display: inline-block;
border: 2px solid rgba(0, 0, 0, 0.1);
border-left-color: #000;
border-radius: 50%;
@ -19,13 +20,12 @@ If your plugin does not need CSS, delete this file.
@keyframes spin {
to { transform: rotate(360deg); }
}
.view-tests-icon {
background: none;
border: none;
cursor: pointer;
padding: 0;
margin-left: 0.5em;
}
.view-tests-icon svg {