From 5ed06e6b9af55b97bfd9f95e08313473e4503d88 Mon Sep 17 00:00:00 2001 From: Lars Date: Sat, 17 Jan 2026 10:31:44 +0100 Subject: [PATCH] Enhance interview configuration and wizard functionality - Added parsing for new edging configuration options in interview profiles, including mode, wrapper callout type, title, and folded state. - Updated the InterviewWizardModal to support post-run edging, allowing for semantic mapping based on the new edging settings. - Enhanced the unresolved link handler to pass plugin instance and settings for improved graph schema loading and post-run edging functionality. - Improved error handling and user notifications for semantic mapping processes, ensuring better feedback during execution. --- src/interview/parseInterviewConfig.ts | 19 +++ src/interview/types.ts | 6 + src/main.ts | 6 +- src/tests/interview/postRunEdging.test.ts | 142 ++++++++++++++++++++ src/ui/InterviewWizardModal.ts | 89 +++++++++++- src/unresolvedLink/unresolvedLinkHandler.ts | 7 +- 6 files changed, 263 insertions(+), 6 deletions(-) create mode 100644 src/tests/interview/postRunEdging.test.ts diff --git a/src/interview/parseInterviewConfig.ts b/src/interview/parseInterviewConfig.ts index 12f1824..7ae772c 100644 --- a/src/interview/parseInterviewConfig.ts +++ b/src/interview/parseInterviewConfig.ts @@ -118,6 +118,25 @@ function parseProfile( profile.defaults = raw.defaults as Record; } + // Parse edging config + if (raw.edging && typeof raw.edging === "object") { + const edgingRaw = raw.edging as Record; + profile.edging = {}; + + if (edgingRaw.mode === "none" || edgingRaw.mode === "post_run" || edgingRaw.mode === "inline_micro") { + profile.edging.mode = edgingRaw.mode; + } + if (typeof edgingRaw.wrapperCalloutType === "string") { + profile.edging.wrapperCalloutType = edgingRaw.wrapperCalloutType; + } + if (typeof edgingRaw.wrapperTitle === "string") { + profile.edging.wrapperTitle = edgingRaw.wrapperTitle; + } + if (typeof edgingRaw.wrapperFolded === "boolean") { + profile.edging.wrapperFolded = edgingRaw.wrapperFolded; + } + } + // Parse steps if (Array.isArray(raw.steps)) { for (let i = 0; i < raw.steps.length; i++) { diff --git a/src/interview/types.ts b/src/interview/types.ts index 53373ad..8470d69 100644 --- a/src/interview/types.ts +++ b/src/interview/types.ts @@ -15,6 +15,12 @@ export interface InterviewProfile { group?: string; defaults?: Record; steps: InterviewStep[]; + edging?: { + mode?: "none" | "post_run" | "inline_micro"; // Semantic mapping mode (default: "none") + wrapperCalloutType?: string; // Override wrapper callout type + wrapperTitle?: string; // Override wrapper title + wrapperFolded?: boolean; // Override wrapper folded state + }; } export type InterviewStep = diff --git a/src/main.ts b/src/main.ts index d7c8bfd..052b3a9 100644 --- a/src/main.ts +++ b/src/main.ts @@ -489,7 +489,8 @@ export default class MindnetCausalAssistantPlugin extends Plugin { }, async (wizardResult: WizardResult) => { new Notice("Interview saved and changes applied"); - } + }, + this // Pass plugin instance for graph schema loading ); } catch (e) { const msg = e instanceof Error ? e.message : String(e); @@ -887,7 +888,8 @@ export default class MindnetCausalAssistantPlugin extends Plugin { }, async (wizardResult: WizardResult) => { new Notice("Interview saved and changes applied"); - } + }, + this // Pass plugin instance for graph schema loading ); } catch (e) { const msg = e instanceof Error ? e.message : String(e); diff --git a/src/tests/interview/postRunEdging.test.ts b/src/tests/interview/postRunEdging.test.ts new file mode 100644 index 0000000..d6ee185 --- /dev/null +++ b/src/tests/interview/postRunEdging.test.ts @@ -0,0 +1,142 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import type { InterviewProfile } from "../../interview/types"; + +describe("Post-run edging integration", () => { + let mockApp: any; + let mockFile: any; + let mockSettings: any; + let mockBuildSemanticMappings: any; + + beforeEach(() => { + mockApp = { + vault: { + read: vi.fn().mockResolvedValue("Content"), + }, + }; + mockFile = { + path: "test.md", + basename: "test", + }; + mockSettings = { + mappingWrapperCalloutType: "abstract", + mappingWrapperTitle: "🕸️ Semantic Mapping", + mappingWrapperFolded: true, + unassignedHandling: "prompt", + allowOverwriteExistingMappings: false, + }; + mockBuildSemanticMappings = vi.fn().mockResolvedValue({ + sectionsProcessed: 2, + sectionsWithMappings: 2, + totalLinks: 5, + existingMappingsKept: 3, + newMappingsAssigned: 2, + unmappedLinksSkipped: 0, + }); + }); + + it("should invoke builder when profile has edging.mode=post_run", async () => { + const profile: InterviewProfile = { + key: "test-profile", + label: "Test Profile", + note_type: "note", + steps: [], + edging: { + mode: "post_run", + }, + }; + + // This is a unit test for the hook path + // In real implementation, this would be called from InterviewWizardModal + const shouldRunEdging = profile.edging?.mode === "post_run"; + expect(shouldRunEdging).toBe(true); + }); + + it("should not invoke builder when profile has edging.mode=none", () => { + const profile: InterviewProfile = { + key: "test-profile", + label: "Test Profile", + note_type: "note", + steps: [], + edging: { + mode: "none", + }, + }; + + const shouldRunEdging = profile.edging?.mode === "post_run"; + expect(shouldRunEdging).toBe(false); + }); + + it("should not invoke builder when profile has no edging config", () => { + const profile: InterviewProfile = { + key: "test-profile", + label: "Test Profile", + note_type: "note", + steps: [], + }; + + const shouldRunEdging = profile.edging?.mode === "post_run"; + expect(shouldRunEdging).toBe(false); + }); + + it("should use profile edging config over settings", () => { + const profile: InterviewProfile = { + key: "test-profile", + label: "Test Profile", + note_type: "note", + steps: [], + edging: { + mode: "post_run", + wrapperCalloutType: "info", + wrapperTitle: "Custom Title", + wrapperFolded: false, + }, + }; + + const settings = { + mappingWrapperCalloutType: "abstract", + mappingWrapperTitle: "Default Title", + mappingWrapperFolded: true, + }; + + // Profile config should override settings + const finalCalloutType = profile.edging?.wrapperCalloutType || settings.mappingWrapperCalloutType; + const finalTitle = profile.edging?.wrapperTitle || settings.mappingWrapperTitle; + const finalFolded = profile.edging?.wrapperFolded !== undefined + ? profile.edging.wrapperFolded + : settings.mappingWrapperFolded; + + expect(finalCalloutType).toBe("info"); + expect(finalTitle).toBe("Custom Title"); + expect(finalFolded).toBe(false); + }); + + it("should use settings defaults when profile edging config is partial", () => { + const profile: InterviewProfile = { + key: "test-profile", + label: "Test Profile", + note_type: "note", + steps: [], + edging: { + mode: "post_run", + wrapperCalloutType: "info", + // wrapperTitle and wrapperFolded not specified + }, + }; + + const settings = { + mappingWrapperCalloutType: "abstract", + mappingWrapperTitle: "Default Title", + mappingWrapperFolded: true, + }; + + const finalCalloutType = profile.edging?.wrapperCalloutType || settings.mappingWrapperCalloutType; + const finalTitle = profile.edging?.wrapperTitle || settings.mappingWrapperTitle; + const finalFolded = profile.edging?.wrapperFolded !== undefined + ? profile.edging.wrapperFolded + : settings.mappingWrapperFolded; + + expect(finalCalloutType).toBe("info"); // From profile + expect(finalTitle).toBe("Default Title"); // From settings (fallback) + expect(finalFolded).toBe(true); // From settings (fallback) + }); +}); diff --git a/src/ui/InterviewWizardModal.ts b/src/ui/InterviewWizardModal.ts index 5a067e0..5ee2211 100644 --- a/src/ui/InterviewWizardModal.ts +++ b/src/ui/InterviewWizardModal.ts @@ -29,6 +29,8 @@ import { renderProfileToMarkdown, type RenderAnswers } from "../interview/render import { NoteIndex } from "../entityPicker/noteIndex"; import { EntityPickerModal, type EntityPickerResult } from "./EntityPickerModal"; import { insertWikilinkIntoTextarea } from "../entityPicker/wikilink"; +import { buildSemanticMappings, type BuildResult } from "../mapping/semanticMappingBuilder"; +import type { MindnetSettings } from "../settings"; import { type LoopRuntimeState, createLoopState, @@ -57,7 +59,10 @@ export class InterviewWizardModal extends Modal { fileContent: string; onSubmit: (result: WizardResult) => void; onSaveAndExit: (result: WizardResult) => void; + profile: InterviewProfile; profileKey: string; + settings?: MindnetSettings; // Optional settings for post-run edging + plugin?: { ensureGraphSchemaLoaded?: () => Promise }; // Optional plugin instance // Store current input values to save on navigation private currentInputValues: Map = new Map(); // Store preview mode state per step key @@ -71,7 +76,9 @@ export class InterviewWizardModal extends Modal { file: TFile, fileContent: string, onSubmit: (result: WizardResult) => void, - onSaveAndExit: (result: WizardResult) => void + onSaveAndExit: (result: WizardResult) => void, + settings?: MindnetSettings, + plugin?: { ensureGraphSchemaLoaded?: () => Promise } ) { super(app); @@ -81,7 +88,10 @@ export class InterviewWizardModal extends Modal { throw new Error("Profile is required"); } + this.profile = profile; this.profileKey = profile.key; + this.settings = settings; + this.plugin = plugin; // Log profile info const stepKinds = profile.steps?.map(s => s.type) || []; @@ -89,6 +99,9 @@ export class InterviewWizardModal extends Modal { key: profile.key, stepCount: profile.steps?.length, kinds: stepKinds, + edgingMode: profile.edging?.mode || "none", + edging: profile.edging, // Full edging object for debugging + profileKeys: Object.keys(profile), // All keys in profile }); // Validate steps - only throw if profile.steps is actually empty @@ -2255,7 +2268,7 @@ export class InterviewWizardModal extends Modal { .setButtonText(isReview ? "Apply & Finish" : "Next") .setCta() .setDisabled((!canGoNext(this.state) && !isReview) || !canProceedLoop); - button.onClick(() => { + button.onClick(async () => { if (isReview) { console.log("=== FINISH WIZARD (Apply & Finish) ==="); // Save current step data before finishing @@ -2264,6 +2277,20 @@ export class InterviewWizardModal extends Modal { this.saveCurrentStepData(currentStep); } this.applyPatches(); + + // Run semantic mapping builder if edging mode is post_run + console.log("[Mindnet] Checking edging mode:", { + profileKey: this.profile.key, + edgingMode: this.profile.edging?.mode, + hasEdging: !!this.profile.edging, + }); + if (this.profile.edging?.mode === "post_run") { + console.log("[Mindnet] Starting post-run edging"); + await this.runPostRunEdging(); + } else { + console.log("[Mindnet] Post-run edging skipped (mode:", this.profile.edging?.mode || "none", ")"); + } + this.onSubmit({ applied: true, patches: this.state.patches }); this.close(); } else { @@ -2289,6 +2316,64 @@ export class InterviewWizardModal extends Modal { }); } + /** + * Run semantic mapping builder after interview finish (post_run mode). + */ + private async runPostRunEdging(): Promise { + console.log("[Mindnet] runPostRunEdging called", { + hasSettings: !!this.settings, + hasPlugin: !!this.plugin, + file: this.file.path, + }); + + if (!this.settings) { + console.warn("[Mindnet] Cannot run post-run edging: settings not provided"); + new Notice("Edging: Settings nicht verfügbar"); + return; + } + + try { + console.log("[Mindnet] Starting semantic mapping builder"); + // Create settings override from profile edging config + const edgingSettings: MindnetSettings = { + ...this.settings, + mappingWrapperCalloutType: this.profile.edging?.wrapperCalloutType || this.settings.mappingWrapperCalloutType, + mappingWrapperTitle: this.profile.edging?.wrapperTitle || this.settings.mappingWrapperTitle, + mappingWrapperFolded: this.profile.edging?.wrapperFolded !== undefined + ? this.profile.edging.wrapperFolded + : this.settings.mappingWrapperFolded, + // Use prompt mode for unmapped links (default behavior) + unassignedHandling: "prompt", + // Don't overwrite existing mappings unless explicitly allowed + allowOverwriteExistingMappings: false, + }; + + // Run semantic mapping builder + const result: BuildResult = await buildSemanticMappings( + this.app, + this.file, + edgingSettings, + false, // allowOverwrite: false (respect existing) + this.plugin + ); + + // Show summary notice + const summary = [ + `Edger: Sections updated ${result.sectionsWithMappings}`, + `kept ${result.existingMappingsKept}`, + `changed ${result.newMappingsAssigned}`, + ]; + if (result.unmappedLinksSkipped > 0) { + summary.push(`skipped ${result.unmappedLinksSkipped}`); + } + new Notice(summary.join(", ")); + } catch (e) { + const msg = e instanceof Error ? e.message : String(e); + new Notice(`Failed to run semantic mapping builder: ${msg}`); + console.error("[Mindnet] Failed to run post-run edging:", e); + } + } + goNext(): void { const currentStep = getCurrentStep(this.state); diff --git a/src/unresolvedLink/unresolvedLinkHandler.ts b/src/unresolvedLink/unresolvedLinkHandler.ts index 76c3f84..63b0b52 100644 --- a/src/unresolvedLink/unresolvedLinkHandler.ts +++ b/src/unresolvedLink/unresolvedLinkHandler.ts @@ -47,7 +47,8 @@ export async function startWizardAfterCreate( content: string, isUnresolvedClick: boolean, onWizardComplete: (result: any) => void, - onWizardSave: (result: any) => void + onWizardSave: (result: any) => void, + pluginInstance?: { ensureGraphSchemaLoaded?: () => Promise } ): Promise { // Determine if wizard should start const shouldStartInterview = isUnresolvedClick @@ -106,7 +107,9 @@ export async function startWizardAfterCreate( file, content, onWizardComplete, - onWizardSave + onWizardSave, + settings, // Pass settings for post-run edging + pluginInstance // Pass plugin instance for graph schema loading ).open(); } catch (e) { const msg = e instanceof Error ? e.message : String(e);