Enhance chain inspection functionality with editor content support
- Updated `inspectChains` to accept optional `editorContent`, allowing for real-time inspection without relying on potentially stale vault data. - Introduced `buildNoteIndexFromContent` to facilitate graph indexing directly from provided content. - Improved handling of template matching profiles in `ChainWorkbenchModal`, ensuring accurate context during chain inspections. - Added debug logging for better traceability of the chain inspection process.
This commit is contained in:
parent
dbd76b764d
commit
99c77ef616
|
|
@ -679,6 +679,8 @@ function computeFindings(
|
|||
|
||||
/**
|
||||
* Inspect chains for current section context.
|
||||
* @param editorContent Optional: If provided, use this content instead of reading from vault.
|
||||
* This is useful when the file was just modified and vault cache is stale.
|
||||
*/
|
||||
export async function inspectChains(
|
||||
app: App,
|
||||
|
|
@ -688,7 +690,8 @@ export async function inspectChains(
|
|||
edgeVocabularyPath?: string,
|
||||
chainTemplates?: ChainTemplatesConfig | null,
|
||||
templatesLoadResult?: { path: string; status: string; loadedAt: number | null; templateCount: number },
|
||||
templateMatchingProfileName?: string
|
||||
templateMatchingProfileName?: string,
|
||||
editorContent?: string
|
||||
): Promise<ChainInspectorReport> {
|
||||
// Build index for current note
|
||||
const currentFile = app.vault.getAbstractFileByPath(context.file);
|
||||
|
|
@ -700,10 +703,11 @@ export async function inspectChains(
|
|||
throw new Error(`File not found or not a markdown file: ${context.file}`);
|
||||
}
|
||||
|
||||
const { edges: currentEdges, sections } = await buildNoteIndex(
|
||||
app,
|
||||
currentFile as TFile
|
||||
);
|
||||
// Use editor content if provided, otherwise read from vault
|
||||
const { buildNoteIndex, buildNoteIndexFromContent } = await import("./graphIndex");
|
||||
const { edges: currentEdges, sections } = editorContent
|
||||
? await buildNoteIndexFromContent(app, currentFile as TFile, editorContent)
|
||||
: await buildNoteIndex(app, currentFile as TFile);
|
||||
|
||||
// Collect all outgoing targets to load neighbor notes
|
||||
// Respect includeNoteLinks and includeCandidates toggles
|
||||
|
|
@ -905,7 +909,8 @@ export async function inspectChains(
|
|||
}
|
||||
|
||||
// Get section content for gap analysis
|
||||
const content = await app.vault.read(currentFile as TFile);
|
||||
// Use editor content if provided, otherwise read from vault
|
||||
const content = editorContent || await app.vault.read(currentFile as TFile);
|
||||
const sectionsWithContent = splitIntoSections(content);
|
||||
const currentSectionContent =
|
||||
sectionsWithContent[context.sectionIndex]?.content || "";
|
||||
|
|
|
|||
|
|
@ -83,13 +83,27 @@ function parseTarget(linkText: string, currentFilePath: string, sections: NoteSe
|
|||
}
|
||||
|
||||
/**
|
||||
* Build graph index for a single note.
|
||||
* Build graph index for a single note from file content.
|
||||
* Use buildNoteIndexFromContent if you have the content already (e.g., from editor).
|
||||
*/
|
||||
export async function buildNoteIndex(
|
||||
app: App,
|
||||
file: TFile
|
||||
): Promise<{ edges: IndexedEdge[]; sections: SectionNode[] }> {
|
||||
const content = await app.vault.read(file);
|
||||
return buildNoteIndexFromContent(app, file, content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build graph index for a single note from provided content.
|
||||
* This is useful when you have the content from an editor and want to avoid
|
||||
* reading stale data from the vault cache.
|
||||
*/
|
||||
export async function buildNoteIndexFromContent(
|
||||
app: App,
|
||||
file: TFile,
|
||||
content: string
|
||||
): Promise<{ edges: IndexedEdge[]; sections: SectionNode[] }> {
|
||||
const sections = splitIntoSections(content);
|
||||
const edges: IndexedEdge[] = [];
|
||||
const sectionNodes: SectionNode[] = [];
|
||||
|
|
|
|||
|
|
@ -105,7 +105,8 @@ export async function executeChainWorkbench(
|
|||
chainRoles,
|
||||
chainTemplates,
|
||||
vocabulary,
|
||||
pluginInstance
|
||||
pluginInstance,
|
||||
templatesLoadResult
|
||||
);
|
||||
modal.open();
|
||||
} catch (e) {
|
||||
|
|
|
|||
|
|
@ -8,12 +8,15 @@ import type { WorkbenchModel, WorkbenchMatch, WorkbenchTodoUnion } from "../work
|
|||
import type { MindnetSettings } from "../settings";
|
||||
import type { ChainRolesConfig, ChainTemplatesConfig } from "../dictionary/types";
|
||||
import type { Vocabulary } from "../vocab/Vocabulary";
|
||||
import type { IndexedEdge, SectionNode } from "../analysis/graphIndex";
|
||||
import type { DictionaryLoadResult } from "../dictionary/types";
|
||||
|
||||
export class ChainWorkbenchModal extends Modal {
|
||||
private model: WorkbenchModel;
|
||||
private settings: MindnetSettings;
|
||||
private chainRoles: ChainRolesConfig | null;
|
||||
private chainTemplates: ChainTemplatesConfig | null;
|
||||
private templatesLoadResult: DictionaryLoadResult<ChainTemplatesConfig> | undefined;
|
||||
private vocabulary: Vocabulary;
|
||||
private pluginInstance: any;
|
||||
|
||||
|
|
@ -28,13 +31,15 @@ export class ChainWorkbenchModal extends Modal {
|
|||
chainRoles: ChainRolesConfig | null,
|
||||
chainTemplates: ChainTemplatesConfig | null,
|
||||
vocabulary: Vocabulary,
|
||||
pluginInstance: any
|
||||
pluginInstance: any,
|
||||
templatesLoadResult?: DictionaryLoadResult<ChainTemplatesConfig>
|
||||
) {
|
||||
super(app);
|
||||
this.model = model;
|
||||
this.settings = settings;
|
||||
this.chainRoles = chainRoles;
|
||||
this.chainTemplates = chainTemplates;
|
||||
this.templatesLoadResult = templatesLoadResult;
|
||||
this.vocabulary = vocabulary;
|
||||
this.pluginInstance = pluginInstance;
|
||||
|
||||
|
|
@ -94,22 +99,37 @@ export class ChainWorkbenchModal extends Modal {
|
|||
private render(): void {
|
||||
const { contentEl } = this;
|
||||
|
||||
// Debug: Log render state
|
||||
console.log("[Chain Workbench] Render - model.matches.length:", this.model.matches.length);
|
||||
console.log("[Chain Workbench] Render - filterStatus:", this.filterStatus, "searchQuery:", this.searchQuery);
|
||||
|
||||
// Filter matches
|
||||
let filteredMatches = this.model.matches;
|
||||
if (this.filterStatus) {
|
||||
filteredMatches = filteredMatches.filter((m) => m.status === this.filterStatus);
|
||||
console.log("[Chain Workbench] After status filter:", filteredMatches.length);
|
||||
}
|
||||
if (this.searchQuery) {
|
||||
filteredMatches = filteredMatches.filter((m) =>
|
||||
m.templateName.toLowerCase().includes(this.searchQuery)
|
||||
);
|
||||
console.log("[Chain Workbench] After search filter:", filteredMatches.length);
|
||||
}
|
||||
|
||||
console.log("[Chain Workbench] Final filtered matches:", filteredMatches.length);
|
||||
|
||||
// Sort: near_complete first (default), then by score descending
|
||||
// Use stable sort to preserve order within same status/score groups
|
||||
filteredMatches.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;
|
||||
return b.score - a.score;
|
||||
const scoreDiff = b.score - a.score;
|
||||
if (scoreDiff !== 0) return scoreDiff;
|
||||
// Preserve original order for matches with same status and score
|
||||
// Use templateName + slotAssignments as tiebreaker for stability
|
||||
const aId = this.getMatchIdentifier(a);
|
||||
const bId = this.getMatchIdentifier(b);
|
||||
return aId.localeCompare(bId);
|
||||
});
|
||||
|
||||
// Render tree view (left column)
|
||||
|
|
@ -125,6 +145,19 @@ export class ChainWorkbenchModal extends Modal {
|
|||
|
||||
treeContainer.empty();
|
||||
treeContainer.createEl("h3", { text: "Templates & Chains" });
|
||||
|
||||
// Show message if no matches
|
||||
if (matches.length === 0) {
|
||||
const emptyMessage = treeContainer.createDiv({ cls: "workbench-empty-message" });
|
||||
emptyMessage.createEl("p", { text: "Keine Chains gefunden." });
|
||||
if (this.filterStatus || this.searchQuery) {
|
||||
emptyMessage.createEl("p", {
|
||||
cls: "workbench-empty-hint",
|
||||
text: "Versuchen Sie, die Filter zu löschen, um alle Chains anzuzeigen."
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Group matches by template name
|
||||
const matchesByTemplate = new Map<string, WorkbenchMatch[]>();
|
||||
|
|
@ -969,8 +1002,39 @@ export class ChainWorkbenchModal extends Modal {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a unique identifier for a match to restore selection after refresh.
|
||||
*/
|
||||
private getMatchIdentifier(match: WorkbenchMatch): string {
|
||||
// Create a stable identifier based on template name and slot assignments
|
||||
const slotKeys = Object.keys(match.slotAssignments).sort();
|
||||
const slotValues = slotKeys.map(key => {
|
||||
const assignment = match.slotAssignments[key];
|
||||
if (!assignment) return `${key}:null`;
|
||||
return `${key}:${assignment.file}#${assignment.heading || ''}`;
|
||||
});
|
||||
return `${match.templateName}|${slotValues.join('|')}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a match in the new model that corresponds to the saved identifier.
|
||||
*/
|
||||
private findMatchByIdentifier(identifier: string, matches: WorkbenchMatch[]): WorkbenchMatch | null {
|
||||
for (const match of matches) {
|
||||
if (this.getMatchIdentifier(match) === identifier) {
|
||||
return match;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private async refreshWorkbench(): Promise<void> {
|
||||
try {
|
||||
// Save current state before refresh
|
||||
const savedSelectedMatchId = this.selectedMatch ? this.getMatchIdentifier(this.selectedMatch) : null;
|
||||
const savedFilterStatus = this.filterStatus;
|
||||
const savedSearchQuery = this.searchQuery;
|
||||
|
||||
// Reload the workbench model without closing the modal
|
||||
const activeFile = this.app.workspace.getActiveFile();
|
||||
const activeEditor = this.app.workspace.activeEditor?.editor;
|
||||
|
|
@ -1002,8 +1066,34 @@ export class ChainWorkbenchModal extends Modal {
|
|||
const vocabText = await VocabularyLoader.loadText(this.app, this.settings.edgeVocabularyPath);
|
||||
const edgeVocabulary = parseEdgeVocabulary(vocabText);
|
||||
|
||||
// Inspect chains
|
||||
// Prepare templates source info for inspectChains
|
||||
// This is required for template matching to work!
|
||||
let templatesSourceInfo: { path: string; status: string; loadedAt: number | null; templateCount: number } | undefined;
|
||||
if (this.templatesLoadResult) {
|
||||
templatesSourceInfo = {
|
||||
path: this.templatesLoadResult.resolvedPath,
|
||||
status: this.templatesLoadResult.status,
|
||||
loadedAt: this.templatesLoadResult.loadedAt,
|
||||
templateCount: this.chainTemplates?.templates?.length || 0,
|
||||
};
|
||||
} else if (this.chainTemplates) {
|
||||
// Fallback: create a minimal templatesSourceInfo if templatesLoadResult is not available
|
||||
templatesSourceInfo = {
|
||||
path: this.settings.chainTemplatesPath || "unknown",
|
||||
status: "loaded",
|
||||
loadedAt: Date.now(),
|
||||
templateCount: this.chainTemplates.templates?.length || 0,
|
||||
};
|
||||
}
|
||||
|
||||
// Inspect chains - pass editor content if available to avoid stale vault cache
|
||||
const { inspectChains } = await import("../analysis/chainInspector");
|
||||
const editorContent = activeEditor ? activeEditor.getValue() : undefined;
|
||||
console.log("[Chain Workbench] Calling inspectChains with context:", context);
|
||||
console.log("[Chain Workbench] Using editor content:", !!editorContent);
|
||||
console.log("[Chain Workbench] chainTemplates:", this.chainTemplates ? "loaded" : "null");
|
||||
console.log("[Chain Workbench] chainRoles:", this.chainRoles ? "loaded" : "null");
|
||||
console.log("[Chain Workbench] templatesSourceInfo:", templatesSourceInfo ? "provided" : "null");
|
||||
const report = await inspectChains(
|
||||
this.app,
|
||||
context,
|
||||
|
|
@ -1011,20 +1101,36 @@ export class ChainWorkbenchModal extends Modal {
|
|||
this.chainRoles,
|
||||
this.settings.edgeVocabularyPath,
|
||||
this.chainTemplates,
|
||||
undefined,
|
||||
this.settings.templateMatchingProfile
|
||||
templatesSourceInfo,
|
||||
this.settings.templateMatchingProfile,
|
||||
editorContent
|
||||
);
|
||||
console.log("[Chain Workbench] inspectChains returned - templateMatches:", report.templateMatches?.length || 0);
|
||||
|
||||
// Build all edges index
|
||||
const { buildNoteIndex } = await import("../analysis/graphIndex");
|
||||
// Build all edges index for workbench model (using same editor content if available)
|
||||
const { buildNoteIndex, buildNoteIndexFromContent } = await import("../analysis/graphIndex");
|
||||
const fileObj = this.app.vault.getAbstractFileByPath(activeFile.path);
|
||||
if (!fileObj || !(fileObj instanceof TFile)) {
|
||||
throw new Error("Active file not found");
|
||||
}
|
||||
const { edges: allEdges } = await buildNoteIndex(this.app, fileObj);
|
||||
|
||||
// Use editor content if available to ensure consistency with inspectChains
|
||||
let allEdges: IndexedEdge[];
|
||||
if (editorContent) {
|
||||
const result = await buildNoteIndexFromContent(this.app, fileObj, editorContent);
|
||||
allEdges = result.edges;
|
||||
console.log("[Chain Workbench] Built index from editor content for workbench, edges:", allEdges.length);
|
||||
} else {
|
||||
const result = await buildNoteIndex(this.app, fileObj);
|
||||
allEdges = result.edges;
|
||||
console.log("[Chain Workbench] Built index from vault for workbench, edges:", allEdges.length);
|
||||
}
|
||||
|
||||
// Build workbench model
|
||||
const { buildWorkbenchModel } = await import("../workbench/workbenchBuilder");
|
||||
console.log("[Chain Workbench] Building workbench model...");
|
||||
console.log("[Chain Workbench] Report has templateMatches:", report.templateMatches?.length || 0);
|
||||
console.log("[Chain Workbench] allEdges count:", allEdges.length);
|
||||
const newModel = await buildWorkbenchModel(
|
||||
this.app,
|
||||
report,
|
||||
|
|
@ -1035,11 +1141,48 @@ export class ChainWorkbenchModal extends Modal {
|
|||
this.settings.debugLogging
|
||||
);
|
||||
|
||||
// Update model and re-render
|
||||
// Update model
|
||||
this.model = newModel;
|
||||
this.selectedMatch = null; // Reset selection
|
||||
this.filterStatus = null; // Reset filters
|
||||
this.searchQuery = ""; // Reset search
|
||||
|
||||
// Debug: Log model state
|
||||
console.log("[Chain Workbench] Refresh complete - matches:", newModel.matches.length);
|
||||
if (newModel.matches.length === 0) {
|
||||
console.warn("[Chain Workbench] WARNING: No matches found after refresh!");
|
||||
console.log("[Chain Workbench] Report templateMatches:", report.templateMatches?.length || 0);
|
||||
console.log("[Chain Workbench] Context:", context);
|
||||
console.log("[Chain Workbench] chainTemplates:", this.chainTemplates ? `loaded (${this.chainTemplates.templates?.length || 0} templates)` : "null");
|
||||
console.log("[Chain Workbench] allEdges:", allEdges.length);
|
||||
if (report.templateMatches && report.templateMatches.length > 0) {
|
||||
console.warn("[Chain Workbench] Report HAS templateMatches but model.matches is empty!");
|
||||
console.log("[Chain Workbench] First templateMatch:", report.templateMatches[0]);
|
||||
}
|
||||
} else {
|
||||
console.log("[Chain Workbench] Model has matches, first match:", newModel.matches[0]?.templateName);
|
||||
}
|
||||
|
||||
// Restore selection if possible
|
||||
if (savedSelectedMatchId && newModel.matches.length > 0) {
|
||||
const restoredMatch = this.findMatchByIdentifier(savedSelectedMatchId, newModel.matches);
|
||||
this.selectedMatch = restoredMatch;
|
||||
// If exact match not found, try to find a similar match (same template)
|
||||
if (!this.selectedMatch && savedSelectedMatchId) {
|
||||
const templateName = savedSelectedMatchId.split('|')[0];
|
||||
const templateMatches = newModel.matches.filter(m => m.templateName === templateName);
|
||||
if (templateMatches.length > 0) {
|
||||
// Select the first match of the same template as fallback
|
||||
this.selectedMatch = templateMatches[0] || null;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.selectedMatch = null;
|
||||
}
|
||||
|
||||
// Restore filters
|
||||
this.filterStatus = savedFilterStatus;
|
||||
this.searchQuery = savedSearchQuery;
|
||||
|
||||
// Debug: Log filter state
|
||||
console.log("[Chain Workbench] Filter status:", this.filterStatus, "Search query:", this.searchQuery);
|
||||
|
||||
// Clear and re-render the entire UI
|
||||
const { contentEl } = this;
|
||||
|
|
@ -1062,6 +1205,10 @@ export class ChainWorkbenchModal extends Modal {
|
|||
statusSelect.createEl("option", { text: "Near Complete", value: "near_complete" });
|
||||
statusSelect.createEl("option", { text: "Partial", value: "partial" });
|
||||
statusSelect.createEl("option", { text: "Weak", value: "weak" });
|
||||
// Restore filter value
|
||||
if (this.filterStatus) {
|
||||
statusSelect.value = this.filterStatus;
|
||||
}
|
||||
statusSelect.addEventListener("change", (e) => {
|
||||
const target = e.target as HTMLSelectElement;
|
||||
this.filterStatus = target.value || null;
|
||||
|
|
@ -1070,6 +1217,10 @@ export class ChainWorkbenchModal extends Modal {
|
|||
|
||||
filterContainer.createEl("label", { text: "Search:" });
|
||||
const searchInput = filterContainer.createEl("input", { type: "text", placeholder: "Template name..." });
|
||||
// Restore search query
|
||||
if (this.searchQuery) {
|
||||
searchInput.value = this.searchQuery;
|
||||
}
|
||||
searchInput.addEventListener("input", (e) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
this.searchQuery = target.value.toLowerCase();
|
||||
|
|
@ -1088,7 +1239,9 @@ export class ChainWorkbenchModal extends Modal {
|
|||
detailsContainer.createEl("h3", { text: "Chain Details" });
|
||||
|
||||
// Now render the content
|
||||
console.log("[Chain Workbench] About to call render() - model.matches.length:", this.model.matches.length);
|
||||
this.render();
|
||||
console.log("[Chain Workbench] render() completed");
|
||||
} catch (error) {
|
||||
console.error("[Chain Workbench] Error refreshing workbench:", error);
|
||||
new Notice(`Error refreshing workbench: ${error instanceof Error ? error.message : String(error)}`);
|
||||
|
|
|
|||
|
|
@ -26,12 +26,19 @@ export async function buildWorkbenchModel(
|
|||
const matches: WorkbenchMatch[] = [];
|
||||
|
||||
if (!report.templateMatches || !chainTemplates) {
|
||||
console.warn("[buildWorkbenchModel] No templateMatches or chainTemplates:", {
|
||||
hasTemplateMatches: !!report.templateMatches,
|
||||
templateMatchesCount: report.templateMatches?.length || 0,
|
||||
hasChainTemplates: !!chainTemplates,
|
||||
});
|
||||
return {
|
||||
context: report.context,
|
||||
matches: [],
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
console.log("[buildWorkbenchModel] Processing", report.templateMatches.length, "template matches");
|
||||
|
||||
// Get candidate edges from current note
|
||||
// Also check for confirmed edges that would make candidates "not needed"
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user