Enhance interview configuration and wizard functionality
Some checks are pending
Node.js build / build (20.x) (push) Waiting to run
Node.js build / build (22.x) (push) Waiting to run

- 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.
This commit is contained in:
Lars 2026-01-17 10:31:44 +01:00
parent a6ce268b04
commit 5ed06e6b9a
6 changed files with 263 additions and 6 deletions

View File

@ -118,6 +118,25 @@ function parseProfile(
profile.defaults = raw.defaults as Record<string, unknown>; profile.defaults = raw.defaults as Record<string, unknown>;
} }
// Parse edging config
if (raw.edging && typeof raw.edging === "object") {
const edgingRaw = raw.edging as Record<string, unknown>;
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 // Parse steps
if (Array.isArray(raw.steps)) { if (Array.isArray(raw.steps)) {
for (let i = 0; i < raw.steps.length; i++) { for (let i = 0; i < raw.steps.length; i++) {

View File

@ -15,6 +15,12 @@ export interface InterviewProfile {
group?: string; group?: string;
defaults?: Record<string, unknown>; defaults?: Record<string, unknown>;
steps: InterviewStep[]; 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 = export type InterviewStep =

View File

@ -489,7 +489,8 @@ export default class MindnetCausalAssistantPlugin extends Plugin {
}, },
async (wizardResult: WizardResult) => { async (wizardResult: WizardResult) => {
new Notice("Interview saved and changes applied"); new Notice("Interview saved and changes applied");
} },
this // Pass plugin instance for graph schema loading
); );
} catch (e) { } catch (e) {
const msg = e instanceof Error ? e.message : String(e); const msg = e instanceof Error ? e.message : String(e);
@ -887,7 +888,8 @@ export default class MindnetCausalAssistantPlugin extends Plugin {
}, },
async (wizardResult: WizardResult) => { async (wizardResult: WizardResult) => {
new Notice("Interview saved and changes applied"); new Notice("Interview saved and changes applied");
} },
this // Pass plugin instance for graph schema loading
); );
} catch (e) { } catch (e) {
const msg = e instanceof Error ? e.message : String(e); const msg = e instanceof Error ? e.message : String(e);

View File

@ -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)
});
});

View File

@ -29,6 +29,8 @@ import { renderProfileToMarkdown, type RenderAnswers } from "../interview/render
import { NoteIndex } from "../entityPicker/noteIndex"; import { NoteIndex } from "../entityPicker/noteIndex";
import { EntityPickerModal, type EntityPickerResult } from "./EntityPickerModal"; import { EntityPickerModal, type EntityPickerResult } from "./EntityPickerModal";
import { insertWikilinkIntoTextarea } from "../entityPicker/wikilink"; import { insertWikilinkIntoTextarea } from "../entityPicker/wikilink";
import { buildSemanticMappings, type BuildResult } from "../mapping/semanticMappingBuilder";
import type { MindnetSettings } from "../settings";
import { import {
type LoopRuntimeState, type LoopRuntimeState,
createLoopState, createLoopState,
@ -57,7 +59,10 @@ export class InterviewWizardModal extends Modal {
fileContent: string; fileContent: string;
onSubmit: (result: WizardResult) => void; onSubmit: (result: WizardResult) => void;
onSaveAndExit: (result: WizardResult) => void; onSaveAndExit: (result: WizardResult) => void;
profile: InterviewProfile;
profileKey: string; profileKey: string;
settings?: MindnetSettings; // Optional settings for post-run edging
plugin?: { ensureGraphSchemaLoaded?: () => Promise<import("../mapping/graphSchema").GraphSchema | null> }; // Optional plugin instance
// Store current input values to save on navigation // Store current input values to save on navigation
private currentInputValues: Map<string, string> = new Map(); private currentInputValues: Map<string, string> = new Map();
// Store preview mode state per step key // Store preview mode state per step key
@ -71,7 +76,9 @@ export class InterviewWizardModal extends Modal {
file: TFile, file: TFile,
fileContent: string, fileContent: string,
onSubmit: (result: WizardResult) => void, onSubmit: (result: WizardResult) => void,
onSaveAndExit: (result: WizardResult) => void onSaveAndExit: (result: WizardResult) => void,
settings?: MindnetSettings,
plugin?: { ensureGraphSchemaLoaded?: () => Promise<import("../mapping/graphSchema").GraphSchema | null> }
) { ) {
super(app); super(app);
@ -81,7 +88,10 @@ export class InterviewWizardModal extends Modal {
throw new Error("Profile is required"); throw new Error("Profile is required");
} }
this.profile = profile;
this.profileKey = profile.key; this.profileKey = profile.key;
this.settings = settings;
this.plugin = plugin;
// Log profile info // Log profile info
const stepKinds = profile.steps?.map(s => s.type) || []; const stepKinds = profile.steps?.map(s => s.type) || [];
@ -89,6 +99,9 @@ export class InterviewWizardModal extends Modal {
key: profile.key, key: profile.key,
stepCount: profile.steps?.length, stepCount: profile.steps?.length,
kinds: stepKinds, 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 // Validate steps - only throw if profile.steps is actually empty
@ -2255,7 +2268,7 @@ export class InterviewWizardModal extends Modal {
.setButtonText(isReview ? "Apply & Finish" : "Next") .setButtonText(isReview ? "Apply & Finish" : "Next")
.setCta() .setCta()
.setDisabled((!canGoNext(this.state) && !isReview) || !canProceedLoop); .setDisabled((!canGoNext(this.state) && !isReview) || !canProceedLoop);
button.onClick(() => { button.onClick(async () => {
if (isReview) { if (isReview) {
console.log("=== FINISH WIZARD (Apply & Finish) ==="); console.log("=== FINISH WIZARD (Apply & Finish) ===");
// Save current step data before finishing // Save current step data before finishing
@ -2264,6 +2277,20 @@ export class InterviewWizardModal extends Modal {
this.saveCurrentStepData(currentStep); this.saveCurrentStepData(currentStep);
} }
this.applyPatches(); 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.onSubmit({ applied: true, patches: this.state.patches });
this.close(); this.close();
} else { } else {
@ -2289,6 +2316,64 @@ export class InterviewWizardModal extends Modal {
}); });
} }
/**
* Run semantic mapping builder after interview finish (post_run mode).
*/
private async runPostRunEdging(): Promise<void> {
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 { goNext(): void {
const currentStep = getCurrentStep(this.state); const currentStep = getCurrentStep(this.state);

View File

@ -47,7 +47,8 @@ export async function startWizardAfterCreate(
content: string, content: string,
isUnresolvedClick: boolean, isUnresolvedClick: boolean,
onWizardComplete: (result: any) => void, onWizardComplete: (result: any) => void,
onWizardSave: (result: any) => void onWizardSave: (result: any) => void,
pluginInstance?: { ensureGraphSchemaLoaded?: () => Promise<import("../mapping/graphSchema").GraphSchema | null> }
): Promise<void> { ): Promise<void> {
// Determine if wizard should start // Determine if wizard should start
const shouldStartInterview = isUnresolvedClick const shouldStartInterview = isUnresolvedClick
@ -106,7 +107,9 @@ export async function startWizardAfterCreate(
file, file,
content, content,
onWizardComplete, onWizardComplete,
onWizardSave onWizardSave,
settings, // Pass settings for post-run edging
pluginInstance // Pass plugin instance for graph schema loading
).open(); ).open();
} catch (e) { } catch (e) {
const msg = e instanceof Error ? e.message : String(e); const msg = e instanceof Error ? e.message : String(e);