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.
This commit is contained in:
parent
7627a05af4
commit
0a346d3886
|
|
@ -643,6 +643,7 @@ export class ChainWorkbenchModal extends Modal {
|
||||||
const labels: Record<string, string> = {
|
const labels: Record<string, string> = {
|
||||||
link_existing: "Link Existing",
|
link_existing: "Link Existing",
|
||||||
create_note_via_interview: "Create Note",
|
create_note_via_interview: "Create Note",
|
||||||
|
create_section_in_note: "Create Section",
|
||||||
insert_edge_forward: "Insert Edge",
|
insert_edge_forward: "Insert Edge",
|
||||||
insert_edge_inverse: "Insert Inverse",
|
insert_edge_inverse: "Insert Inverse",
|
||||||
choose_target_anchor: "Choose Anchor",
|
choose_target_anchor: "Choose Anchor",
|
||||||
|
|
@ -867,6 +868,45 @@ export class ChainWorkbenchModal extends Modal {
|
||||||
}
|
}
|
||||||
break;
|
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:
|
default:
|
||||||
new Notice(`Action "${action}" not yet implemented`);
|
new Notice(`Action "${action}" not yet implemented`);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -93,7 +93,8 @@ export class InterviewWizardModal extends Modal {
|
||||||
onSubmit: (result: WizardResult) => void,
|
onSubmit: (result: WizardResult) => void,
|
||||||
onSaveAndExit: (result: WizardResult) => void,
|
onSaveAndExit: (result: WizardResult) => void,
|
||||||
settings?: MindnetSettings,
|
settings?: MindnetSettings,
|
||||||
plugin?: { ensureGraphSchemaLoaded?: () => Promise<import("../mapping/graphSchema").GraphSchema | null> }
|
plugin?: { ensureGraphSchemaLoaded?: () => Promise<import("../mapping/graphSchema").GraphSchema | null> },
|
||||||
|
initialPendingEdgeAssignments?: PendingEdgeAssignment[]
|
||||||
) {
|
) {
|
||||||
super(app);
|
super(app);
|
||||||
|
|
||||||
|
|
@ -117,6 +118,7 @@ export class InterviewWizardModal extends Modal {
|
||||||
edgingMode: profile.edging?.mode || "none",
|
edgingMode: profile.edging?.mode || "none",
|
||||||
edging: profile.edging, // Full edging object for debugging
|
edging: profile.edging, // Full edging object for debugging
|
||||||
profileKeys: Object.keys(profile), // All keys in profile
|
profileKeys: Object.keys(profile), // All keys in profile
|
||||||
|
initialPendingEdgeAssignments: initialPendingEdgeAssignments?.length || 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Validate steps - only throw if profile.steps is actually empty
|
// Validate steps - only throw if profile.steps is actually empty
|
||||||
|
|
@ -130,6 +132,12 @@ export class InterviewWizardModal extends Modal {
|
||||||
|
|
||||||
this.state = createWizardState(profile);
|
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
|
// Initialize note index for entity picker
|
||||||
this.noteIndex = new NoteIndex(this.app);
|
this.noteIndex = new NoteIndex(this.app);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,19 +16,22 @@ export class ProfileSelectionModal extends Modal {
|
||||||
initialTitle: string;
|
initialTitle: string;
|
||||||
defaultFolderPath: string;
|
defaultFolderPath: string;
|
||||||
private noteIndex: NoteIndex;
|
private noteIndex: NoteIndex;
|
||||||
|
preferredNoteTypes: string[] = []; // Note types to mark as preferred
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
app: App,
|
app: App,
|
||||||
config: InterviewConfig,
|
config: InterviewConfig,
|
||||||
onSubmit: (result: ProfileSelectionResult) => void,
|
onSubmit: (result: ProfileSelectionResult) => void,
|
||||||
initialTitle: string = "",
|
initialTitle: string = "",
|
||||||
defaultFolderPath: string = ""
|
defaultFolderPath: string = "",
|
||||||
|
preferredNoteTypes: string[] = []
|
||||||
) {
|
) {
|
||||||
super(app);
|
super(app);
|
||||||
this.config = config;
|
this.config = config;
|
||||||
this.onSubmit = onSubmit;
|
this.onSubmit = onSubmit;
|
||||||
this.initialTitle = initialTitle;
|
this.initialTitle = initialTitle;
|
||||||
this.defaultFolderPath = defaultFolderPath;
|
this.defaultFolderPath = defaultFolderPath;
|
||||||
|
this.preferredNoteTypes = preferredNoteTypes;
|
||||||
this.noteIndex = new NoteIndex(app);
|
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)
|
// Profile selection (grouped)
|
||||||
for (const [groupName, profiles] of grouped.entries()) {
|
for (const [groupName, profiles] of grouped.entries()) {
|
||||||
contentEl.createEl("h3", { text: groupName });
|
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)
|
const setting = new Setting(contentEl)
|
||||||
.setName(profile.label)
|
.setName(profile.label)
|
||||||
.setDesc(`Type: ${profile.note_type}`)
|
.setDesc(`Type: ${profile.note_type}${isPreferred ? " (empfohlen)" : ""}`);
|
||||||
.addButton((button) => {
|
|
||||||
button.setButtonText("Select").onClick(() => {
|
// Auto-select first preferred profile if none selected yet
|
||||||
// Clear previous selection
|
if (isPreferred && !selectedProfile && !firstPreferredSelected) {
|
||||||
contentEl.querySelectorAll(".profile-selected").forEach((el) => {
|
setting.settingEl.addClass("profile-selected");
|
||||||
if (el instanceof HTMLElement) {
|
selectedProfile = profile;
|
||||||
el.removeClass("profile-selected");
|
firstPreferredSelected = true;
|
||||||
}
|
|
||||||
});
|
// Update folder path from profile defaults
|
||||||
// Mark as selected
|
const profileFolder = (profile.defaults?.folder as string) || this.defaultFolderPath || "";
|
||||||
setting.settingEl.addClass("profile-selected");
|
folderPath = profileFolder;
|
||||||
selectedProfile = profile;
|
folderPathSpan.textContent = folderPath || "(root)";
|
||||||
|
}
|
||||||
// Update folder path from profile defaults
|
|
||||||
const profileFolder = (profile.defaults?.folder as string) || this.defaultFolderPath || "";
|
// Highlight preferred profiles
|
||||||
folderPath = profileFolder;
|
if (isPreferred) {
|
||||||
folderPathSpan.textContent = folderPath || "(root)";
|
setting.settingEl.addClass("profile-preferred");
|
||||||
|
}
|
||||||
// Log selection
|
|
||||||
console.log("Profile selected", {
|
setting.addButton((button) => {
|
||||||
key: profile.key,
|
button.setButtonText("Select").onClick(() => {
|
||||||
label: profile.label,
|
// Clear previous selection
|
||||||
noteType: profile.note_type,
|
contentEl.querySelectorAll(".profile-selected").forEach((el) => {
|
||||||
stepCount: profile.steps?.length || 0,
|
if (el instanceof HTMLElement) {
|
||||||
defaultFolder: profileFolder,
|
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" });
|
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)
|
const setting = new Setting(contentEl)
|
||||||
.setName(profile.label)
|
.setName(profile.label)
|
||||||
.setDesc(`Type: ${profile.note_type}`)
|
.setDesc(`Type: ${profile.note_type}${isPreferred ? " (empfohlen)" : ""}`);
|
||||||
.addButton((button) => {
|
|
||||||
button.setButtonText("Select").onClick(() => {
|
// Auto-select first preferred profile if none selected yet
|
||||||
// Clear previous selection
|
if (isPreferred && !selectedProfile && !firstPreferredSelectedUngrouped) {
|
||||||
contentEl.querySelectorAll(".profile-selected").forEach((el) => {
|
setting.settingEl.addClass("profile-selected");
|
||||||
if (el instanceof HTMLElement) {
|
selectedProfile = profile;
|
||||||
el.removeClass("profile-selected");
|
firstPreferredSelectedUngrouped = true;
|
||||||
}
|
|
||||||
});
|
// Update folder path from profile defaults
|
||||||
// Mark as selected
|
const profileFolder = (profile.defaults?.folder as string) || this.defaultFolderPath || "";
|
||||||
setting.settingEl.addClass("profile-selected");
|
folderPath = profileFolder;
|
||||||
selectedProfile = profile;
|
folderPathSpan.textContent = folderPath || "(root)";
|
||||||
|
}
|
||||||
// Update folder path from profile defaults
|
|
||||||
const profileFolder = (profile.defaults?.folder as string) || this.defaultFolderPath || "";
|
// Highlight preferred profiles
|
||||||
folderPath = profileFolder;
|
if (isPreferred) {
|
||||||
folderPathSpan.textContent = folderPath || "(root)";
|
setting.settingEl.addClass("profile-preferred");
|
||||||
|
}
|
||||||
// Log selection
|
|
||||||
console.log("Profile selected", {
|
setting.addButton((button) => {
|
||||||
key: profile.key,
|
button.setButtonText("Select").onClick(() => {
|
||||||
label: profile.label,
|
// Clear previous selection
|
||||||
noteType: profile.note_type,
|
contentEl.querySelectorAll(".profile-selected").forEach((el) => {
|
||||||
stepCount: profile.steps?.length || 0,
|
if (el instanceof HTMLElement) {
|
||||||
defaultFolder: profileFolder,
|
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,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,8 @@ export async function startWizardAfterCreate(
|
||||||
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> }
|
pluginInstance?: { ensureGraphSchemaLoaded?: () => Promise<import("../mapping/graphSchema").GraphSchema | null> },
|
||||||
|
initialPendingEdgeAssignments?: import("../interview/wizardState").PendingEdgeAssignment[]
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// Determine if wizard should start
|
// Determine if wizard should start
|
||||||
const shouldStartInterview = isUnresolvedClick
|
const shouldStartInterview = isUnresolvedClick
|
||||||
|
|
@ -109,7 +110,8 @@ export async function startWizardAfterCreate(
|
||||||
onWizardComplete,
|
onWizardComplete,
|
||||||
onWizardSave,
|
onWizardSave,
|
||||||
settings, // Pass settings for post-run edging
|
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();
|
).open();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const msg = e instanceof Error ? e.message : String(e);
|
const msg = e instanceof Error ? e.message : String(e);
|
||||||
|
|
|
||||||
462
src/workbench/createSectionAction.ts
Normal file
462
src/workbench/createSectionAction.ts
Normal file
|
|
@ -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<void> {
|
||||||
|
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<string>();
|
||||||
|
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<CreateSectionResult | null> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
@ -153,10 +153,7 @@ export async function createNoteViaInterview(
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
// Start wizard with payload
|
// Start wizard with initial pending edge assignments
|
||||||
// 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
|
|
||||||
await startWizardAfterCreate(
|
await startWizardAfterCreate(
|
||||||
app,
|
app,
|
||||||
settings,
|
settings,
|
||||||
|
|
@ -186,12 +183,9 @@ export async function createNoteViaInterview(
|
||||||
);
|
);
|
||||||
new Notice("Wizard saved");
|
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,
|
app: App,
|
||||||
interviewConfig: InterviewConfig,
|
interviewConfig: InterviewConfig,
|
||||||
allowedProfiles: InterviewProfile[],
|
allowedProfiles: InterviewProfile[],
|
||||||
settings: MindnetSettings
|
settings: MindnetSettings,
|
||||||
|
preferredNoteTypes: string[] = []
|
||||||
): Promise<{ profile: InterviewProfile; title: string; folderPath: string } | null> {
|
): Promise<{ profile: InterviewProfile; title: string; folderPath: string } | null> {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
// Filter config to only allowed profiles
|
// Filter config to only allowed profiles
|
||||||
|
|
@ -285,7 +280,7 @@ async function selectProfile(
|
||||||
profiles: allowedProfiles,
|
profiles: allowedProfiles,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Use ProfileSelectionModal with filtered profiles
|
// Use ProfileSelectionModal with filtered profiles and preferred types
|
||||||
const modal = new ProfileSelectionModal(
|
const modal = new ProfileSelectionModal(
|
||||||
app,
|
app,
|
||||||
filteredConfig,
|
filteredConfig,
|
||||||
|
|
@ -297,11 +292,10 @@ async function selectProfile(
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
"", // Default title (user will set)
|
"", // 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 = () => {
|
modal.onClose = () => {
|
||||||
resolve(null);
|
resolve(null);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,7 @@ export async function generateTodos(
|
||||||
priority: "high",
|
priority: "high",
|
||||||
slotId,
|
slotId,
|
||||||
allowedNodeTypes,
|
allowedNodeTypes,
|
||||||
actions: ["link_existing", "create_note_via_interview"],
|
actions: ["link_existing", "create_note_via_interview", "create_section_in_note"],
|
||||||
} as MissingSlotTodo);
|
} as MissingSlotTodo);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@ export interface MissingSlotTodo extends WorkbenchTodo {
|
||||||
type: "missing_slot";
|
type: "missing_slot";
|
||||||
slotId: string;
|
slotId: string;
|
||||||
allowedNodeTypes: string[];
|
allowedNodeTypes: string[];
|
||||||
actions: Array<"link_existing" | "create_note_via_interview">;
|
actions: Array<"link_existing" | "create_note_via_interview" | "create_section_in_note">;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
10
styles.css
10
styles.css
|
|
@ -18,6 +18,16 @@ If your plugin does not need CSS, delete this file.
|
||||||
font-weight: 600;
|
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 wizard styles */
|
||||||
.interview-prompt {
|
.interview-prompt {
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user