From 0a346d3886f9591b374358f595632342722413b1 Mon Sep 17 00:00:00 2001 From: Lars Date: Sat, 7 Feb 2026 21:22:35 +0100 Subject: [PATCH] Enhance profile selection and interview wizard functionality - Introduced preferred note types in ProfileSelectionModal to prioritize user selections, improving the user experience during profile selection. - Updated InterviewWizardModal to accept initial pending edge assignments, allowing for better state management and user feedback. - Added a new action, `create_section_in_note`, to the todo generation process, expanding the capabilities of the interview workflow. - Enhanced the startWizardAfterCreate function to support initial pending edge assignments, streamlining the wizard initiation process. - Improved CSS styles for preferred profiles, enhancing visual distinction in the profile selection interface. --- src/ui/ChainWorkbenchModal.ts | 40 ++ src/ui/InterviewWizardModal.ts | 10 +- src/ui/ProfileSelectionModal.ts | 170 ++++--- src/unresolvedLink/unresolvedLinkHandler.ts | 6 +- src/workbench/createSectionAction.ts | 462 ++++++++++++++++++++ src/workbench/interviewOrchestration.ts | 22 +- src/workbench/todoGenerator.ts | 2 +- src/workbench/types.ts | 2 +- styles.css | 10 + 9 files changed, 650 insertions(+), 74 deletions(-) create mode 100644 src/workbench/createSectionAction.ts diff --git a/src/ui/ChainWorkbenchModal.ts b/src/ui/ChainWorkbenchModal.ts index ac12191..142bfcf 100644 --- a/src/ui/ChainWorkbenchModal.ts +++ b/src/ui/ChainWorkbenchModal.ts @@ -643,6 +643,7 @@ export class ChainWorkbenchModal extends Modal { const labels: Record = { link_existing: "Link Existing", create_note_via_interview: "Create Note", + create_section_in_note: "Create Section", insert_edge_forward: "Insert Edge", insert_edge_inverse: "Insert Inverse", choose_target_anchor: "Choose Anchor", @@ -867,6 +868,45 @@ export class ChainWorkbenchModal extends Modal { } break; + case "create_section_in_note": + if (todo.type === "missing_slot") { + const { createSectionInNote } = await import("../workbench/createSectionAction"); + + // Find the match for this todo + const matchForTodo = this.model.matches.find((m) => + m.todos.some((t) => t.id === todo.id) + ); + + if (!matchForTodo) { + new Notice("Could not find match for todo"); + return; + } + + // Get edge vocabulary + const { VocabularyLoader } = await import("../vocab/VocabularyLoader"); + const { parseEdgeVocabulary } = await import("../vocab/parseEdgeVocabulary"); + const vocabText = await VocabularyLoader.loadText(this.app, this.settings.edgeVocabularyPath); + const edgeVocabulary = parseEdgeVocabulary(vocabText); + + await createSectionInNote( + this.app, + activeEditor ?? null, + activeFile ?? null, + todo, + this.vocabulary, + edgeVocabulary, + this.settings, + matchForTodo.templateName, + matchForTodo, + this.chainTemplates, + this.chainRoles + ); + + // Workbench mit neuen Daten aktualisieren (wie beim Anlegen von Edges) + await this.refreshWorkbench(); + } + break; + default: new Notice(`Action "${action}" not yet implemented`); } diff --git a/src/ui/InterviewWizardModal.ts b/src/ui/InterviewWizardModal.ts index e2ddc1d..9699fa6 100644 --- a/src/ui/InterviewWizardModal.ts +++ b/src/ui/InterviewWizardModal.ts @@ -93,7 +93,8 @@ export class InterviewWizardModal extends Modal { onSubmit: (result: WizardResult) => void, onSaveAndExit: (result: WizardResult) => void, settings?: MindnetSettings, - plugin?: { ensureGraphSchemaLoaded?: () => Promise } + plugin?: { ensureGraphSchemaLoaded?: () => Promise }, + initialPendingEdgeAssignments?: PendingEdgeAssignment[] ) { super(app); @@ -117,6 +118,7 @@ export class InterviewWizardModal extends Modal { edgingMode: profile.edging?.mode || "none", edging: profile.edging, // Full edging object for debugging profileKeys: Object.keys(profile), // All keys in profile + initialPendingEdgeAssignments: initialPendingEdgeAssignments?.length || 0, }); // Validate steps - only throw if profile.steps is actually empty @@ -130,6 +132,12 @@ export class InterviewWizardModal extends Modal { this.state = createWizardState(profile); + // Inject initial pending edge assignments if provided + if (initialPendingEdgeAssignments && initialPendingEdgeAssignments.length > 0) { + this.state.pendingEdgeAssignments = [...initialPendingEdgeAssignments]; + console.log("[InterviewWizardModal] Injected initial pending edge assignments:", initialPendingEdgeAssignments.length); + } + // Initialize note index for entity picker this.noteIndex = new NoteIndex(this.app); diff --git a/src/ui/ProfileSelectionModal.ts b/src/ui/ProfileSelectionModal.ts index a9a9868..e5d1a96 100644 --- a/src/ui/ProfileSelectionModal.ts +++ b/src/ui/ProfileSelectionModal.ts @@ -16,19 +16,22 @@ export class ProfileSelectionModal extends Modal { initialTitle: string; defaultFolderPath: string; private noteIndex: NoteIndex; + preferredNoteTypes: string[] = []; // Note types to mark as preferred constructor( app: App, config: InterviewConfig, onSubmit: (result: ProfileSelectionResult) => void, initialTitle: string = "", - defaultFolderPath: string = "" + defaultFolderPath: string = "", + preferredNoteTypes: string[] = [] ) { super(app); this.config = config; this.onSubmit = onSubmit; this.initialTitle = initialTitle; this.defaultFolderPath = defaultFolderPath; + this.preferredNoteTypes = preferredNoteTypes; this.noteIndex = new NoteIndex(app); } @@ -106,41 +109,75 @@ export class ProfileSelectionModal extends Modal { } }; + // Sort profiles: preferred first, then others + const sortProfiles = (profiles: InterviewProfile[]): InterviewProfile[] => { + return [...profiles].sort((a, b) => { + const aPreferred = this.preferredNoteTypes.includes(a.note_type); + const bPreferred = this.preferredNoteTypes.includes(b.note_type); + if (aPreferred && !bPreferred) return -1; + if (!aPreferred && bPreferred) return 1; + return 0; + }); + }; + // Profile selection (grouped) for (const [groupName, profiles] of grouped.entries()) { contentEl.createEl("h3", { text: groupName }); - for (const profile of profiles) { + const sortedProfiles = sortProfiles(profiles); + let firstPreferredSelected = false; + + for (const profile of sortedProfiles) { + const isPreferred = this.preferredNoteTypes.includes(profile.note_type); const setting = new Setting(contentEl) .setName(profile.label) - .setDesc(`Type: ${profile.note_type}`) - .addButton((button) => { - button.setButtonText("Select").onClick(() => { - // Clear previous selection - contentEl.querySelectorAll(".profile-selected").forEach((el) => { - if (el instanceof HTMLElement) { - el.removeClass("profile-selected"); - } - }); - // Mark as selected - setting.settingEl.addClass("profile-selected"); - selectedProfile = profile; - - // Update folder path from profile defaults - const profileFolder = (profile.defaults?.folder as string) || this.defaultFolderPath || ""; - folderPath = profileFolder; - folderPathSpan.textContent = folderPath || "(root)"; - - // Log selection - console.log("Profile selected", { - key: profile.key, - label: profile.label, - noteType: profile.note_type, - stepCount: profile.steps?.length || 0, - defaultFolder: profileFolder, - }); + .setDesc(`Type: ${profile.note_type}${isPreferred ? " (empfohlen)" : ""}`); + + // Auto-select first preferred profile if none selected yet + if (isPreferred && !selectedProfile && !firstPreferredSelected) { + setting.settingEl.addClass("profile-selected"); + selectedProfile = profile; + firstPreferredSelected = true; + + // Update folder path from profile defaults + const profileFolder = (profile.defaults?.folder as string) || this.defaultFolderPath || ""; + folderPath = profileFolder; + folderPathSpan.textContent = folderPath || "(root)"; + } + + // Highlight preferred profiles + if (isPreferred) { + setting.settingEl.addClass("profile-preferred"); + } + + setting.addButton((button) => { + button.setButtonText("Select").onClick(() => { + // Clear previous selection + contentEl.querySelectorAll(".profile-selected").forEach((el) => { + if (el instanceof HTMLElement) { + el.removeClass("profile-selected"); + } + }); + // Mark as selected + setting.settingEl.addClass("profile-selected"); + selectedProfile = profile; + + // Update folder path from profile defaults + const profileFolder = (profile.defaults?.folder as string) || this.defaultFolderPath || ""; + folderPath = profileFolder; + folderPathSpan.textContent = folderPath || "(root)"; + + // Log selection + console.log("Profile selected", { + key: profile.key, + label: profile.label, + noteType: profile.note_type, + stepCount: profile.steps?.length || 0, + defaultFolder: profileFolder, + isPreferred, }); }); + }); } } @@ -150,37 +187,60 @@ export class ProfileSelectionModal extends Modal { contentEl.createEl("h3", { text: "Other" }); } - for (const profile of ungrouped) { + const sortedUngrouped = sortProfiles(ungrouped); + let firstPreferredSelectedUngrouped = false; + + for (const profile of sortedUngrouped) { + const isPreferred = this.preferredNoteTypes.includes(profile.note_type); const setting = new Setting(contentEl) .setName(profile.label) - .setDesc(`Type: ${profile.note_type}`) - .addButton((button) => { - button.setButtonText("Select").onClick(() => { - // Clear previous selection - contentEl.querySelectorAll(".profile-selected").forEach((el) => { - if (el instanceof HTMLElement) { - el.removeClass("profile-selected"); - } - }); - // Mark as selected - setting.settingEl.addClass("profile-selected"); - selectedProfile = profile; - - // Update folder path from profile defaults - const profileFolder = (profile.defaults?.folder as string) || this.defaultFolderPath || ""; - folderPath = profileFolder; - folderPathSpan.textContent = folderPath || "(root)"; - - // Log selection - console.log("Profile selected", { - key: profile.key, - label: profile.label, - noteType: profile.note_type, - stepCount: profile.steps?.length || 0, - defaultFolder: profileFolder, - }); + .setDesc(`Type: ${profile.note_type}${isPreferred ? " (empfohlen)" : ""}`); + + // Auto-select first preferred profile if none selected yet + if (isPreferred && !selectedProfile && !firstPreferredSelectedUngrouped) { + setting.settingEl.addClass("profile-selected"); + selectedProfile = profile; + firstPreferredSelectedUngrouped = true; + + // Update folder path from profile defaults + const profileFolder = (profile.defaults?.folder as string) || this.defaultFolderPath || ""; + folderPath = profileFolder; + folderPathSpan.textContent = folderPath || "(root)"; + } + + // Highlight preferred profiles + if (isPreferred) { + setting.settingEl.addClass("profile-preferred"); + } + + setting.addButton((button) => { + button.setButtonText("Select").onClick(() => { + // Clear previous selection + contentEl.querySelectorAll(".profile-selected").forEach((el) => { + if (el instanceof HTMLElement) { + el.removeClass("profile-selected"); + } + }); + // Mark as selected + setting.settingEl.addClass("profile-selected"); + selectedProfile = profile; + + // Update folder path from profile defaults + const profileFolder = (profile.defaults?.folder as string) || this.defaultFolderPath || ""; + folderPath = profileFolder; + folderPathSpan.textContent = folderPath || "(root)"; + + // Log selection + console.log("Profile selected", { + key: profile.key, + label: profile.label, + noteType: profile.note_type, + stepCount: profile.steps?.length || 0, + defaultFolder: profileFolder, + isPreferred, }); }); + }); } } diff --git a/src/unresolvedLink/unresolvedLinkHandler.ts b/src/unresolvedLink/unresolvedLinkHandler.ts index 63b0b52..aa28cf9 100644 --- a/src/unresolvedLink/unresolvedLinkHandler.ts +++ b/src/unresolvedLink/unresolvedLinkHandler.ts @@ -48,7 +48,8 @@ export async function startWizardAfterCreate( isUnresolvedClick: boolean, onWizardComplete: (result: any) => void, onWizardSave: (result: any) => void, - pluginInstance?: { ensureGraphSchemaLoaded?: () => Promise } + pluginInstance?: { ensureGraphSchemaLoaded?: () => Promise }, + initialPendingEdgeAssignments?: import("../interview/wizardState").PendingEdgeAssignment[] ): Promise { // Determine if wizard should start const shouldStartInterview = isUnresolvedClick @@ -109,7 +110,8 @@ export async function startWizardAfterCreate( onWizardComplete, onWizardSave, settings, // Pass settings for post-run edging - pluginInstance // Pass plugin instance for graph schema loading + pluginInstance, // Pass plugin instance for graph schema loading + initialPendingEdgeAssignments // Pass initial edge assignments ).open(); } catch (e) { const msg = e instanceof Error ? e.message : String(e); diff --git a/src/workbench/createSectionAction.ts b/src/workbench/createSectionAction.ts new file mode 100644 index 0000000..792fc81 --- /dev/null +++ b/src/workbench/createSectionAction.ts @@ -0,0 +1,462 @@ +/** + * Create section action for workbench missing slot todos. + */ + +import { Notice, TFile, Modal, Setting } from "obsidian"; +import type { App, Editor } from "obsidian"; +import type { MissingSlotTodo } from "./types"; +import type { Vocabulary } from "../vocab/Vocabulary"; +import type { EdgeVocabulary } from "../vocab/types"; +import type { MindnetSettings } from "../settings"; +import type { ChainTemplate, ChainRolesConfig, ChainTemplatesConfig } from "../dictionary/types"; +import type { TemplateMatch } from "../analysis/chainInspector"; +import { EntityPickerModal } from "../ui/EntityPickerModal"; +import { NoteIndex } from "../entityPicker/noteIndex"; + +/** Common section/node types for dropdown when slot has no allowed_node_types. */ +const FALLBACK_SECTION_TYPES = [ + "experience", + "insight", + "decision", + "learning", + "value", + "trigger", + "outcome", + "other", +]; + +export interface CreateSectionResult { + targetFilePath: string; + heading: string; + blockId: string | null; + sectionType: string | null; + sectionBody: string; + initialEdges: Array<{ + edgeType: string; + targetNote: string; + targetHeading: string | null; + }>; +} + +/** + * Create a new section in the chosen note. + */ +export async function createSectionInNote( + app: App, + editor: Editor | null, + activeFile: TFile | null, + todo: MissingSlotTodo, + vocabulary: Vocabulary, + edgeVocabulary: EdgeVocabulary | null, + settings: MindnetSettings, + matchTemplateName: string, + match: TemplateMatch, + chainTemplates: ChainTemplatesConfig | null, + chainRoles: ChainRolesConfig | null +): Promise { + const debugLogging = settings.debugLogging; + + if (!chainTemplates) { + new Notice("Chain templates not available"); + return; + } + + const template = chainTemplates.templates?.find((t) => t.name === matchTemplateName); + if (!template) { + new Notice("Template not found"); + return; + } + + const requiredEdges = getRequiredEdgesForSlot( + template, + todo.slotId, + match, + chainRoles, + vocabulary, + edgeVocabulary + ); + + // Chain notes: unique file paths from slot assignments + const chainNotePaths = getChainNotePaths(match); + const defaultSectionType = getDefaultSectionType(template, todo); + const sectionTypeOptions = getSectionTypeOptions(todo, template); + + const result = await showCreateSectionModal( + app, + todo, + requiredEdges, + chainNotePaths, + activeFile?.path ?? null, + defaultSectionType, + sectionTypeOptions, + vocabulary, + edgeVocabulary, + settings + ); + + if (!result) { + return; + } + + // Build section content + let sectionContent = `## ${result.heading}`; + if (result.blockId) { + sectionContent += ` ^${result.blockId}`; + } + sectionContent += "\n\n"; + + if (result.sectionType) { + sectionContent += `> [!section] ${result.sectionType}\n\n`; + } + + if (result.initialEdges.length > 0) { + const wrapperType = settings.mappingWrapperCalloutType || "abstract"; + const wrapperTitle = settings.mappingWrapperTitle || ""; + const wrapperFolded = settings.mappingWrapperFolded || false; + const foldMarker = wrapperFolded ? "-" : "+"; + + sectionContent += `> [!${wrapperType}]${foldMarker}${wrapperTitle ? ` ${wrapperTitle}` : ""}\n`; + + for (const edge of result.initialEdges) { + const targetLink = edge.targetHeading + ? `${edge.targetNote}#${edge.targetHeading}` + : edge.targetNote; + sectionContent += `>> [!edge] ${edge.edgeType}\n>> [[${targetLink}]]\n`; + } + + sectionContent += "\n"; + } + + if (result.sectionBody.trim()) { + sectionContent += result.sectionBody.trim() + "\n\n"; + } + + // Resolve target file + const targetFile = app.vault.getAbstractFileByPath(result.targetFilePath); + if (!targetFile || !(targetFile instanceof TFile)) { + new Notice("Zieldatei nicht gefunden"); + return; + } + + const content = await app.vault.read(targetFile); + const needsNewline = content.length > 0 && !content.endsWith("\n"); + const sectionToInsert = (needsNewline ? "\n" : "") + sectionContent; + + await app.vault.modify(targetFile, content + sectionToInsert); + + // Die Rückwärtskanten stehen in der neuen Sektion (initialEdges sind bereits Inverse-Typen und zeigen auf Quell-Notes). + // Kein separater Eintrag in anderen Notes nötig. + + new Notice(`Sektion "${result.heading}" in ${targetFile.basename} erstellt`); + + if (debugLogging) { + console.log("[createSectionInNote] Section created:", result.heading, "in", result.targetFilePath); + } +} + +function getChainNotePaths(match: TemplateMatch): string[] { + const paths = new Set(); + for (const a of Object.values(match.slotAssignments)) { + if (a?.file) { + paths.add(a.file); + } + } + return Array.from(paths).sort(); +} + +function getDefaultSectionType(template: ChainTemplate, todo: MissingSlotTodo): string { + const slot = template.slots.find((s) => (typeof s === "string" ? s === todo.slotId : s.id === todo.slotId)); + if (typeof slot !== "string" && slot?.allowed_node_types?.length) { + const first = slot.allowed_node_types[0]; + return first != null ? first : FALLBACK_SECTION_TYPES[0]!; + } + const slotId = todo.slotId.toLowerCase(); + if (FALLBACK_SECTION_TYPES.includes(slotId)) { + return slotId; + } + return todo.allowedNodeTypes?.[0] ?? FALLBACK_SECTION_TYPES[0]!; +} + +function getSectionTypeOptions(todo: MissingSlotTodo, template: ChainTemplate): string[] { + const slot = template.slots.find((s) => (typeof s === "string" ? s === todo.slotId : s.id === todo.slotId)); + if (typeof slot !== "string" && slot?.allowed_node_types?.length) { + return [...slot.allowed_node_types]; + } + if (todo.allowedNodeTypes?.length) { + return [...todo.allowedNodeTypes]; + } + return [...FALLBACK_SECTION_TYPES]; +} + +async function showCreateSectionModal( + app: App, + todo: MissingSlotTodo, + requiredEdges: Array<{ + edgeType: string; + targetNote: string; + targetHeading: string | null; + suggestedAlias?: string; + }>, + chainNotePaths: string[], + activeFilePath: string | null, + defaultSectionType: string, + sectionTypeOptions: string[], + vocabulary: Vocabulary, + edgeVocabulary: EdgeVocabulary | null, + settings: MindnetSettings +): Promise { + return new Promise((resolve) => { + const noteIndex = new NoteIndex(app); + + class CreateSectionModal extends Modal { + result: CreateSectionResult | null = null; + heading = ""; + blockId = ""; + sectionType = defaultSectionType; + sectionBody = ""; + targetFilePath: string = + (activeFilePath && chainNotePaths.includes(activeFilePath) + ? activeFilePath + : chainNotePaths[0] ?? activeFilePath ?? "") as string; + initialEdges: CreateSectionResult["initialEdges"] = []; + + constructor() { + super(app); + } + + onOpen(): void { + const { contentEl } = this; + contentEl.empty(); + contentEl.createEl("h2", { text: "Neue Sektion erstellen" }); + + // Target note dropdown: chain notes first, then "Other" + const chainNoteOptions = chainNotePaths.map((p) => ({ value: p, label: p.split("/").pop() ?? p })); + const hasChainNotes = chainNoteOptions.length > 0; + if (!this.targetFilePath && hasChainNotes && chainNoteOptions[0]) { + this.targetFilePath = chainNoteOptions[0].value; + } + new Setting(contentEl) + .setName("Ziel-Note") + .setDesc("Note, in der die Sektion angelegt wird (primär: Notes aus der Chain)") + .addDropdown((dropdown) => { + for (const { value, label } of chainNoteOptions) { + dropdown.addOption(value, label); + } + dropdown.addOption("__other__", "… Andere Note wählen"); + dropdown.setValue( + this.targetFilePath && (chainNotePaths.includes(this.targetFilePath) || !hasChainNotes) + ? this.targetFilePath + : "__other__" + ); + dropdown.onChange((value) => { + if (value === "__other__") { + this.chooseOtherNote(dropdown); + } else { + this.targetFilePath = value; + } + }); + }); + + // Heading + new Setting(contentEl) + .setName("Überschrift") + .setDesc("Überschrift der neuen Sektion") + .addText((text) => { + text.setValue("").onChange((value) => { + this.heading = value; + }); + text.inputEl.focus(); + }); + + // Section type dropdown + new Setting(contentEl) + .setName("Sektionstyp") + .setDesc("Typ der Sektion (aus Slot/Vorlage)") + .addDropdown((dropdown) => { + for (const opt of sectionTypeOptions) { + dropdown.addOption(opt, opt); + } + dropdown.setValue(this.sectionType); + dropdown.onChange((value) => { + this.sectionType = value; + }); + }); + + // Block ID (optional) + new Setting(contentEl) + .setName("Block-ID (optional)") + .setDesc("z. B. learning, insight-1") + .addText((text) => { + text.setValue("").onChange((value) => { + this.blockId = value.trim(); + }); + }); + + // Initial edges (read-only info, all applied) + if (requiredEdges.length > 0) { + contentEl.createEl("h3", { text: "Verbindungen (werden angelegt)" }); + const ul = contentEl.createEl("ul", { cls: "create-section-edges-list" }); + for (const edge of requiredEdges) { + const li = ul.createEl("li"); + li.textContent = `${edge.suggestedAlias || edge.edgeType} → ${edge.targetNote}${edge.targetHeading ? `#${edge.targetHeading}` : ""}`; + } + this.initialEdges = requiredEdges.map((e) => ({ + edgeType: e.suggestedAlias || e.edgeType, + targetNote: e.targetNote, + targetHeading: e.targetHeading, + })); + } + + // Section body text + const bodyDesc = contentEl.createEl("div", { cls: "setting-item-description" }); + bodyDesc.setText("Inhalt der Sektion (optional). Wird unter den Verbindungen eingefügt."); + const bodyContainer = contentEl.createDiv({ cls: "setting-item" }); + const control = bodyContainer.createDiv({ cls: "setting-item-control" }); + const textarea = control.createEl("textarea", { + attr: { placeholder: "Text für die neue Sektion…", rows: "5" }, + }); + textarea.style.width = "100%"; + textarea.oninput = () => { + this.sectionBody = textarea.value; + }; + + // Buttons + new Setting(contentEl).addButton((button) => { + button.setButtonText("Abbrechen").onClick(() => { + resolve(null); + this.close(); + }); + }).addButton((button) => { + button + .setButtonText("Sektion erstellen") + .setCta() + .onClick(() => { + if (!this.heading.trim()) { + new Notice("Bitte eine Überschrift eingeben"); + return; + } + if (!this.targetFilePath) { + new Notice("Bitte eine Ziel-Note wählen"); + return; + } + this.result = { + targetFilePath: this.targetFilePath, + heading: this.heading.trim(), + blockId: this.blockId || null, + sectionType: this.sectionType || null, + sectionBody: this.sectionBody, + initialEdges: this.initialEdges, + }; + resolve(this.result); + this.close(); + }); + }); + } + + private chooseOtherNote(dropdown: { setValue: (v: string) => void; selectEl: HTMLSelectElement }): void { + const picker = new EntityPickerModal(app, noteIndex, (result) => { + const path = result.path.endsWith(".md") ? result.path : `${result.path}.md`; + this.targetFilePath = path; + const opt = dropdown.selectEl.querySelector(`option[value="${path}"]`); + if (!opt) { + const optOther = dropdown.selectEl.querySelector('option[value="__other__"]'); + const newOpt = document.createElement("option"); + newOpt.value = path; + newOpt.textContent = result.basename; + dropdown.selectEl.insertBefore(newOpt, optOther ?? null); + } + dropdown.setValue(path); + picker.close(); + }); + picker.open(); + } + + onClose(): void { + this.contentEl.empty(); + } + } + + const modal = new CreateSectionModal(); + modal.open(); + }); +} + +function getRequiredEdgesForSlot( + template: ChainTemplate, + slotId: string, + match: TemplateMatch, + chainRoles: ChainRolesConfig | null, + vocabulary: Vocabulary, + edgeVocabulary: EdgeVocabulary | null +): Array<{ + edgeType: string; + targetNote: string; + targetHeading: string | null; + suggestedAlias?: string; +}> { + const requiredEdges: Array<{ + edgeType: string; + targetNote: string; + targetHeading: string | null; + suggestedAlias?: string; + }> = []; + + const normalizedLinks = template.links || []; + for (const link of normalizedLinks) { + const suggestedEdgeTypes: string[] = []; + if (chainRoles?.roles && link.allowed_edge_roles) { + for (const roleName of link.allowed_edge_roles) { + const role = chainRoles.roles[roleName]; + if (role?.edge_types) { + suggestedEdgeTypes.push(...role.edge_types); + } + } + } + const forwardEdgeType = suggestedEdgeTypes[0] || "related_to"; + + // Links, die ZU unserem Slot zeigen: In der neuen Sektion Rückwärtskante (Inverse) zur Quell-Note + if (link.to === slotId) { + const sourceAssignment = match.slotAssignments[link.from]; + if (!sourceAssignment) continue; + + const canonical = vocabulary.getCanonical(forwardEdgeType); + const inverseType = canonical ? vocabulary.getInverse(canonical) : null; + const edgeTypeForSection = inverseType ?? forwardEdgeType; + let suggestedAlias: string | undefined; + if (edgeVocabulary && edgeTypeForSection) { + const entry = edgeVocabulary.byCanonical.get(edgeTypeForSection); + if (entry?.aliases?.length) { + suggestedAlias = entry.aliases[0]; + } + } + requiredEdges.push({ + edgeType: edgeTypeForSection, + targetNote: sourceAssignment.file.replace(/\.md$/, ""), + targetHeading: sourceAssignment.heading ?? null, + suggestedAlias, + }); + continue; + } + + // Links, die VON unserem Slot ausgehen: In der neuen Sektion Vorwärtskante zur Ziel-Note + if (link.from === slotId) { + const targetAssignment = match.slotAssignments[link.to]; + if (!targetAssignment) continue; + let suggestedAlias: string | undefined; + if (edgeVocabulary) { + const entry = edgeVocabulary.byCanonical.get(forwardEdgeType); + if (entry?.aliases?.length) { + suggestedAlias = entry.aliases[0]; + } + } + requiredEdges.push({ + edgeType: forwardEdgeType, + targetNote: targetAssignment.file.replace(/\.md$/, ""), + targetHeading: targetAssignment.heading ?? null, + suggestedAlias, + }); + } + } + + return requiredEdges; +} diff --git a/src/workbench/interviewOrchestration.ts b/src/workbench/interviewOrchestration.ts index ef9b037..7cd3aa3 100644 --- a/src/workbench/interviewOrchestration.ts +++ b/src/workbench/interviewOrchestration.ts @@ -153,10 +153,7 @@ export async function createNoteViaInterview( }; }); - // Start wizard with payload - // Note: The wizard system doesn't directly accept pendingEdgeAssignments in startWizardAfterCreate - // We'll need to inject them into the wizard state after creation - // For now, we'll rely on soft validation after wizard completion + // Start wizard with initial pending edge assignments await startWizardAfterCreate( app, settings, @@ -186,12 +183,9 @@ export async function createNoteViaInterview( ); new Notice("Wizard saved"); }, - pluginInstance + pluginInstance, + pendingEdgeAssignments // Pass initial edge assignments to wizard ); - - // TODO: Inject pendingEdgeAssignments into wizard state - // This would require modifying InterviewWizardModal to accept initial pendingEdgeAssignments - // For MVP, we rely on soft validation and user can manually add edges } /** @@ -276,7 +270,8 @@ async function selectProfile( app: App, interviewConfig: InterviewConfig, allowedProfiles: InterviewProfile[], - settings: MindnetSettings + settings: MindnetSettings, + preferredNoteTypes: string[] = [] ): Promise<{ profile: InterviewProfile; title: string; folderPath: string } | null> { return new Promise((resolve) => { // Filter config to only allowed profiles @@ -285,7 +280,7 @@ async function selectProfile( profiles: allowedProfiles, }; - // Use ProfileSelectionModal with filtered profiles + // Use ProfileSelectionModal with filtered profiles and preferred types const modal = new ProfileSelectionModal( app, filteredConfig, @@ -297,11 +292,10 @@ async function selectProfile( }); }, "", // Default title (user will set) - settings.defaultNotesFolder || "" + settings.defaultNotesFolder || "", + preferredNoteTypes // Pass preferred note types ); - // Filter profiles in modal (we'll need to modify ProfileSelectionModal or filter here) - // For now, we'll pass all profiles and let user choose modal.onClose = () => { resolve(null); }; diff --git a/src/workbench/todoGenerator.ts b/src/workbench/todoGenerator.ts index f5d1c5d..fef5d7e 100644 --- a/src/workbench/todoGenerator.ts +++ b/src/workbench/todoGenerator.ts @@ -58,7 +58,7 @@ export async function generateTodos( priority: "high", slotId, allowedNodeTypes, - actions: ["link_existing", "create_note_via_interview"], + actions: ["link_existing", "create_note_via_interview", "create_section_in_note"], } as MissingSlotTodo); } } diff --git a/src/workbench/types.ts b/src/workbench/types.ts index c81e2b8..6d69532 100644 --- a/src/workbench/types.ts +++ b/src/workbench/types.ts @@ -54,7 +54,7 @@ export interface MissingSlotTodo extends WorkbenchTodo { type: "missing_slot"; slotId: string; allowedNodeTypes: string[]; - actions: Array<"link_existing" | "create_note_via_interview">; + actions: Array<"link_existing" | "create_note_via_interview" | "create_section_in_note">; } /** diff --git a/styles.css b/styles.css index 3ae0bb3..5e3d7c5 100644 --- a/styles.css +++ b/styles.css @@ -18,6 +18,16 @@ If your plugin does not need CSS, delete this file. font-weight: 600; } +.profile-preferred { + background-color: var(--background-modifier-hover); + border-left: 2px solid var(--text-accent); + padding-left: calc(var(--size-4-2) - 2px); +} + +.profile-preferred .setting-item-name { + font-weight: 500; +} + /* Interview wizard styles */ .interview-prompt { color: var(--text-muted);