mindnet_obsidian/src/ui/VaultTriageScanModal.ts
Lars a9b3e2f0e2
Some checks are pending
Node.js build / build (20.x) (push) Waiting to run
Node.js build / build (22.x) (push) Waiting to run
Implement Chain Workbench and Vault Triage features in Mindnet plugin
- Introduced new workflows for Chain Workbench and Vault Triage, enhancing user capabilities for managing template matches and identifying chain gaps.
- Added commands for opening the Chain Workbench and scanning the vault for chain gaps, improving the overall functionality of the plugin.
- Updated documentation to include detailed instructions for the new workflows, ensuring users can effectively utilize the features.
- Enhanced the UI for both the Chain Workbench and Vault Triage Scan, providing a more intuitive user experience.
- Implemented tests for the new functionalities to ensure reliability and accuracy in various scenarios.
2026-01-26 10:51:12 +01:00

363 lines
12 KiB
TypeScript

/**
* Vault Triage Scan Modal - UI for vault triage scan backlog.
*/
import { Modal, Setting, Notice } from "obsidian";
import type { App } from "obsidian";
import type { ChainRolesConfig, ChainTemplatesConfig } from "../dictionary/types";
import type { MindnetSettings } from "../settings";
import type { EdgeVocabulary } from "../vocab/types";
import { scanVaultForChainGaps, saveScanState, type ScanState, type ScanItem } from "../workbench/vaultTriageScan";
export class VaultTriageScanModal extends Modal {
private chainRoles: ChainRolesConfig | null;
private chainTemplates: ChainTemplatesConfig | null;
private edgeVocabulary: EdgeVocabulary | null;
private settings: MindnetSettings;
private pluginInstance: any;
private existingState: ScanState | null;
private items: ScanItem[] = [];
private filterStatus: string | null = null;
private filterHasMissingSlot: boolean = false;
private filterHasMissingLink: boolean = false;
private filterWeak: boolean = false;
private filterCandidateCleanup: boolean = false;
private searchQuery: string = "";
private isScanning: boolean = false;
private shouldCancel: boolean = false;
constructor(
app: App,
chainRoles: ChainRolesConfig | null,
chainTemplates: ChainTemplatesConfig | null,
edgeVocabulary: EdgeVocabulary | null,
settings: MindnetSettings,
pluginInstance: any,
existingState: ScanState | null
) {
super(app);
this.chainRoles = chainRoles;
this.chainTemplates = chainTemplates;
this.edgeVocabulary = edgeVocabulary;
this.settings = settings;
this.pluginInstance = pluginInstance;
this.existingState = existingState;
if (existingState) {
this.items = existingState.items;
}
}
onOpen(): void {
const { contentEl } = this;
contentEl.empty();
// Header
contentEl.createEl("h2", { text: "Vault Triage Scan" });
if (this.existingState && !this.existingState.completed) {
contentEl.createEl("p", {
text: `Resuming scan: ${this.existingState.progress.current}/${this.existingState.progress.total}`,
cls: "scan-progress-info",
});
}
// Controls
const controlsContainer = contentEl.createDiv({ cls: "scan-controls" });
// Start/Resume scan button
if (!this.isScanning && (this.items.length === 0 || !this.existingState?.completed)) {
const scanBtn = controlsContainer.createEl("button", {
text: this.items.length === 0 ? "Start Scan" : "Resume Scan",
cls: "mod-cta",
});
scanBtn.addEventListener("click", () => {
this.startScan();
});
}
// Filters
const filtersContainer = contentEl.createDiv({ cls: "scan-filters" });
filtersContainer.createEl("h3", { text: "Filters" });
// Status filter
const statusSelect = filtersContainer.createEl("select");
statusSelect.createEl("option", { text: "All", value: "" });
statusSelect.createEl("option", { text: "Near Complete", value: "near_complete" });
statusSelect.createEl("option", { text: "Partial", value: "partial" });
statusSelect.createEl("option", { text: "Weak", value: "weak" });
statusSelect.createEl("option", { text: "Deprioritized", value: "deprioritized" });
statusSelect.value = this.filterStatus || "";
statusSelect.addEventListener("change", (e) => {
const target = e.target as HTMLSelectElement;
this.filterStatus = target.value || null;
this.render();
});
// Gap filters
new Setting(filtersContainer)
.setName("Has Missing Slot")
.addToggle((toggle) => {
toggle.setValue(this.filterHasMissingSlot);
toggle.onChange((value) => {
this.filterHasMissingSlot = value;
this.render();
});
});
new Setting(filtersContainer)
.setName("Has Missing Link")
.addToggle((toggle) => {
toggle.setValue(this.filterHasMissingLink);
toggle.onChange((value) => {
this.filterHasMissingLink = value;
this.render();
});
});
new Setting(filtersContainer)
.setName("Weak")
.addToggle((toggle) => {
toggle.setValue(this.filterWeak);
toggle.onChange((value) => {
this.filterWeak = value;
this.render();
});
});
new Setting(filtersContainer)
.setName("Candidate Cleanup")
.addToggle((toggle) => {
toggle.setValue(this.filterCandidateCleanup);
toggle.onChange((value) => {
this.filterCandidateCleanup = value;
this.render();
});
});
// Search
filtersContainer.createEl("label", { text: "Search:" });
const searchInput = filtersContainer.createEl("input", {
type: "text",
placeholder: "File name, heading, template...",
});
searchInput.value = this.searchQuery;
searchInput.addEventListener("input", (e) => {
const target = e.target as HTMLInputElement;
this.searchQuery = target.value.toLowerCase();
this.render();
});
// Results list
const resultsContainer = contentEl.createDiv({ cls: "scan-results" });
resultsContainer.createEl("h3", { text: "Results" });
this.render();
}
private async startScan(): Promise<void> {
this.isScanning = true;
this.shouldCancel = false;
this.render();
try {
const items = await scanVaultForChainGaps(
this.app,
this.chainRoles,
this.chainTemplates,
this.edgeVocabulary,
this.settings,
(progress) => {
// Update progress
const state: ScanState = {
items: this.items,
timestamp: Date.now(),
progress,
completed: false,
};
saveScanState(this.app, state, this.pluginInstance);
this.render();
},
() => this.shouldCancel
);
this.items = items;
// Save completed state
const state: ScanState = {
items,
timestamp: Date.now(),
progress: {
current: items.length,
total: items.length,
currentFile: null,
},
completed: true,
};
await saveScanState(this.app, state, this.pluginInstance);
this.isScanning = false;
this.render();
} catch (e) {
this.isScanning = false;
const msg = e instanceof Error ? e.message : String(e);
console.error("[Vault Triage Scan] Error:", e);
new Notice(`Scan failed: ${msg}`);
this.render();
}
}
private render(): void {
const resultsContainer = this.contentEl.querySelector(".scan-results");
if (!resultsContainer) return;
// Clear previous results
const existingList = resultsContainer.querySelector(".scan-items-list");
if (existingList) {
existingList.remove();
}
// Filter items
let filteredItems = this.items;
if (this.filterStatus) {
filteredItems = filteredItems.filter((item) => item.status === this.filterStatus);
}
if (this.filterHasMissingSlot) {
filteredItems = filteredItems.filter((item) => item.gapCounts.missingSlots > 0);
}
if (this.filterHasMissingLink) {
filteredItems = filteredItems.filter((item) => item.gapCounts.missingLinks > 0);
}
if (this.filterWeak) {
filteredItems = filteredItems.filter((item) => item.status === "weak");
}
if (this.filterCandidateCleanup) {
filteredItems = filteredItems.filter((item) => item.gapCounts.candidateCleanup > 0);
}
if (this.searchQuery) {
filteredItems = filteredItems.filter((item) => {
const fileMatch = item.file.toLowerCase().includes(this.searchQuery);
const headingMatch = item.heading?.toLowerCase().includes(this.searchQuery);
const templateMatch = item.matches.some((m) =>
m.templateName.toLowerCase().includes(this.searchQuery)
);
return fileMatch || headingMatch || templateMatch;
});
}
// Sort: near_complete first, then by score descending
filteredItems.sort((a, b) => {
if (a.status === "near_complete" && b.status !== "near_complete") return -1;
if (a.status !== "near_complete" && b.status === "near_complete") return 1;
const aMaxScore = Math.max(...a.matches.map((m) => m.score));
const bMaxScore = Math.max(...b.matches.map((m) => m.score));
return bMaxScore - aMaxScore;
});
// Render list
const listContainer = resultsContainer.createDiv({ cls: "scan-items-list" });
listContainer.createEl("p", {
text: `${filteredItems.length} item(s)${this.isScanning ? " (scanning...)" : ""}`,
});
for (const item of filteredItems) {
const itemEl = listContainer.createDiv({ cls: "scan-item" });
const headerEl = itemEl.createDiv({ cls: "scan-item-header" });
headerEl.createEl("div", {
cls: "scan-item-title",
text: `${item.file}${item.heading ? `#${item.heading}` : ""}`,
});
const statusBadge = headerEl.createEl("span", {
cls: `scan-item-status status-${item.status}`,
text: item.status,
});
// Gap counts
const gapCountsEl = itemEl.createDiv({ cls: "scan-item-gaps" });
if (item.gapCounts.missingSlots > 0) {
gapCountsEl.createEl("span", {
text: `Missing Slots: ${item.gapCounts.missingSlots}`,
cls: "gap-badge",
});
}
if (item.gapCounts.missingLinks > 0) {
gapCountsEl.createEl("span", {
text: `Missing Links: ${item.gapCounts.missingLinks}`,
cls: "gap-badge",
});
}
if (item.gapCounts.weakRoles > 0) {
gapCountsEl.createEl("span", {
text: `Weak Roles: ${item.gapCounts.weakRoles}`,
cls: "gap-badge",
});
}
if (item.gapCounts.candidateCleanup > 0) {
gapCountsEl.createEl("span", {
text: `Candidate Cleanup: ${item.gapCounts.candidateCleanup}`,
cls: "gap-badge",
});
}
// Matches summary
const matchesEl = itemEl.createDiv({ cls: "scan-item-matches" });
matchesEl.createEl("div", {
text: `${item.matches.length} template match(es)`,
});
// Actions
const actionsEl = itemEl.createDiv({ cls: "scan-item-actions" });
const openBtn = actionsEl.createEl("button", {
text: "Open Workbench",
});
openBtn.addEventListener("click", async () => {
// Open file and section, then open workbench
await this.app.workspace.openLinkText(item.file, "", true);
// Wait a bit for file to open, then trigger workbench
setTimeout(async () => {
const { executeChainWorkbench } = await import("../commands/chainWorkbenchCommand");
const activeFile = this.app.workspace.getActiveFile();
const activeEditor = this.app.workspace.activeEditor?.editor;
if (activeFile && activeEditor) {
await executeChainWorkbench(
this.app,
activeEditor,
activeFile.path,
this.chainRoles,
this.chainTemplates,
undefined,
this.settings,
this.pluginInstance
);
}
}, 500);
});
const deprioritizeBtn = actionsEl.createEl("button", {
text: item.status === "deprioritized" ? "Prioritize" : "Deprioritize",
});
deprioritizeBtn.addEventListener("click", () => {
item.status = item.status === "deprioritized" ? "near_complete" : "deprioritized";
this.render();
});
}
}
onClose(): void {
const { contentEl } = this;
contentEl.empty();
}
}