2026-01-16 12:27:44 +01:00
|
|
|
|
import {
|
|
|
|
|
|
App,
|
2026-01-16 14:11:04 +01:00
|
|
|
|
Component,
|
|
|
|
|
|
MarkdownRenderer,
|
2026-01-16 12:27:44 +01:00
|
|
|
|
Modal,
|
|
|
|
|
|
Notice,
|
|
|
|
|
|
Setting,
|
|
|
|
|
|
TFile,
|
|
|
|
|
|
TextAreaComponent,
|
|
|
|
|
|
TextComponent,
|
|
|
|
|
|
} from "obsidian";
|
2026-01-16 20:53:02 +01:00
|
|
|
|
import type { InterviewProfile, InterviewStep, LoopStep } from "../interview/types";
|
2026-01-16 12:27:44 +01:00
|
|
|
|
import {
|
|
|
|
|
|
type WizardState,
|
|
|
|
|
|
createWizardState,
|
|
|
|
|
|
getCurrentStep,
|
|
|
|
|
|
getNextStepIndex,
|
|
|
|
|
|
getPreviousStepIndex,
|
|
|
|
|
|
canGoNext,
|
|
|
|
|
|
canGoBack,
|
|
|
|
|
|
type Patch,
|
|
|
|
|
|
flattenSteps,
|
|
|
|
|
|
} from "../interview/wizardState";
|
|
|
|
|
|
import { extractFrontmatterId } from "../parser/parseFrontmatter";
|
2026-01-16 14:11:04 +01:00
|
|
|
|
import {
|
|
|
|
|
|
createMarkdownToolbar,
|
|
|
|
|
|
} from "./markdownToolbar";
|
2026-01-17 22:15:11 +01:00
|
|
|
|
import { detectEdgeSelectorContext, changeEdgeTypeForLinks } from "../mapping/edgeTypeSelector";
|
2026-01-27 11:18:56 +01:00
|
|
|
|
import { renderProfileToMarkdown, type RenderAnswers, type RenderOptions } from "../interview/renderer";
|
|
|
|
|
|
import type { GraphSchema } from "../mapping/graphSchema";
|
|
|
|
|
|
import { Vocabulary } from "../vocab/Vocabulary";
|
|
|
|
|
|
import type { SectionInfo } from "../interview/wizardState";
|
|
|
|
|
|
import { slugify } from "../interview/slugify";
|
2026-01-17 06:28:36 +01:00
|
|
|
|
import { NoteIndex } from "../entityPicker/noteIndex";
|
|
|
|
|
|
import { EntityPickerModal, type EntityPickerResult } from "./EntityPickerModal";
|
|
|
|
|
|
import { insertWikilinkIntoTextarea } from "../entityPicker/wikilink";
|
2026-01-17 10:31:44 +01:00
|
|
|
|
import { buildSemanticMappings, type BuildResult } from "../mapping/semanticMappingBuilder";
|
|
|
|
|
|
import type { MindnetSettings } from "../settings";
|
2026-01-17 11:54:14 +01:00
|
|
|
|
import { InlineEdgeTypeModal, type InlineEdgeTypeResult } from "./InlineEdgeTypeModal";
|
2026-01-30 12:37:06 +01:00
|
|
|
|
import { SectionEdgesOverviewModal, type SectionEdgesOverviewResult } from "./SectionEdgesOverviewModal";
|
2026-01-17 11:54:14 +01:00
|
|
|
|
import { getSectionKeyForWizardContext } from "../interview/sectionKeyResolver";
|
|
|
|
|
|
import type { PendingEdgeAssignment } from "../interview/wizardState";
|
|
|
|
|
|
import { VocabularyLoader } from "../vocab/VocabularyLoader";
|
|
|
|
|
|
import { parseEdgeVocabulary } from "../vocab/parseEdgeVocabulary";
|
|
|
|
|
|
import type { EdgeVocabulary } from "../vocab/types";
|
2026-01-16 19:25:55 +01:00
|
|
|
|
import {
|
|
|
|
|
|
type LoopRuntimeState,
|
|
|
|
|
|
createLoopState,
|
|
|
|
|
|
setDraftField,
|
|
|
|
|
|
isDraftDirty,
|
|
|
|
|
|
commitDraft,
|
|
|
|
|
|
startEdit,
|
|
|
|
|
|
deleteItem,
|
|
|
|
|
|
moveItemUp,
|
|
|
|
|
|
moveItemDown,
|
|
|
|
|
|
clearDraft,
|
2026-01-16 20:10:08 +01:00
|
|
|
|
setActiveItemStepIndex,
|
|
|
|
|
|
itemNextStep,
|
|
|
|
|
|
itemPrevStep,
|
|
|
|
|
|
resetItemWizard,
|
2026-01-16 19:25:55 +01:00
|
|
|
|
} from "../interview/loopState";
|
2026-01-16 12:27:44 +01:00
|
|
|
|
|
|
|
|
|
|
export interface WizardResult {
|
|
|
|
|
|
applied: boolean;
|
|
|
|
|
|
patches: Patch[];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export class InterviewWizardModal extends Modal {
|
|
|
|
|
|
state: WizardState;
|
|
|
|
|
|
file: TFile;
|
|
|
|
|
|
fileContent: string;
|
|
|
|
|
|
onSubmit: (result: WizardResult) => void;
|
|
|
|
|
|
onSaveAndExit: (result: WizardResult) => void;
|
2026-01-17 10:31:44 +01:00
|
|
|
|
profile: InterviewProfile;
|
2026-01-16 12:27:44 +01:00
|
|
|
|
profileKey: string;
|
2026-01-17 10:31:44 +01:00
|
|
|
|
settings?: MindnetSettings; // Optional settings for post-run edging
|
|
|
|
|
|
plugin?: { ensureGraphSchemaLoaded?: () => Promise<import("../mapping/graphSchema").GraphSchema | null> }; // Optional plugin instance
|
2026-01-16 12:27:44 +01:00
|
|
|
|
// Store current input values to save on navigation
|
|
|
|
|
|
private currentInputValues: Map<string, string> = new Map();
|
2026-01-16 14:11:04 +01:00
|
|
|
|
// Store preview mode state per step key
|
|
|
|
|
|
private previewMode: Map<string, boolean> = new Map();
|
2026-01-17 06:28:36 +01:00
|
|
|
|
// Note index for entity picker (shared instance)
|
|
|
|
|
|
private noteIndex: NoteIndex | null = null;
|
2026-01-17 11:54:14 +01:00
|
|
|
|
// Vocabulary and schema for inline micro suggester
|
|
|
|
|
|
private vocabulary: EdgeVocabulary | null = null;
|
2026-01-16 12:27:44 +01:00
|
|
|
|
|
|
|
|
|
|
constructor(
|
|
|
|
|
|
app: App,
|
|
|
|
|
|
profile: InterviewProfile,
|
|
|
|
|
|
file: TFile,
|
|
|
|
|
|
fileContent: string,
|
|
|
|
|
|
onSubmit: (result: WizardResult) => void,
|
2026-01-17 10:31:44 +01:00
|
|
|
|
onSaveAndExit: (result: WizardResult) => void,
|
|
|
|
|
|
settings?: MindnetSettings,
|
|
|
|
|
|
plugin?: { ensureGraphSchemaLoaded?: () => Promise<import("../mapping/graphSchema").GraphSchema | null> }
|
2026-01-16 12:27:44 +01:00
|
|
|
|
) {
|
|
|
|
|
|
super(app);
|
|
|
|
|
|
|
|
|
|
|
|
// Validate profile
|
|
|
|
|
|
if (!profile) {
|
|
|
|
|
|
new Notice(`Interview profile not found`);
|
|
|
|
|
|
throw new Error("Profile is required");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-17 10:31:44 +01:00
|
|
|
|
this.profile = profile;
|
2026-01-16 12:27:44 +01:00
|
|
|
|
this.profileKey = profile.key;
|
2026-01-17 10:31:44 +01:00
|
|
|
|
this.settings = settings;
|
|
|
|
|
|
this.plugin = plugin;
|
2026-01-16 12:27:44 +01:00
|
|
|
|
|
|
|
|
|
|
// Log profile info
|
|
|
|
|
|
const stepKinds = profile.steps?.map(s => s.type) || [];
|
|
|
|
|
|
console.log("Wizard profile", {
|
|
|
|
|
|
key: profile.key,
|
|
|
|
|
|
stepCount: profile.steps?.length,
|
|
|
|
|
|
kinds: stepKinds,
|
2026-01-17 10:31:44 +01:00
|
|
|
|
edgingMode: profile.edging?.mode || "none",
|
|
|
|
|
|
edging: profile.edging, // Full edging object for debugging
|
|
|
|
|
|
profileKeys: Object.keys(profile), // All keys in profile
|
2026-01-16 12:27:44 +01:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Validate steps - only throw if profile.steps is actually empty
|
|
|
|
|
|
if (!profile.steps || profile.steps.length === 0) {
|
|
|
|
|
|
new Notice(`Interview has no steps for profile: ${profile.key}`);
|
|
|
|
|
|
throw new Error("Profile has no steps");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Check flattened steps after creation (will be logged in createWizardState)
|
|
|
|
|
|
// If flattened is empty but profile.steps is not, that's a flattenSteps bug
|
|
|
|
|
|
|
|
|
|
|
|
this.state = createWizardState(profile);
|
|
|
|
|
|
|
2026-01-17 06:28:36 +01:00
|
|
|
|
// Initialize note index for entity picker
|
|
|
|
|
|
this.noteIndex = new NoteIndex(this.app);
|
|
|
|
|
|
|
2026-01-16 12:27:44 +01:00
|
|
|
|
// Validate flattened steps after creation
|
|
|
|
|
|
const flat = flattenSteps(profile.steps);
|
|
|
|
|
|
if (flat.length === 0 && profile.steps.length > 0) {
|
|
|
|
|
|
console.error("Flatten produced 0 steps but profile has steps", {
|
|
|
|
|
|
profileKey: profile.key,
|
|
|
|
|
|
originalStepCount: profile.steps.length,
|
|
|
|
|
|
originalKinds: stepKinds,
|
|
|
|
|
|
});
|
|
|
|
|
|
new Notice(`Flatten produced 0 steps (check flattenSteps) for profile: ${profile.key}`);
|
|
|
|
|
|
throw new Error("FlattenSteps produced empty result");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
this.file = file;
|
|
|
|
|
|
this.fileContent = fileContent;
|
|
|
|
|
|
this.onSubmit = onSubmit;
|
|
|
|
|
|
this.onSaveAndExit = onSaveAndExit;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
onOpen(): void {
|
|
|
|
|
|
const fileName = this.file.basename || this.file.name.replace(/\.md$/, "");
|
|
|
|
|
|
|
2026-01-16 14:11:04 +01:00
|
|
|
|
// Add CSS class for styling
|
|
|
|
|
|
this.modalEl.addClass("mindnet-wizard-modal");
|
|
|
|
|
|
|
2026-01-16 12:27:44 +01:00
|
|
|
|
console.log("=== WIZARD START ===", {
|
|
|
|
|
|
profileKey: this.profileKey,
|
|
|
|
|
|
file: this.file.path,
|
|
|
|
|
|
fileName: fileName,
|
|
|
|
|
|
stepCount: this.state.profile.steps?.length || 0,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
this.renderStep();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
renderStep(): void {
|
|
|
|
|
|
const { contentEl } = this;
|
|
|
|
|
|
contentEl.empty();
|
2026-01-16 14:11:04 +01:00
|
|
|
|
|
|
|
|
|
|
// Apply flex layout structure
|
|
|
|
|
|
contentEl.addClass("modal-content");
|
|
|
|
|
|
|
2026-01-16 12:27:44 +01:00
|
|
|
|
const step = getCurrentStep(this.state);
|
|
|
|
|
|
|
|
|
|
|
|
console.log("Render step", {
|
|
|
|
|
|
stepIndex: this.state.currentStepIndex,
|
|
|
|
|
|
stepType: step?.type || "null",
|
|
|
|
|
|
stepKey: step?.key || "null",
|
|
|
|
|
|
stepLabel: step?.label || "null",
|
|
|
|
|
|
totalSteps: flattenSteps(this.state.profile.steps).length,
|
2026-01-27 11:18:56 +01:00
|
|
|
|
sectionSequenceLength: this.state.sectionSequence.length,
|
|
|
|
|
|
generatedBlockIdsCount: this.state.generatedBlockIds.size,
|
2026-01-16 12:27:44 +01:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-01-27 11:18:56 +01:00
|
|
|
|
// WP-26: Track Section-Info während des Wizard-Durchlaufs
|
|
|
|
|
|
if (step) {
|
2026-01-30 12:37:06 +01:00
|
|
|
|
// Debug: Zeige alle Step-Felder
|
|
|
|
|
|
if (step.type === "capture_text" || step.type === "capture_text_line") {
|
|
|
|
|
|
const isCaptureText = step.type === "capture_text";
|
|
|
|
|
|
const captureTextStep = isCaptureText ? step as import("../interview/types").CaptureTextStep : null;
|
|
|
|
|
|
const captureTextLineStep = !isCaptureText ? step as import("../interview/types").CaptureTextLineStep : null;
|
|
|
|
|
|
console.log(`[WP-26] Step-Details für ${step.key}:`, {
|
|
|
|
|
|
type: step.type,
|
|
|
|
|
|
section: isCaptureText ? captureTextStep?.section : undefined,
|
|
|
|
|
|
section_type: isCaptureText ? captureTextStep?.section_type : captureTextLineStep?.section_type,
|
|
|
|
|
|
block_id: isCaptureText ? captureTextStep?.block_id : captureTextLineStep?.block_id,
|
|
|
|
|
|
generate_block_id: isCaptureText ? captureTextStep?.generate_block_id : captureTextLineStep?.generate_block_id,
|
|
|
|
|
|
references: isCaptureText ? captureTextStep?.references : captureTextLineStep?.references,
|
|
|
|
|
|
heading_level: !isCaptureText ? captureTextLineStep?.heading_level : undefined,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2026-01-27 11:18:56 +01:00
|
|
|
|
console.log(`[WP-26] trackSectionInfo wird aufgerufen für Step ${step.key}, type: ${step.type}`);
|
|
|
|
|
|
this.trackSectionInfo(step);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.log(`[WP-26] Kein Step gefunden, trackSectionInfo wird nicht aufgerufen`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-16 12:27:44 +01:00
|
|
|
|
if (!step) {
|
|
|
|
|
|
// Check if we're at the end legitimately or if there's an error
|
|
|
|
|
|
const steps = flattenSteps(this.state.profile.steps);
|
|
|
|
|
|
if (steps.length === 0) {
|
|
|
|
|
|
new Notice(`Interview has no steps for profile: ${this.profileKey}`);
|
|
|
|
|
|
this.close();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (this.state.currentStepIndex >= steps.length) {
|
|
|
|
|
|
contentEl.createEl("p", { text: "Interview completed" });
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
// Unexpected: step is null but we should have one
|
|
|
|
|
|
console.error("Unexpected: step is null", {
|
|
|
|
|
|
currentStepIndex: this.state.currentStepIndex,
|
|
|
|
|
|
stepCount: steps.length,
|
|
|
|
|
|
profileKey: this.profileKey,
|
|
|
|
|
|
});
|
|
|
|
|
|
new Notice(`Error: Could not load step ${this.state.currentStepIndex + 1}`);
|
|
|
|
|
|
this.close();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-16 14:11:04 +01:00
|
|
|
|
// Create body container for scrollable content
|
|
|
|
|
|
const bodyEl = contentEl.createEl("div", {
|
|
|
|
|
|
cls: "modal-content-body",
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-01-16 12:27:44 +01:00
|
|
|
|
// Check if ID exists
|
|
|
|
|
|
const hasId = this.checkIdExists();
|
|
|
|
|
|
|
|
|
|
|
|
if (!hasId) {
|
2026-01-16 14:11:04 +01:00
|
|
|
|
const warningEl = bodyEl.createEl("div", {
|
2026-01-16 12:27:44 +01:00
|
|
|
|
cls: "interview-warning",
|
|
|
|
|
|
});
|
|
|
|
|
|
warningEl.createEl("p", {
|
|
|
|
|
|
text: "⚠️ Note missing frontmatter ID",
|
|
|
|
|
|
});
|
|
|
|
|
|
new Setting(warningEl).addButton((button) => {
|
|
|
|
|
|
button.setButtonText("Generate ID").onClick(() => {
|
|
|
|
|
|
this.generateId();
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-16 14:11:04 +01:00
|
|
|
|
// Create step content container
|
|
|
|
|
|
const stepContentEl = bodyEl.createEl("div", {
|
|
|
|
|
|
cls: "step-content",
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-01-16 12:27:44 +01:00
|
|
|
|
// Render step based on type
|
|
|
|
|
|
switch (step.type) {
|
|
|
|
|
|
case "instruction":
|
2026-01-16 14:11:04 +01:00
|
|
|
|
this.renderInstructionStep(step, stepContentEl);
|
2026-01-16 12:27:44 +01:00
|
|
|
|
break;
|
|
|
|
|
|
case "capture_text":
|
2026-01-16 14:11:04 +01:00
|
|
|
|
this.renderCaptureTextStep(step, stepContentEl);
|
2026-01-16 12:27:44 +01:00
|
|
|
|
break;
|
2026-01-16 18:26:01 +01:00
|
|
|
|
case "capture_text_line":
|
|
|
|
|
|
this.renderCaptureTextLineStep(step, stepContentEl);
|
|
|
|
|
|
break;
|
2026-01-16 12:27:44 +01:00
|
|
|
|
case "capture_frontmatter":
|
2026-01-16 14:11:04 +01:00
|
|
|
|
this.renderCaptureFrontmatterStep(step, stepContentEl);
|
2026-01-16 12:27:44 +01:00
|
|
|
|
break;
|
|
|
|
|
|
case "loop":
|
2026-01-16 14:11:04 +01:00
|
|
|
|
this.renderLoopStep(step, stepContentEl);
|
2026-01-16 12:27:44 +01:00
|
|
|
|
break;
|
|
|
|
|
|
case "llm_dialog":
|
2026-01-16 14:11:04 +01:00
|
|
|
|
this.renderLLMDialogStep(step, stepContentEl);
|
2026-01-16 12:27:44 +01:00
|
|
|
|
break;
|
2026-01-17 06:28:36 +01:00
|
|
|
|
case "entity_picker":
|
|
|
|
|
|
this.renderEntityPickerStep(step, stepContentEl);
|
|
|
|
|
|
break;
|
2026-01-16 12:27:44 +01:00
|
|
|
|
case "review":
|
2026-01-16 14:11:04 +01:00
|
|
|
|
this.renderReviewStep(step, stepContentEl);
|
2026-01-16 12:27:44 +01:00
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-16 14:11:04 +01:00
|
|
|
|
// Navigation buttons in sticky footer
|
|
|
|
|
|
const footerEl = contentEl.createEl("div", {
|
|
|
|
|
|
cls: "modal-content-footer",
|
|
|
|
|
|
});
|
|
|
|
|
|
this.renderNavigation(footerEl);
|
2026-01-16 12:27:44 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
checkIdExists(): boolean {
|
|
|
|
|
|
const id = extractFrontmatterId(this.fileContent);
|
|
|
|
|
|
return id !== null && id.trim() !== "";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
generateId(): void {
|
|
|
|
|
|
const id = `note_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
|
|
|
|
const frontmatterMatch = this.fileContent.match(/^---\n([\s\S]*?)\n---/);
|
|
|
|
|
|
if (frontmatterMatch && frontmatterMatch[1]) {
|
|
|
|
|
|
// Add id to existing frontmatter
|
|
|
|
|
|
const frontmatter = frontmatterMatch[1];
|
|
|
|
|
|
if (!frontmatter.includes("id:")) {
|
|
|
|
|
|
const newFrontmatter = `${frontmatter}\nid: ${id}`;
|
|
|
|
|
|
this.fileContent = this.fileContent.replace(
|
|
|
|
|
|
/^---\n([\s\S]*?)\n---/,
|
|
|
|
|
|
`---\n${newFrontmatter}\n---`
|
|
|
|
|
|
);
|
|
|
|
|
|
this.state.patches.push({
|
|
|
|
|
|
type: "frontmatter",
|
|
|
|
|
|
field: "id",
|
|
|
|
|
|
value: id,
|
|
|
|
|
|
});
|
|
|
|
|
|
new Notice("ID generated");
|
|
|
|
|
|
this.renderStep();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
renderInstructionStep(step: InterviewStep, containerEl: HTMLElement): void {
|
|
|
|
|
|
if (step.type !== "instruction") return;
|
|
|
|
|
|
|
|
|
|
|
|
containerEl.createEl("h2", {
|
|
|
|
|
|
text: step.label || "Instruction",
|
|
|
|
|
|
});
|
|
|
|
|
|
containerEl.createEl("div", {
|
|
|
|
|
|
text: step.content,
|
|
|
|
|
|
cls: "interview-instruction-content",
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
renderCaptureTextStep(step: InterviewStep, containerEl: HTMLElement): void {
|
|
|
|
|
|
if (step.type !== "capture_text") return;
|
|
|
|
|
|
|
|
|
|
|
|
const existingValue =
|
|
|
|
|
|
(this.state.collectedData.get(step.key) as string) || "";
|
2026-01-16 14:11:04 +01:00
|
|
|
|
const isPreviewMode = this.previewMode.get(step.key) || false;
|
2026-01-16 12:27:44 +01:00
|
|
|
|
|
|
|
|
|
|
console.log("Render capture_text step", {
|
|
|
|
|
|
stepKey: step.key,
|
|
|
|
|
|
stepLabel: step.label,
|
|
|
|
|
|
existingValue: existingValue,
|
|
|
|
|
|
valueLength: existingValue.length,
|
2026-01-16 14:11:04 +01:00
|
|
|
|
isPreviewMode: isPreviewMode,
|
2026-01-16 12:27:44 +01:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-01-16 14:11:04 +01:00
|
|
|
|
// Field container with vertical layout
|
|
|
|
|
|
const fieldContainer = containerEl.createEl("div", {
|
|
|
|
|
|
cls: "mindnet-field",
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Label
|
|
|
|
|
|
if (step.label) {
|
|
|
|
|
|
const labelEl = fieldContainer.createEl("div", {
|
|
|
|
|
|
cls: "mindnet-field__label",
|
|
|
|
|
|
text: step.label,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Description/Prompt
|
|
|
|
|
|
if (step.prompt) {
|
|
|
|
|
|
const descEl = fieldContainer.createEl("div", {
|
|
|
|
|
|
cls: "mindnet-field__desc",
|
|
|
|
|
|
text: step.prompt,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Container for editor/preview
|
|
|
|
|
|
const editorContainer = fieldContainer.createEl("div", {
|
|
|
|
|
|
cls: "markdown-editor-container",
|
|
|
|
|
|
});
|
|
|
|
|
|
editorContainer.style.width = "100%";
|
|
|
|
|
|
editorContainer.style.position = "relative";
|
|
|
|
|
|
|
|
|
|
|
|
// Preview container (hidden by default)
|
|
|
|
|
|
const previewContainer = editorContainer.createEl("div", {
|
|
|
|
|
|
cls: "markdown-preview-container",
|
|
|
|
|
|
});
|
|
|
|
|
|
previewContainer.style.display = isPreviewMode ? "block" : "none";
|
|
|
|
|
|
previewContainer.style.width = "100%";
|
|
|
|
|
|
previewContainer.style.minHeight = "240px";
|
|
|
|
|
|
previewContainer.style.padding = "1em";
|
|
|
|
|
|
previewContainer.style.border = "1px solid var(--background-modifier-border)";
|
|
|
|
|
|
previewContainer.style.borderRadius = "4px";
|
|
|
|
|
|
previewContainer.style.background = "var(--background-primary)";
|
|
|
|
|
|
previewContainer.style.overflowY = "auto";
|
2026-01-16 22:32:36 +01:00
|
|
|
|
previewContainer.style.position = "relative";
|
|
|
|
|
|
|
|
|
|
|
|
// Add "Back to Edit" button wrapper (outside preview content, so it doesn't get cleared)
|
|
|
|
|
|
const backToEditWrapper = editorContainer.createEl("div", {
|
|
|
|
|
|
cls: "preview-back-button-wrapper",
|
|
|
|
|
|
});
|
|
|
|
|
|
backToEditWrapper.style.display = isPreviewMode ? "block" : "none";
|
|
|
|
|
|
backToEditWrapper.style.position = "absolute";
|
|
|
|
|
|
backToEditWrapper.style.top = "0.5em";
|
|
|
|
|
|
backToEditWrapper.style.right = "0.5em";
|
|
|
|
|
|
backToEditWrapper.style.zIndex = "20";
|
|
|
|
|
|
|
|
|
|
|
|
const backToEditBtn = backToEditWrapper.createEl("button", {
|
|
|
|
|
|
text: "✏️ Zurück zum Bearbeiten",
|
|
|
|
|
|
cls: "mod-cta",
|
|
|
|
|
|
});
|
|
|
|
|
|
backToEditBtn.onclick = () => {
|
|
|
|
|
|
// Get current value from stored input values
|
|
|
|
|
|
const currentValue = this.currentInputValues.get(step.key) || existingValue;
|
|
|
|
|
|
// Update stored value
|
|
|
|
|
|
this.currentInputValues.set(step.key, String(currentValue));
|
|
|
|
|
|
this.state.collectedData.set(step.key, currentValue);
|
|
|
|
|
|
|
|
|
|
|
|
// Toggle preview mode off
|
|
|
|
|
|
this.previewMode.set(step.key, false);
|
|
|
|
|
|
|
|
|
|
|
|
// Re-render to show editor
|
|
|
|
|
|
this.renderStep();
|
|
|
|
|
|
};
|
2026-01-16 14:11:04 +01:00
|
|
|
|
|
|
|
|
|
|
// Editor container
|
|
|
|
|
|
const textEditorContainer = editorContainer.createEl("div", {
|
|
|
|
|
|
cls: "markdown-editor-wrapper",
|
2026-01-16 12:27:44 +01:00
|
|
|
|
});
|
2026-01-16 14:11:04 +01:00
|
|
|
|
textEditorContainer.style.display = isPreviewMode ? "none" : "block";
|
|
|
|
|
|
textEditorContainer.style.width = "100%";
|
2026-01-16 12:27:44 +01:00
|
|
|
|
|
2026-01-16 14:11:04 +01:00
|
|
|
|
// Create textarea first
|
|
|
|
|
|
const textSetting = new Setting(textEditorContainer);
|
|
|
|
|
|
textSetting.settingEl.style.width = "100%";
|
|
|
|
|
|
textSetting.controlEl.style.width = "100%";
|
|
|
|
|
|
|
|
|
|
|
|
let textareaRef: HTMLTextAreaElement | null = null;
|
|
|
|
|
|
|
|
|
|
|
|
textSetting.addTextArea((text) => {
|
|
|
|
|
|
textareaRef = text.inputEl;
|
2026-01-16 12:27:44 +01:00
|
|
|
|
text.setValue(existingValue);
|
|
|
|
|
|
// Store initial value
|
|
|
|
|
|
this.currentInputValues.set(step.key, existingValue);
|
2026-01-16 14:11:04 +01:00
|
|
|
|
|
2026-01-16 12:27:44 +01:00
|
|
|
|
text.onChange((value) => {
|
|
|
|
|
|
console.log("Text field changed", {
|
|
|
|
|
|
stepKey: step.key,
|
|
|
|
|
|
valueLength: value.length,
|
|
|
|
|
|
valuePreview: value.substring(0, 50) + (value.length > 50 ? "..." : ""),
|
|
|
|
|
|
});
|
2026-01-16 14:11:04 +01:00
|
|
|
|
|
2026-01-27 11:18:56 +01:00
|
|
|
|
// WP-26: Speichere Fokus-Info vor State-Update, um Fokus-Verlust zu vermeiden
|
|
|
|
|
|
let hadFocus = false;
|
|
|
|
|
|
let selectionStart = 0;
|
|
|
|
|
|
let selectionEnd = 0;
|
2026-01-30 12:37:06 +01:00
|
|
|
|
if (textareaRef && document.activeElement === textareaRef) {
|
2026-01-27 11:18:56 +01:00
|
|
|
|
hadFocus = true;
|
|
|
|
|
|
selectionStart = textareaRef.selectionStart;
|
|
|
|
|
|
selectionEnd = textareaRef.selectionEnd;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-16 12:27:44 +01:00
|
|
|
|
// Update stored value
|
|
|
|
|
|
this.currentInputValues.set(step.key, value);
|
|
|
|
|
|
this.state.collectedData.set(step.key, value);
|
2026-01-16 14:11:04 +01:00
|
|
|
|
|
2026-01-27 11:18:56 +01:00
|
|
|
|
// WP-26: Stelle Fokus wieder her, wenn er vorher vorhanden war
|
|
|
|
|
|
if (hadFocus && textareaRef) {
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
if (textareaRef && document.body.contains(textareaRef)) {
|
|
|
|
|
|
textareaRef.focus();
|
|
|
|
|
|
textareaRef.setSelectionRange(selectionStart, selectionEnd);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, 0);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-16 14:11:04 +01:00
|
|
|
|
// Update preview if in preview mode
|
|
|
|
|
|
if (isPreviewMode) {
|
|
|
|
|
|
this.updatePreview(previewContainer, value);
|
|
|
|
|
|
}
|
2026-01-16 12:27:44 +01:00
|
|
|
|
});
|
|
|
|
|
|
text.inputEl.rows = 10;
|
2026-01-16 14:11:04 +01:00
|
|
|
|
text.inputEl.style.width = "100%";
|
|
|
|
|
|
text.inputEl.style.minHeight = "240px";
|
|
|
|
|
|
text.inputEl.style.boxSizing = "border-box";
|
2026-01-16 12:27:44 +01:00
|
|
|
|
text.inputEl.focus();
|
|
|
|
|
|
});
|
2026-01-16 14:11:04 +01:00
|
|
|
|
|
|
|
|
|
|
// Create toolbar after textarea is created
|
|
|
|
|
|
// Use setTimeout to ensure textarea is in DOM
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
const textarea = textEditorContainer.querySelector("textarea");
|
|
|
|
|
|
if (textarea) {
|
|
|
|
|
|
const toolbar = createMarkdownToolbar(
|
|
|
|
|
|
textarea,
|
2026-01-16 22:32:36 +01:00
|
|
|
|
async () => {
|
|
|
|
|
|
// Get current value from textarea before toggling
|
|
|
|
|
|
let currentValue = textarea.value;
|
|
|
|
|
|
// If textarea is empty, try to get from stored values
|
|
|
|
|
|
if (!currentValue || currentValue.trim() === "") {
|
|
|
|
|
|
currentValue = this.currentInputValues.get(step.key) || existingValue || "";
|
|
|
|
|
|
}
|
|
|
|
|
|
// Update stored value
|
|
|
|
|
|
this.currentInputValues.set(step.key, currentValue);
|
|
|
|
|
|
this.state.collectedData.set(step.key, currentValue);
|
|
|
|
|
|
|
|
|
|
|
|
// Toggle preview mode
|
2026-01-16 14:11:04 +01:00
|
|
|
|
const newPreviewMode = !this.previewMode.get(step.key);
|
|
|
|
|
|
this.previewMode.set(step.key, newPreviewMode);
|
2026-01-16 22:32:36 +01:00
|
|
|
|
|
|
|
|
|
|
// If switching to preview mode, render preview immediately
|
|
|
|
|
|
if (newPreviewMode) {
|
|
|
|
|
|
// Update preview container visibility
|
|
|
|
|
|
previewContainer.style.display = "block";
|
|
|
|
|
|
textEditorContainer.style.display = "none";
|
|
|
|
|
|
backToEditWrapper.style.display = "block";
|
|
|
|
|
|
// Render preview content (use existingValue as fallback)
|
|
|
|
|
|
const valueToRender = currentValue || existingValue || "";
|
|
|
|
|
|
await this.updatePreview(previewContainer, valueToRender);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Switching back to edit mode
|
|
|
|
|
|
previewContainer.style.display = "none";
|
|
|
|
|
|
textEditorContainer.style.display = "block";
|
|
|
|
|
|
backToEditWrapper.style.display = "none";
|
|
|
|
|
|
}
|
2026-01-17 06:28:36 +01:00
|
|
|
|
},
|
2026-01-27 11:18:56 +01:00
|
|
|
|
async (app: App) => {
|
|
|
|
|
|
// WP-26: Erweitere Entity-Picker um Block-ID-Vorschläge für Intra-Note-Links
|
|
|
|
|
|
// Zeige zuerst Block-ID-Auswahl für Intra-Note-Links, dann normale Note-Auswahl
|
|
|
|
|
|
const blockIds = Array.from(this.state.generatedBlockIds.keys());
|
|
|
|
|
|
|
|
|
|
|
|
if (blockIds.length > 0) {
|
|
|
|
|
|
// Zeige Block-ID-Auswahl-Modal
|
|
|
|
|
|
const blockIdModal = new Modal(app);
|
|
|
|
|
|
blockIdModal.titleEl.textContent = "Block-ID oder Note auswählen";
|
|
|
|
|
|
blockIdModal.contentEl.createEl("p", {
|
|
|
|
|
|
text: "Wähle eine Block-ID für Intra-Note-Link oder eine Note:",
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Block-ID-Liste
|
|
|
|
|
|
const blockIdList = blockIdModal.contentEl.createEl("div");
|
|
|
|
|
|
blockIdList.style.display = "flex";
|
|
|
|
|
|
blockIdList.style.flexDirection = "column";
|
|
|
|
|
|
blockIdList.style.gap = "0.5em";
|
|
|
|
|
|
blockIdList.style.marginBottom = "1em";
|
|
|
|
|
|
|
|
|
|
|
|
for (const blockId of blockIds) {
|
|
|
|
|
|
const sectionInfo = this.state.generatedBlockIds.get(blockId);
|
|
|
|
|
|
const btn = blockIdList.createEl("button", {
|
|
|
|
|
|
text: `#^${blockId} - ${sectionInfo?.heading || blockId}`,
|
|
|
|
|
|
});
|
|
|
|
|
|
btn.style.width = "100%";
|
|
|
|
|
|
btn.style.textAlign = "left";
|
|
|
|
|
|
btn.style.padding = "0.5em";
|
|
|
|
|
|
btn.onclick = async () => {
|
|
|
|
|
|
blockIdModal.close();
|
|
|
|
|
|
|
|
|
|
|
|
// Erstelle Block-ID-Link
|
|
|
|
|
|
const blockIdLink = `#^${blockId}`;
|
|
|
|
|
|
|
|
|
|
|
|
// Prüfe, ob inline micro edging aktiviert ist
|
|
|
|
|
|
const edgingMode = this.profile.edging?.mode;
|
|
|
|
|
|
const shouldRunInlineMicro =
|
|
|
|
|
|
(edgingMode === "inline_micro" || edgingMode === "both") &&
|
|
|
|
|
|
this.settings?.inlineMicroEnabled !== false;
|
|
|
|
|
|
|
|
|
|
|
|
let linkText = `[[${blockIdLink}]]`;
|
|
|
|
|
|
|
|
|
|
|
|
if (shouldRunInlineMicro) {
|
|
|
|
|
|
// Get current step for section key resolution
|
|
|
|
|
|
const currentStep = getCurrentStep(this.state);
|
|
|
|
|
|
if (currentStep) {
|
|
|
|
|
|
console.log("[WP-26] Starting inline micro edging for Block-ID:", blockId);
|
|
|
|
|
|
const edgeType = await this.handleInlineMicroEdging(currentStep, blockIdLink, "");
|
|
|
|
|
|
if (edgeType && typeof edgeType === "string") {
|
|
|
|
|
|
// Use [[rel:type|#^block-id]] format
|
|
|
|
|
|
linkText = `[[rel:${edgeType}|${blockIdLink}]]`;
|
|
|
|
|
|
}
|
2026-01-17 11:54:14 +01:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-27 11:18:56 +01:00
|
|
|
|
|
|
|
|
|
|
// Insert link
|
|
|
|
|
|
const innerLink = linkText.replace(/^\[\[/, "").replace(/\]\]$/, "");
|
|
|
|
|
|
insertWikilinkIntoTextarea(textarea, innerLink);
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Separator
|
|
|
|
|
|
blockIdModal.contentEl.createEl("hr");
|
|
|
|
|
|
|
|
|
|
|
|
// Button für normale Note-Auswahl
|
|
|
|
|
|
const noteBtn = blockIdModal.contentEl.createEl("button", {
|
|
|
|
|
|
text: "📄 Note auswählen...",
|
|
|
|
|
|
cls: "mod-cta",
|
|
|
|
|
|
});
|
|
|
|
|
|
noteBtn.style.width = "100%";
|
|
|
|
|
|
noteBtn.style.marginTop = "1em";
|
|
|
|
|
|
noteBtn.onclick = () => {
|
|
|
|
|
|
blockIdModal.close();
|
|
|
|
|
|
// Öffne normalen Entity-Picker
|
|
|
|
|
|
if (!this.noteIndex) {
|
|
|
|
|
|
new Notice("Note index not available");
|
|
|
|
|
|
return;
|
2026-01-17 11:54:14 +01:00
|
|
|
|
}
|
2026-01-27 11:18:56 +01:00
|
|
|
|
new EntityPickerModal(
|
|
|
|
|
|
app,
|
|
|
|
|
|
this.noteIndex,
|
|
|
|
|
|
async (result: EntityPickerResult) => {
|
|
|
|
|
|
// Check if inline micro edging is enabled (also for toolbar)
|
|
|
|
|
|
// Support: inline_micro, both (inline_micro + post_run)
|
|
|
|
|
|
const edgingMode = this.profile.edging?.mode;
|
|
|
|
|
|
const shouldRunInlineMicro =
|
|
|
|
|
|
(edgingMode === "inline_micro" || edgingMode === "both") &&
|
|
|
|
|
|
this.settings?.inlineMicroEnabled !== false;
|
|
|
|
|
|
|
|
|
|
|
|
let linkText = `[[${result.basename}]]`;
|
|
|
|
|
|
|
|
|
|
|
|
if (shouldRunInlineMicro) {
|
|
|
|
|
|
// Get current step for section key resolution
|
|
|
|
|
|
const currentStep = getCurrentStep(this.state);
|
|
|
|
|
|
if (currentStep) {
|
|
|
|
|
|
console.log("[Mindnet] Starting inline micro edging from toolbar");
|
|
|
|
|
|
const edgeType = await this.handleInlineMicroEdging(currentStep, result.basename, result.path);
|
|
|
|
|
|
if (edgeType && typeof edgeType === "string") {
|
|
|
|
|
|
// Use [[rel:type|link]] format
|
|
|
|
|
|
linkText = `[[rel:${edgeType}|${result.basename}]]`;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Insert link with rel: prefix if edge type was selected
|
|
|
|
|
|
// Extract inner part (without [[ and ]])
|
|
|
|
|
|
const innerLink = linkText.replace(/^\[\[/, "").replace(/\]\]$/, "");
|
|
|
|
|
|
insertWikilinkIntoTextarea(textarea, innerLink);
|
|
|
|
|
|
}
|
|
|
|
|
|
).open();
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
blockIdModal.open();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Keine Block-IDs vorhanden, öffne direkt Entity-Picker
|
|
|
|
|
|
if (!this.noteIndex) {
|
|
|
|
|
|
new Notice("Note index not available");
|
|
|
|
|
|
return;
|
2026-01-17 06:28:36 +01:00
|
|
|
|
}
|
2026-01-27 11:18:56 +01:00
|
|
|
|
new EntityPickerModal(
|
|
|
|
|
|
app,
|
|
|
|
|
|
this.noteIndex,
|
|
|
|
|
|
async (result: EntityPickerResult) => {
|
|
|
|
|
|
// Check if inline micro edging is enabled (also for toolbar)
|
|
|
|
|
|
// Support: inline_micro, both (inline_micro + post_run)
|
|
|
|
|
|
const edgingMode = this.profile.edging?.mode;
|
|
|
|
|
|
const shouldRunInlineMicro =
|
|
|
|
|
|
(edgingMode === "inline_micro" || edgingMode === "both") &&
|
|
|
|
|
|
this.settings?.inlineMicroEnabled !== false;
|
|
|
|
|
|
|
|
|
|
|
|
let linkText = `[[${result.basename}]]`;
|
|
|
|
|
|
|
|
|
|
|
|
if (shouldRunInlineMicro) {
|
|
|
|
|
|
// Get current step for section key resolution
|
|
|
|
|
|
const currentStep = getCurrentStep(this.state);
|
|
|
|
|
|
if (currentStep) {
|
|
|
|
|
|
console.log("[Mindnet] Starting inline micro edging from toolbar");
|
|
|
|
|
|
const edgeType = await this.handleInlineMicroEdging(currentStep, result.basename, result.path);
|
|
|
|
|
|
if (edgeType && typeof edgeType === "string") {
|
|
|
|
|
|
// Use [[rel:type|link]] format
|
|
|
|
|
|
linkText = `[[rel:${edgeType}|${result.basename}]]`;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Insert link with rel: prefix if edge type was selected
|
|
|
|
|
|
// Extract inner part (without [[ and ]])
|
|
|
|
|
|
const innerLink = linkText.replace(/^\[\[/, "").replace(/\]\]$/, "");
|
|
|
|
|
|
insertWikilinkIntoTextarea(textarea, innerLink);
|
|
|
|
|
|
}
|
|
|
|
|
|
).open();
|
|
|
|
|
|
}
|
2026-01-17 22:15:11 +01:00
|
|
|
|
},
|
|
|
|
|
|
async (app: App, textarea: HTMLTextAreaElement) => {
|
|
|
|
|
|
// Edge-Type-Selektor für Interview-Eingabefeld
|
|
|
|
|
|
await this.handleEdgeTypeSelectorForTextarea(app, textarea, step, textEditorContainer);
|
2026-01-16 14:11:04 +01:00
|
|
|
|
}
|
|
|
|
|
|
);
|
|
|
|
|
|
textEditorContainer.insertBefore(toolbar, textEditorContainer.firstChild);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, 10);
|
|
|
|
|
|
|
|
|
|
|
|
// Render preview if in preview mode
|
|
|
|
|
|
if (isPreviewMode && existingValue) {
|
2026-01-16 22:32:36 +01:00
|
|
|
|
this.updatePreview(previewContainer, existingValue).then(() => {
|
|
|
|
|
|
// After preview is rendered, ensure back button is visible
|
|
|
|
|
|
backToEditWrapper.style.display = "block";
|
|
|
|
|
|
});
|
2026-01-16 14:11:04 +01:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-16 18:26:01 +01:00
|
|
|
|
renderCaptureTextLineStep(step: InterviewStep, containerEl: HTMLElement): void {
|
|
|
|
|
|
if (step.type !== "capture_text_line") return;
|
|
|
|
|
|
|
|
|
|
|
|
const existingValue =
|
|
|
|
|
|
(this.state.collectedData.get(step.key) as string) || "";
|
|
|
|
|
|
|
|
|
|
|
|
console.log("Render capture_text_line step", {
|
|
|
|
|
|
stepKey: step.key,
|
|
|
|
|
|
stepLabel: step.label,
|
|
|
|
|
|
existingValue: existingValue,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Field container with vertical layout
|
|
|
|
|
|
const fieldContainer = containerEl.createEl("div", {
|
|
|
|
|
|
cls: "mindnet-field",
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Label
|
|
|
|
|
|
if (step.label) {
|
|
|
|
|
|
const labelEl = fieldContainer.createEl("div", {
|
|
|
|
|
|
cls: "mindnet-field__label",
|
|
|
|
|
|
text: step.label,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Description/Prompt
|
|
|
|
|
|
if (step.prompt) {
|
|
|
|
|
|
const descEl = fieldContainer.createEl("div", {
|
|
|
|
|
|
cls: "mindnet-field__desc",
|
|
|
|
|
|
text: step.prompt,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-16 22:05:36 +01:00
|
|
|
|
// Input container with heading level selector
|
2026-01-16 18:26:01 +01:00
|
|
|
|
const inputContainer = fieldContainer.createEl("div", {
|
|
|
|
|
|
cls: "mindnet-field__input",
|
|
|
|
|
|
});
|
|
|
|
|
|
inputContainer.style.width = "100%";
|
2026-01-16 22:05:36 +01:00
|
|
|
|
inputContainer.style.display = "flex";
|
|
|
|
|
|
inputContainer.style.gap = "0.5em";
|
|
|
|
|
|
inputContainer.style.alignItems = "center";
|
|
|
|
|
|
|
|
|
|
|
|
// Heading level dropdown (if enabled)
|
|
|
|
|
|
let headingLevel: number | null = null;
|
|
|
|
|
|
const headingLevelKey = `${step.key}_heading_level`;
|
|
|
|
|
|
const storedHeadingLevel = this.state.collectedData.get(headingLevelKey);
|
|
|
|
|
|
if (storedHeadingLevel !== undefined && typeof storedHeadingLevel === "number") {
|
|
|
|
|
|
headingLevel = storedHeadingLevel;
|
|
|
|
|
|
} else if (step.heading_level?.enabled) {
|
|
|
|
|
|
headingLevel = step.heading_level.default || 2;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (step.heading_level?.enabled) {
|
|
|
|
|
|
const headingSelectorContainer = inputContainer.createEl("div");
|
|
|
|
|
|
headingSelectorContainer.style.flexShrink = "0";
|
|
|
|
|
|
|
|
|
|
|
|
const headingLabel = headingSelectorContainer.createEl("label", {
|
|
|
|
|
|
text: "H",
|
|
|
|
|
|
attr: { for: `heading-level-${step.key}` },
|
|
|
|
|
|
});
|
|
|
|
|
|
headingLabel.style.marginRight = "0.25em";
|
|
|
|
|
|
headingLabel.style.fontSize = "0.9em";
|
|
|
|
|
|
headingLabel.style.color = "var(--text-muted)";
|
2026-01-16 18:26:01 +01:00
|
|
|
|
|
2026-01-16 22:05:36 +01:00
|
|
|
|
const headingSelect = headingSelectorContainer.createEl("select", {
|
|
|
|
|
|
attr: { id: `heading-level-${step.key}` },
|
|
|
|
|
|
});
|
|
|
|
|
|
headingSelect.style.padding = "0.25em 0.5em";
|
|
|
|
|
|
headingSelect.style.border = "1px solid var(--background-modifier-border)";
|
|
|
|
|
|
headingSelect.style.borderRadius = "4px";
|
|
|
|
|
|
headingSelect.style.background = "var(--background-primary)";
|
|
|
|
|
|
headingSelect.style.fontSize = "0.9em";
|
|
|
|
|
|
headingSelect.style.minWidth = "3em";
|
|
|
|
|
|
|
|
|
|
|
|
// Add options H1-H6
|
|
|
|
|
|
for (let level = 1; level <= 6; level++) {
|
|
|
|
|
|
const option = headingSelect.createEl("option", {
|
|
|
|
|
|
text: `H${level}`,
|
|
|
|
|
|
attr: { value: String(level) },
|
|
|
|
|
|
});
|
|
|
|
|
|
if (headingLevel === level) {
|
|
|
|
|
|
option.selected = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
headingSelect.onchange = () => {
|
|
|
|
|
|
const selectedLevel = parseInt(headingSelect.value, 10);
|
|
|
|
|
|
this.state.collectedData.set(headingLevelKey, selectedLevel);
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Text input (takes remaining space)
|
|
|
|
|
|
const textInputContainer = inputContainer.createEl("div");
|
|
|
|
|
|
textInputContainer.style.flex = "1";
|
|
|
|
|
|
textInputContainer.style.minWidth = "0";
|
|
|
|
|
|
|
|
|
|
|
|
const fieldSetting = new Setting(textInputContainer);
|
2026-01-16 18:26:01 +01:00
|
|
|
|
fieldSetting.settingEl.style.width = "100%";
|
|
|
|
|
|
fieldSetting.controlEl.style.width = "100%";
|
|
|
|
|
|
|
|
|
|
|
|
// Hide the default label from Setting component
|
|
|
|
|
|
const settingNameEl = fieldSetting.settingEl.querySelector(".setting-item-name") as HTMLElement | null;
|
|
|
|
|
|
if (settingNameEl) {
|
|
|
|
|
|
settingNameEl.style.display = "none";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fieldSetting.addText((text) => {
|
|
|
|
|
|
text.setValue(existingValue);
|
|
|
|
|
|
// Store initial value
|
|
|
|
|
|
this.currentInputValues.set(step.key, existingValue);
|
|
|
|
|
|
|
|
|
|
|
|
text.onChange((value) => {
|
|
|
|
|
|
console.log("Text line field changed", {
|
|
|
|
|
|
stepKey: step.key,
|
|
|
|
|
|
value: value,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Update stored value
|
|
|
|
|
|
this.currentInputValues.set(step.key, value);
|
|
|
|
|
|
this.state.collectedData.set(step.key, value);
|
|
|
|
|
|
});
|
|
|
|
|
|
text.inputEl.style.width = "100%";
|
|
|
|
|
|
text.inputEl.style.boxSizing = "border-box";
|
|
|
|
|
|
text.inputEl.focus();
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-16 14:11:04 +01:00
|
|
|
|
/**
|
|
|
|
|
|
* Update preview container with rendered markdown.
|
|
|
|
|
|
*/
|
|
|
|
|
|
private async updatePreview(
|
|
|
|
|
|
container: HTMLElement,
|
|
|
|
|
|
markdown: string
|
|
|
|
|
|
): Promise<void> {
|
2026-01-16 22:32:36 +01:00
|
|
|
|
console.log("updatePreview called", {
|
|
|
|
|
|
markdownLength: markdown?.length || 0,
|
|
|
|
|
|
markdownPreview: markdown?.substring(0, 100) || "(empty)",
|
|
|
|
|
|
containerExists: !!container,
|
|
|
|
|
|
containerId: container.id || "no-id",
|
|
|
|
|
|
containerClasses: container.className,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Clear container but keep structure
|
|
|
|
|
|
const existingChildren = Array.from(container.children);
|
|
|
|
|
|
for (const child of existingChildren) {
|
|
|
|
|
|
child.remove();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!markdown || !markdown.trim()) {
|
|
|
|
|
|
const emptyEl = container.createEl("p", {
|
2026-01-16 14:11:04 +01:00
|
|
|
|
text: "(empty)",
|
|
|
|
|
|
cls: "text-muted",
|
|
|
|
|
|
});
|
2026-01-16 22:32:36 +01:00
|
|
|
|
console.log("Preview: showing empty message");
|
2026-01-16 14:11:04 +01:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Use Obsidian's MarkdownRenderer
|
|
|
|
|
|
// Create a component for the renderer
|
|
|
|
|
|
const component = new Component();
|
|
|
|
|
|
|
2026-01-16 22:32:36 +01:00
|
|
|
|
try {
|
|
|
|
|
|
console.log("Calling MarkdownRenderer.render", {
|
|
|
|
|
|
markdownLength: markdown.length,
|
|
|
|
|
|
filePath: this.file.path,
|
|
|
|
|
|
containerTag: container.tagName,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// IMPORTANT: Component must be loaded before rendering
|
|
|
|
|
|
// This ensures proper lifecycle management
|
|
|
|
|
|
component.load();
|
|
|
|
|
|
|
|
|
|
|
|
await MarkdownRenderer.render(
|
|
|
|
|
|
this.app,
|
|
|
|
|
|
markdown,
|
|
|
|
|
|
container,
|
|
|
|
|
|
this.file.path,
|
|
|
|
|
|
component
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
console.log("Preview rendered successfully", {
|
|
|
|
|
|
containerChildren: container.children.length,
|
|
|
|
|
|
containerHTML: container.innerHTML.substring(0, 200),
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// If container is still empty after rendering, try alternative approach
|
|
|
|
|
|
if (container.children.length === 0) {
|
|
|
|
|
|
console.warn("Container is empty after MarkdownRenderer.render, trying alternative");
|
|
|
|
|
|
// Fallback: create a simple div with the markdown as HTML (basic rendering)
|
|
|
|
|
|
const fallbackDiv = container.createEl("div", {
|
|
|
|
|
|
cls: "markdown-preview-fallback",
|
|
|
|
|
|
});
|
|
|
|
|
|
// Simple markdown to HTML conversion (very basic)
|
|
|
|
|
|
const html = markdown
|
|
|
|
|
|
.replace(/\n\n/g, "</p><p>")
|
|
|
|
|
|
.replace(/\n/g, "<br>")
|
|
|
|
|
|
.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>")
|
|
|
|
|
|
.replace(/\*(.+?)\*/g, "<em>$1</em>")
|
|
|
|
|
|
.replace(/`(.+?)`/g, "<code>$1</code>");
|
|
|
|
|
|
fallbackDiv.innerHTML = `<p>${html}</p>`;
|
|
|
|
|
|
console.log("Fallback preview rendered");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Clean up component when done (optional, but good practice)
|
|
|
|
|
|
// Component will be cleaned up when modal closes
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("Error rendering preview", error);
|
|
|
|
|
|
const errorEl = container.createEl("p", {
|
|
|
|
|
|
text: `Error rendering preview: ${String(error)}`,
|
|
|
|
|
|
cls: "text-error",
|
|
|
|
|
|
});
|
|
|
|
|
|
// Also show the raw markdown for debugging
|
|
|
|
|
|
const rawEl = container.createEl("pre", {
|
|
|
|
|
|
text: markdown,
|
|
|
|
|
|
cls: "text-muted",
|
|
|
|
|
|
});
|
|
|
|
|
|
rawEl.style.fontSize = "0.8em";
|
|
|
|
|
|
rawEl.style.overflow = "auto";
|
|
|
|
|
|
}
|
2026-01-16 12:27:44 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
renderCaptureFrontmatterStep(
|
|
|
|
|
|
step: InterviewStep,
|
|
|
|
|
|
containerEl: HTMLElement
|
|
|
|
|
|
): void {
|
|
|
|
|
|
if (step.type !== "capture_frontmatter") return;
|
|
|
|
|
|
if (!step.field) return;
|
|
|
|
|
|
|
|
|
|
|
|
// Prefill with filename if field is "title" and no existing value
|
|
|
|
|
|
const fileName = this.file.basename || this.file.name.replace(/\.md$/, "");
|
|
|
|
|
|
const existingValue =
|
|
|
|
|
|
(this.state.collectedData.get(step.key) as string) || "";
|
|
|
|
|
|
|
|
|
|
|
|
// Use filename as default for title field if no existing value
|
|
|
|
|
|
const defaultValue = step.field === "title" && !existingValue
|
|
|
|
|
|
? fileName
|
|
|
|
|
|
: existingValue;
|
|
|
|
|
|
|
|
|
|
|
|
console.log("Render capture_frontmatter step", {
|
|
|
|
|
|
stepKey: step.key,
|
|
|
|
|
|
field: step.field,
|
|
|
|
|
|
existingValue: existingValue,
|
|
|
|
|
|
fileName: fileName,
|
|
|
|
|
|
defaultValue: defaultValue,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-01-16 14:11:04 +01:00
|
|
|
|
// Field container with vertical layout
|
|
|
|
|
|
const fieldContainer = containerEl.createEl("div", {
|
|
|
|
|
|
cls: "mindnet-field",
|
2026-01-16 12:27:44 +01:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-01-16 14:11:04 +01:00
|
|
|
|
// Label
|
|
|
|
|
|
const labelText = step.label || step.field;
|
|
|
|
|
|
if (labelText) {
|
|
|
|
|
|
const labelEl = fieldContainer.createEl("div", {
|
|
|
|
|
|
cls: "mindnet-field__label",
|
|
|
|
|
|
text: labelText,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Description/Prompt
|
|
|
|
|
|
if (step.prompt) {
|
|
|
|
|
|
const descEl = fieldContainer.createEl("div", {
|
|
|
|
|
|
cls: "mindnet-field__desc",
|
|
|
|
|
|
text: step.prompt,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Input container
|
|
|
|
|
|
const inputContainer = fieldContainer.createEl("div", {
|
|
|
|
|
|
cls: "mindnet-field__input",
|
|
|
|
|
|
});
|
|
|
|
|
|
inputContainer.style.width = "100%";
|
|
|
|
|
|
|
|
|
|
|
|
const fieldSetting = new Setting(inputContainer);
|
|
|
|
|
|
fieldSetting.settingEl.style.width = "100%";
|
|
|
|
|
|
fieldSetting.controlEl.style.width = "100%";
|
|
|
|
|
|
|
|
|
|
|
|
// Hide the default label from Setting component
|
|
|
|
|
|
const settingNameEl2 = fieldSetting.settingEl.querySelector(".setting-item-name") as HTMLElement | null;
|
|
|
|
|
|
if (settingNameEl2) {
|
|
|
|
|
|
settingNameEl2.style.display = "none";
|
2026-01-16 12:27:44 +01:00
|
|
|
|
}
|
2026-01-16 14:11:04 +01:00
|
|
|
|
|
|
|
|
|
|
fieldSetting.addText((text) => {
|
|
|
|
|
|
text.setValue(defaultValue);
|
|
|
|
|
|
// Store initial value
|
|
|
|
|
|
this.currentInputValues.set(step.key, defaultValue);
|
|
|
|
|
|
|
|
|
|
|
|
text.onChange((value) => {
|
|
|
|
|
|
console.log("Frontmatter field changed", {
|
|
|
|
|
|
stepKey: step.key,
|
|
|
|
|
|
field: step.field,
|
|
|
|
|
|
value: value,
|
2026-01-16 12:27:44 +01:00
|
|
|
|
});
|
2026-01-16 14:11:04 +01:00
|
|
|
|
|
|
|
|
|
|
// Update stored value
|
|
|
|
|
|
this.currentInputValues.set(step.key, value);
|
|
|
|
|
|
|
|
|
|
|
|
this.state.collectedData.set(step.key, value);
|
|
|
|
|
|
// Update or add patch
|
|
|
|
|
|
const existingPatchIndex = this.state.patches.findIndex(
|
|
|
|
|
|
p => p.type === "frontmatter" && p.field === step.field
|
|
|
|
|
|
);
|
|
|
|
|
|
const patch = {
|
|
|
|
|
|
type: "frontmatter" as const,
|
|
|
|
|
|
field: step.field!,
|
|
|
|
|
|
value: value,
|
|
|
|
|
|
};
|
|
|
|
|
|
if (existingPatchIndex >= 0) {
|
|
|
|
|
|
this.state.patches[existingPatchIndex] = patch;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.state.patches.push(patch);
|
|
|
|
|
|
}
|
2026-01-16 12:27:44 +01:00
|
|
|
|
});
|
2026-01-16 14:11:04 +01:00
|
|
|
|
text.inputEl.style.width = "100%";
|
|
|
|
|
|
text.inputEl.style.boxSizing = "border-box";
|
|
|
|
|
|
text.inputEl.focus();
|
|
|
|
|
|
});
|
2026-01-16 12:27:44 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
renderLoopStep(step: InterviewStep, containerEl: HTMLElement): void {
|
|
|
|
|
|
if (step.type !== "loop") return;
|
|
|
|
|
|
|
2026-01-16 20:53:02 +01:00
|
|
|
|
// Check if we're in a nested loop (fullscreen mode)
|
|
|
|
|
|
const currentLoopKey = this.state.activeLoopPath.length > 0
|
|
|
|
|
|
? this.state.activeLoopPath[this.state.activeLoopPath.length - 1]
|
|
|
|
|
|
: null;
|
|
|
|
|
|
|
|
|
|
|
|
// If we're in a nested loop, find which nested step it is
|
|
|
|
|
|
if (currentLoopKey && currentLoopKey.startsWith(step.key + ".")) {
|
|
|
|
|
|
// We're in a nested loop of this step - render it in fullscreen mode
|
|
|
|
|
|
this.renderNestedLoopFullscreen(step, containerEl, currentLoopKey);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Otherwise, render normal loop (or top-level loop)
|
|
|
|
|
|
if (this.state.activeLoopPath.length > 0 && this.state.activeLoopPath[0] !== step.key) {
|
|
|
|
|
|
// We're in a different loop's nested loop, don't render this one
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-16 19:25:55 +01:00
|
|
|
|
// Initialize or get loop runtime state
|
|
|
|
|
|
let loopState = this.state.loopRuntimeStates.get(step.key);
|
|
|
|
|
|
if (!loopState) {
|
|
|
|
|
|
// Initialize from legacy loopContexts if available
|
|
|
|
|
|
const legacyItems = this.state.loopContexts.get(step.key) || [];
|
|
|
|
|
|
loopState = {
|
|
|
|
|
|
items: legacyItems,
|
|
|
|
|
|
draft: {},
|
|
|
|
|
|
editIndex: null,
|
2026-01-16 20:10:08 +01:00
|
|
|
|
activeItemStepIndex: 0,
|
2026-01-16 19:25:55 +01:00
|
|
|
|
};
|
|
|
|
|
|
this.state.loopRuntimeStates.set(step.key, loopState);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const commitMode = step.ui?.commit || "explicit_add";
|
2026-01-16 12:27:44 +01:00
|
|
|
|
|
|
|
|
|
|
console.log("Render loop step", {
|
|
|
|
|
|
stepKey: step.key,
|
|
|
|
|
|
stepLabel: step.label,
|
2026-01-16 19:25:55 +01:00
|
|
|
|
itemsCount: loopState.items.length,
|
2026-01-16 12:27:44 +01:00
|
|
|
|
nestedStepsCount: step.items.length,
|
2026-01-16 19:25:55 +01:00
|
|
|
|
editIndex: loopState.editIndex,
|
|
|
|
|
|
commitMode: commitMode,
|
2026-01-16 20:53:02 +01:00
|
|
|
|
activeLoopPath: this.state.activeLoopPath,
|
2026-01-16 12:27:44 +01:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-01-16 20:53:02 +01:00
|
|
|
|
// Breadcrumb navigation if in nested context
|
|
|
|
|
|
if (this.state.activeLoopPath.length > 0) {
|
|
|
|
|
|
this.renderBreadcrumb(containerEl, step);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-16 19:25:55 +01:00
|
|
|
|
// Title
|
2026-01-16 12:27:44 +01:00
|
|
|
|
containerEl.createEl("h2", {
|
|
|
|
|
|
text: step.label || "Loop",
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-01-16 19:25:55 +01:00
|
|
|
|
// Show editing indicator
|
|
|
|
|
|
if (loopState.editIndex !== null) {
|
|
|
|
|
|
const indicator = containerEl.createEl("div", {
|
|
|
|
|
|
cls: "loop-editing-indicator",
|
|
|
|
|
|
text: `✏️ Editing item ${loopState.editIndex + 1}`,
|
|
|
|
|
|
});
|
|
|
|
|
|
indicator.style.padding = "0.5em";
|
|
|
|
|
|
indicator.style.background = "var(--background-modifier-border-hover)";
|
|
|
|
|
|
indicator.style.borderRadius = "4px";
|
|
|
|
|
|
indicator.style.marginBottom = "1em";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 2-pane container
|
|
|
|
|
|
const paneContainer = containerEl.createEl("div", {
|
|
|
|
|
|
cls: "loop-pane-container",
|
|
|
|
|
|
});
|
|
|
|
|
|
paneContainer.style.display = "flex";
|
|
|
|
|
|
paneContainer.style.gap = "1em";
|
|
|
|
|
|
paneContainer.style.width = "100%";
|
|
|
|
|
|
paneContainer.style.minHeight = "400px";
|
|
|
|
|
|
|
|
|
|
|
|
// Left pane: Items list
|
|
|
|
|
|
const leftPane = paneContainer.createEl("div", {
|
|
|
|
|
|
cls: "loop-items-pane",
|
|
|
|
|
|
});
|
|
|
|
|
|
leftPane.style.width = "40%";
|
|
|
|
|
|
leftPane.style.minWidth = "250px";
|
|
|
|
|
|
leftPane.style.borderRight = "1px solid var(--background-modifier-border)";
|
|
|
|
|
|
leftPane.style.paddingRight = "1em";
|
|
|
|
|
|
leftPane.style.overflowY = "auto";
|
|
|
|
|
|
leftPane.style.maxHeight = "600px";
|
|
|
|
|
|
|
|
|
|
|
|
leftPane.createEl("h3", {
|
|
|
|
|
|
text: `Items (${loopState.items.length})`,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Items list
|
|
|
|
|
|
const itemsList = leftPane.createEl("div", {
|
|
|
|
|
|
cls: "loop-items-list",
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (loopState.items.length === 0) {
|
|
|
|
|
|
itemsList.createEl("p", {
|
|
|
|
|
|
text: "No items yet. Use the editor on the right to add items.",
|
|
|
|
|
|
cls: "interview-note",
|
|
|
|
|
|
});
|
|
|
|
|
|
} else {
|
|
|
|
|
|
for (let i = 0; i < loopState.items.length; i++) {
|
|
|
|
|
|
const item = loopState.items[i];
|
|
|
|
|
|
const itemEl = itemsList.createEl("div", {
|
|
|
|
|
|
cls: `loop-item-entry ${loopState.editIndex === i ? "is-editing" : ""}`,
|
|
|
|
|
|
});
|
|
|
|
|
|
itemEl.style.padding = "0.5em";
|
|
|
|
|
|
itemEl.style.marginBottom = "0.5em";
|
|
|
|
|
|
itemEl.style.border = "1px solid var(--background-modifier-border)";
|
|
|
|
|
|
itemEl.style.borderRadius = "4px";
|
|
|
|
|
|
if (loopState.editIndex === i) {
|
|
|
|
|
|
itemEl.style.background = "var(--background-modifier-border-hover)";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Item preview
|
|
|
|
|
|
const preview = itemEl.createEl("div", {
|
|
|
|
|
|
cls: "loop-item-preview",
|
|
|
|
|
|
});
|
|
|
|
|
|
preview.style.marginBottom = "0.5em";
|
|
|
|
|
|
|
2026-01-16 12:27:44 +01:00
|
|
|
|
if (typeof item === "object" && item !== null) {
|
2026-01-16 19:25:55 +01:00
|
|
|
|
const itemEntries = Object.entries(item as Record<string, unknown>);
|
2026-01-16 12:27:44 +01:00
|
|
|
|
if (itemEntries.length > 0) {
|
2026-01-27 11:18:56 +01:00
|
|
|
|
const previewParts: string[] = [];
|
|
|
|
|
|
for (const [key, value] of itemEntries) {
|
|
|
|
|
|
const nestedStep = step.items.find(s => s.key === key);
|
|
|
|
|
|
const label = nestedStep?.label || key;
|
|
|
|
|
|
const strValue = String(value);
|
|
|
|
|
|
|
|
|
|
|
|
// WP-26: Zeige Block-ID wenn vorhanden
|
|
|
|
|
|
let blockIdDisplay = "";
|
|
|
|
|
|
if (nestedStep && (nestedStep.type === "capture_text" || nestedStep.type === "capture_text_line")) {
|
|
|
|
|
|
const captureStep = nestedStep as import("../interview/types").CaptureTextStep | import("../interview/types").CaptureTextLineStep;
|
|
|
|
|
|
let blockId: string | null = null;
|
|
|
|
|
|
if (captureStep.block_id) {
|
|
|
|
|
|
blockId = captureStep.block_id;
|
|
|
|
|
|
} else if (captureStep.generate_block_id) {
|
|
|
|
|
|
blockId = slugify(`${step.key}-${captureStep.key}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (blockId) {
|
|
|
|
|
|
blockIdDisplay = ` ^${blockId}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
previewParts.push(`${label}: ${strValue.substring(0, 40)}${strValue.length > 40 ? "..." : ""}${blockIdDisplay}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
preview.createSpan({ text: previewParts.join(", ") });
|
2026-01-16 12:27:44 +01:00
|
|
|
|
} else {
|
2026-01-16 19:25:55 +01:00
|
|
|
|
preview.createSpan({ text: "Empty item" });
|
2026-01-16 12:27:44 +01:00
|
|
|
|
}
|
|
|
|
|
|
} else {
|
2026-01-16 19:25:55 +01:00
|
|
|
|
preview.createSpan({ text: String(item) });
|
2026-01-16 12:27:44 +01:00
|
|
|
|
}
|
2026-01-16 19:25:55 +01:00
|
|
|
|
|
|
|
|
|
|
// Actions
|
|
|
|
|
|
const actions = itemEl.createEl("div", {
|
|
|
|
|
|
cls: "loop-item-actions",
|
|
|
|
|
|
});
|
|
|
|
|
|
actions.style.display = "flex";
|
|
|
|
|
|
actions.style.gap = "0.25em";
|
|
|
|
|
|
actions.style.flexWrap = "wrap";
|
|
|
|
|
|
|
|
|
|
|
|
// Edit button
|
|
|
|
|
|
const editBtn = actions.createEl("button", {
|
|
|
|
|
|
text: "Edit",
|
|
|
|
|
|
cls: "mod-cta",
|
|
|
|
|
|
});
|
|
|
|
|
|
editBtn.style.fontSize = "0.85em";
|
|
|
|
|
|
editBtn.style.padding = "0.25em 0.5em";
|
|
|
|
|
|
editBtn.onclick = () => {
|
2026-01-16 20:10:08 +01:00
|
|
|
|
const currentState = this.state.loopRuntimeStates.get(step.key);
|
|
|
|
|
|
if (currentState) {
|
|
|
|
|
|
let newState = startEdit(currentState, i);
|
|
|
|
|
|
newState = resetItemWizard(newState); // Reset to first step
|
|
|
|
|
|
this.state.loopRuntimeStates.set(step.key, newState);
|
|
|
|
|
|
this.renderStep();
|
|
|
|
|
|
}
|
2026-01-16 19:25:55 +01:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Delete button
|
|
|
|
|
|
const deleteBtn = actions.createEl("button", {
|
|
|
|
|
|
text: "Delete",
|
|
|
|
|
|
});
|
|
|
|
|
|
deleteBtn.style.fontSize = "0.85em";
|
|
|
|
|
|
deleteBtn.style.padding = "0.25em 0.5em";
|
|
|
|
|
|
deleteBtn.onclick = () => {
|
|
|
|
|
|
if (confirm(`Delete item ${i + 1}?`)) {
|
|
|
|
|
|
const newState = deleteItem(loopState!, i);
|
|
|
|
|
|
this.state.loopRuntimeStates.set(step.key, newState);
|
|
|
|
|
|
// Update answers
|
|
|
|
|
|
this.state.loopContexts.set(step.key, newState.items);
|
|
|
|
|
|
this.renderStep();
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Move Up button
|
|
|
|
|
|
const moveUpBtn = actions.createEl("button", {
|
|
|
|
|
|
text: "↑",
|
|
|
|
|
|
});
|
|
|
|
|
|
moveUpBtn.style.fontSize = "0.85em";
|
|
|
|
|
|
moveUpBtn.style.padding = "0.25em 0.5em";
|
|
|
|
|
|
moveUpBtn.disabled = i === 0;
|
|
|
|
|
|
moveUpBtn.onclick = () => {
|
|
|
|
|
|
const newState = moveItemUp(loopState!, i);
|
|
|
|
|
|
this.state.loopRuntimeStates.set(step.key, newState);
|
|
|
|
|
|
this.state.loopContexts.set(step.key, newState.items);
|
|
|
|
|
|
this.renderStep();
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Move Down button
|
|
|
|
|
|
const moveDownBtn = actions.createEl("button", {
|
|
|
|
|
|
text: "↓",
|
|
|
|
|
|
});
|
|
|
|
|
|
moveDownBtn.style.fontSize = "0.85em";
|
|
|
|
|
|
moveDownBtn.style.padding = "0.25em 0.5em";
|
|
|
|
|
|
moveDownBtn.disabled = i === loopState.items.length - 1;
|
|
|
|
|
|
moveDownBtn.onclick = () => {
|
|
|
|
|
|
const newState = moveItemDown(loopState!, i);
|
|
|
|
|
|
this.state.loopRuntimeStates.set(step.key, newState);
|
|
|
|
|
|
this.state.loopContexts.set(step.key, newState.items);
|
|
|
|
|
|
this.renderStep();
|
|
|
|
|
|
};
|
2026-01-16 12:27:44 +01:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-16 19:25:55 +01:00
|
|
|
|
// Right pane: Editor
|
|
|
|
|
|
const rightPane = paneContainer.createEl("div", {
|
|
|
|
|
|
cls: "loop-editor-pane",
|
|
|
|
|
|
});
|
|
|
|
|
|
rightPane.style.width = "60%";
|
|
|
|
|
|
rightPane.style.flex = "1";
|
|
|
|
|
|
|
2026-01-16 20:10:08 +01:00
|
|
|
|
// Subwizard header
|
|
|
|
|
|
const itemTitle = rightPane.createEl("h3");
|
|
|
|
|
|
const itemTitleText = loopState.editIndex !== null
|
|
|
|
|
|
? `Item: ${loopState.editIndex + 1} (editing)`
|
|
|
|
|
|
: "Item: New";
|
|
|
|
|
|
const stepCounter = step.items.length > 0
|
|
|
|
|
|
? ` - Step ${loopState.activeItemStepIndex + 1}/${step.items.length}`
|
|
|
|
|
|
: "";
|
|
|
|
|
|
itemTitle.textContent = itemTitleText + stepCounter;
|
|
|
|
|
|
|
|
|
|
|
|
// Render only the active nested step (subwizard)
|
2026-01-16 12:27:44 +01:00
|
|
|
|
if (step.items.length > 0) {
|
2026-01-16 20:10:08 +01:00
|
|
|
|
const activeStepIndex = Math.min(loopState.activeItemStepIndex, step.items.length - 1);
|
|
|
|
|
|
const activeNestedStep = step.items[activeStepIndex];
|
|
|
|
|
|
|
|
|
|
|
|
if (activeNestedStep) {
|
|
|
|
|
|
const editorContainer = rightPane.createEl("div", {
|
|
|
|
|
|
cls: "loop-item-editor",
|
|
|
|
|
|
});
|
2026-01-16 12:27:44 +01:00
|
|
|
|
|
2026-01-16 20:10:08 +01:00
|
|
|
|
const draftValue = loopState.draft[activeNestedStep.key];
|
|
|
|
|
|
this.renderLoopNestedStep(activeNestedStep, editorContainer, step.key, draftValue, (fieldId, value) => {
|
2026-01-16 19:25:55 +01:00
|
|
|
|
const currentState = this.state.loopRuntimeStates.get(step.key);
|
|
|
|
|
|
if (currentState) {
|
|
|
|
|
|
const newState = setDraftField(currentState, fieldId, value);
|
|
|
|
|
|
this.state.loopRuntimeStates.set(step.key, newState);
|
2026-01-27 11:18:56 +01:00
|
|
|
|
// WP-26: NICHT renderStep() hier aufrufen, um Fokus-Verlust zu vermeiden
|
|
|
|
|
|
// Das Re-Rendering wird nur bei expliziten Navigationen durchgeführt
|
2026-01-16 12:27:44 +01:00
|
|
|
|
}
|
2026-01-16 19:25:55 +01:00
|
|
|
|
});
|
2026-01-16 12:27:44 +01:00
|
|
|
|
|
2026-01-16 20:10:08 +01:00
|
|
|
|
// Subwizard navigation buttons
|
|
|
|
|
|
const subwizardNav = rightPane.createEl("div", {
|
|
|
|
|
|
cls: "loop-subwizard-navigation",
|
|
|
|
|
|
});
|
|
|
|
|
|
subwizardNav.style.display = "flex";
|
|
|
|
|
|
subwizardNav.style.gap = "0.5em";
|
|
|
|
|
|
subwizardNav.style.marginTop = "1em";
|
|
|
|
|
|
subwizardNav.style.justifyContent = "space-between";
|
|
|
|
|
|
|
|
|
|
|
|
// Left side: Item Back/Next
|
|
|
|
|
|
const itemNavLeft = subwizardNav.createEl("div", {
|
|
|
|
|
|
cls: "loop-item-nav-left",
|
|
|
|
|
|
});
|
|
|
|
|
|
itemNavLeft.style.display = "flex";
|
|
|
|
|
|
itemNavLeft.style.gap = "0.5em";
|
2026-01-16 14:11:04 +01:00
|
|
|
|
|
2026-01-16 20:10:08 +01:00
|
|
|
|
// Item Back button
|
|
|
|
|
|
const itemBackBtn = itemNavLeft.createEl("button", {
|
|
|
|
|
|
text: "← Item Back",
|
2026-01-16 19:25:55 +01:00
|
|
|
|
});
|
2026-01-16 20:10:08 +01:00
|
|
|
|
itemBackBtn.disabled = activeStepIndex === 0;
|
|
|
|
|
|
itemBackBtn.onclick = () => {
|
2026-01-16 19:25:55 +01:00
|
|
|
|
const currentState = this.state.loopRuntimeStates.get(step.key);
|
|
|
|
|
|
if (currentState) {
|
2026-01-16 20:10:08 +01:00
|
|
|
|
const newState = itemPrevStep(currentState);
|
|
|
|
|
|
this.state.loopRuntimeStates.set(step.key, newState);
|
|
|
|
|
|
this.renderStep();
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Item Next button
|
|
|
|
|
|
const itemNextBtn = itemNavLeft.createEl("button", {
|
|
|
|
|
|
text: "Item Next →",
|
|
|
|
|
|
});
|
|
|
|
|
|
itemNextBtn.disabled = activeStepIndex >= step.items.length - 1;
|
|
|
|
|
|
itemNextBtn.onclick = () => {
|
|
|
|
|
|
const currentState = this.state.loopRuntimeStates.get(step.key);
|
|
|
|
|
|
if (currentState) {
|
2026-01-27 11:18:56 +01:00
|
|
|
|
// WP-26: Track Section-Info für aktuelle nested Steps bevor Navigation
|
|
|
|
|
|
const activeNestedStep = step.items[activeStepIndex];
|
|
|
|
|
|
if (activeNestedStep) {
|
|
|
|
|
|
this.trackSectionInfoForLoopItem(activeNestedStep, step.key, currentState.draft);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-16 20:10:08 +01:00
|
|
|
|
const newState = itemNextStep(currentState, step.items.length);
|
|
|
|
|
|
this.state.loopRuntimeStates.set(step.key, newState);
|
|
|
|
|
|
this.renderStep();
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Right side: Done/Save Item
|
|
|
|
|
|
const itemNavRight = subwizardNav.createEl("div", {
|
|
|
|
|
|
cls: "loop-item-nav-right",
|
|
|
|
|
|
});
|
|
|
|
|
|
itemNavRight.style.display = "flex";
|
|
|
|
|
|
itemNavRight.style.gap = "0.5em";
|
|
|
|
|
|
|
|
|
|
|
|
// Check for required fields
|
|
|
|
|
|
const missingRequired: string[] = [];
|
|
|
|
|
|
for (const nestedStep of step.items) {
|
|
|
|
|
|
if (
|
|
|
|
|
|
(nestedStep.type === "capture_text" || nestedStep.type === "capture_text_line" || nestedStep.type === "capture_frontmatter") &&
|
|
|
|
|
|
nestedStep.required
|
|
|
|
|
|
) {
|
|
|
|
|
|
const value = loopState.draft[nestedStep.key];
|
|
|
|
|
|
if (!value || (typeof value === "string" && value.trim() === "")) {
|
|
|
|
|
|
missingRequired.push(nestedStep.label || nestedStep.key);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Done/Save Item button
|
|
|
|
|
|
const doneBtn = itemNavRight.createEl("button", {
|
|
|
|
|
|
text: loopState.editIndex !== null ? "Save Item" : "Done",
|
|
|
|
|
|
cls: "mod-cta",
|
|
|
|
|
|
});
|
|
|
|
|
|
doneBtn.onclick = () => {
|
|
|
|
|
|
const currentState = this.state.loopRuntimeStates.get(step.key);
|
|
|
|
|
|
if (!currentState) return;
|
|
|
|
|
|
|
|
|
|
|
|
// Check required fields
|
|
|
|
|
|
if (missingRequired.length > 0) {
|
|
|
|
|
|
const msg = `Required fields missing: ${missingRequired.join(", ")}. Save anyway?`;
|
|
|
|
|
|
if (!confirm(msg)) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (isDraftDirty(currentState.draft)) {
|
|
|
|
|
|
const wasNewItem = currentState.editIndex === null;
|
2026-01-27 11:18:56 +01:00
|
|
|
|
|
|
|
|
|
|
// WP-26: Track Section-Info für alle nested Steps bevor Commit
|
|
|
|
|
|
for (const nestedStep of step.items) {
|
|
|
|
|
|
if (nestedStep.type === "capture_text" || nestedStep.type === "capture_text_line") {
|
|
|
|
|
|
this.trackSectionInfoForLoopItem(nestedStep, step.key, currentState.draft);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-16 20:10:08 +01:00
|
|
|
|
const newState = commitDraft(currentState);
|
2026-01-16 19:25:55 +01:00
|
|
|
|
this.state.loopRuntimeStates.set(step.key, newState);
|
2026-01-16 20:10:08 +01:00
|
|
|
|
// Update answers
|
|
|
|
|
|
this.state.loopContexts.set(step.key, newState.items);
|
|
|
|
|
|
|
2026-01-16 20:57:27 +01:00
|
|
|
|
// If we just created a new item, reset all nested loop states (items AND draft)
|
2026-01-16 20:10:08 +01:00
|
|
|
|
if (wasNewItem) {
|
|
|
|
|
|
for (const nestedStep of step.items) {
|
|
|
|
|
|
if (nestedStep.type === "loop") {
|
|
|
|
|
|
const nestedLoopKey = `${step.key}.${nestedStep.key}`;
|
2026-01-16 20:57:27 +01:00
|
|
|
|
// Reset nested loop state completely: clear items and draft
|
|
|
|
|
|
const resetState = {
|
|
|
|
|
|
items: [],
|
|
|
|
|
|
draft: {},
|
|
|
|
|
|
editIndex: null,
|
|
|
|
|
|
activeItemStepIndex: 0,
|
|
|
|
|
|
};
|
|
|
|
|
|
this.state.loopRuntimeStates.set(nestedLoopKey, resetState);
|
|
|
|
|
|
// Also clear preview mode for nested loop fields
|
|
|
|
|
|
const nestedLoopPreviewKeys: string[] = [];
|
|
|
|
|
|
for (const nestedNestedStep of (nestedStep as LoopStep).items) {
|
|
|
|
|
|
nestedLoopPreviewKeys.push(`${nestedLoopKey}.${nestedNestedStep.key}`);
|
2026-01-16 20:10:08 +01:00
|
|
|
|
}
|
2026-01-16 20:57:27 +01:00
|
|
|
|
nestedLoopPreviewKeys.forEach(key => this.previewMode.delete(key));
|
2026-01-16 20:10:08 +01:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-27 11:18:56 +01:00
|
|
|
|
// WP-26: Nach dem Speichern bleibt der Loop-Step aktiv (kein automatischer Wechsel zum nächsten Step)
|
|
|
|
|
|
// Der Benutzer kann weitere Items hinzufügen oder mit "Next" zum nächsten Step navigieren
|
2026-01-16 19:25:55 +01:00
|
|
|
|
this.renderStep();
|
2026-01-16 20:10:08 +01:00
|
|
|
|
} else {
|
|
|
|
|
|
new Notice("Please enter at least one field");
|
2026-01-16 19:25:55 +01:00
|
|
|
|
}
|
|
|
|
|
|
};
|
2026-01-16 20:10:08 +01:00
|
|
|
|
|
|
|
|
|
|
// Show warning if required fields missing
|
|
|
|
|
|
if (missingRequired.length > 0) {
|
|
|
|
|
|
const warning = rightPane.createEl("div", {
|
|
|
|
|
|
cls: "loop-required-warning",
|
|
|
|
|
|
text: `⚠️ Required fields missing: ${missingRequired.join(", ")}`,
|
|
|
|
|
|
});
|
|
|
|
|
|
warning.style.padding = "0.5em";
|
|
|
|
|
|
warning.style.background = "var(--background-modifier-error)";
|
|
|
|
|
|
warning.style.borderRadius = "4px";
|
|
|
|
|
|
warning.style.marginTop = "0.5em";
|
|
|
|
|
|
warning.style.color = "var(--text-error)";
|
|
|
|
|
|
}
|
2026-01-16 19:25:55 +01:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-16 14:11:04 +01:00
|
|
|
|
|
2026-01-16 19:25:55 +01:00
|
|
|
|
/**
|
|
|
|
|
|
* Render a nested step within a loop editor.
|
|
|
|
|
|
*/
|
|
|
|
|
|
private renderLoopNestedStep(
|
|
|
|
|
|
nestedStep: InterviewStep,
|
|
|
|
|
|
containerEl: HTMLElement,
|
|
|
|
|
|
loopKey: string,
|
|
|
|
|
|
draftValue: unknown,
|
|
|
|
|
|
onFieldChange: (fieldId: string, value: unknown) => void
|
|
|
|
|
|
): void {
|
|
|
|
|
|
const existingValue = draftValue !== undefined ? String(draftValue) : "";
|
2026-01-16 20:53:02 +01:00
|
|
|
|
// Use unique key for preview mode tracking in nested loops
|
|
|
|
|
|
const previewKey = `${loopKey}.${nestedStep.key}`;
|
|
|
|
|
|
const isPreviewMode = this.previewMode.get(previewKey) || false;
|
2026-01-16 14:11:04 +01:00
|
|
|
|
|
2026-01-16 19:25:55 +01:00
|
|
|
|
if (nestedStep.type === "capture_text") {
|
|
|
|
|
|
// Field container
|
|
|
|
|
|
const fieldContainer = containerEl.createEl("div", {
|
|
|
|
|
|
cls: "mindnet-field",
|
|
|
|
|
|
});
|
2026-01-16 14:11:04 +01:00
|
|
|
|
|
2026-01-16 19:25:55 +01:00
|
|
|
|
// Label
|
|
|
|
|
|
if (nestedStep.label) {
|
|
|
|
|
|
const labelEl = fieldContainer.createEl("div", {
|
|
|
|
|
|
cls: "mindnet-field__label",
|
|
|
|
|
|
text: nestedStep.label,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2026-01-16 14:11:04 +01:00
|
|
|
|
|
2026-01-16 19:25:55 +01:00
|
|
|
|
// Description/Prompt
|
|
|
|
|
|
if (nestedStep.prompt) {
|
|
|
|
|
|
const descEl = fieldContainer.createEl("div", {
|
|
|
|
|
|
cls: "mindnet-field__desc",
|
|
|
|
|
|
text: nestedStep.prompt,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2026-01-16 18:26:01 +01:00
|
|
|
|
|
2026-01-27 11:18:56 +01:00
|
|
|
|
// WP-26: Block-ID-Anzeige für capture_text mit section
|
|
|
|
|
|
const captureStep = nestedStep as import("../interview/types").CaptureTextStep;
|
|
|
|
|
|
if (captureStep.section) {
|
|
|
|
|
|
let blockId: string | null = null;
|
|
|
|
|
|
if (captureStep.block_id) {
|
|
|
|
|
|
blockId = captureStep.block_id;
|
|
|
|
|
|
} else if (captureStep.generate_block_id) {
|
|
|
|
|
|
blockId = slugify(`${loopKey}-${captureStep.key}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (blockId) {
|
|
|
|
|
|
const blockIdDisplay = fieldContainer.createEl("div", {
|
|
|
|
|
|
cls: "block-id-display",
|
|
|
|
|
|
text: `Block-ID: ^${blockId}`,
|
|
|
|
|
|
});
|
|
|
|
|
|
blockIdDisplay.style.fontSize = "0.85em";
|
|
|
|
|
|
blockIdDisplay.style.color = "var(--text-muted)";
|
|
|
|
|
|
blockIdDisplay.style.marginBottom = "0.5em";
|
|
|
|
|
|
blockIdDisplay.style.fontStyle = "italic";
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-16 20:53:02 +01:00
|
|
|
|
// Container for editor/preview
|
2026-01-16 19:25:55 +01:00
|
|
|
|
const editorContainer = fieldContainer.createEl("div", {
|
|
|
|
|
|
cls: "markdown-editor-container",
|
|
|
|
|
|
});
|
|
|
|
|
|
editorContainer.style.width = "100%";
|
|
|
|
|
|
editorContainer.style.position = "relative";
|
2026-01-16 18:26:01 +01:00
|
|
|
|
|
2026-01-16 20:53:02 +01:00
|
|
|
|
// Preview container (hidden by default)
|
|
|
|
|
|
const previewContainer = editorContainer.createEl("div", {
|
|
|
|
|
|
cls: "markdown-preview-container",
|
|
|
|
|
|
});
|
|
|
|
|
|
previewContainer.style.display = isPreviewMode ? "block" : "none";
|
|
|
|
|
|
previewContainer.style.width = "100%";
|
|
|
|
|
|
previewContainer.style.minHeight = "240px";
|
|
|
|
|
|
previewContainer.style.padding = "1em";
|
|
|
|
|
|
previewContainer.style.border = "1px solid var(--background-modifier-border)";
|
|
|
|
|
|
previewContainer.style.borderRadius = "4px";
|
|
|
|
|
|
previewContainer.style.background = "var(--background-primary)";
|
|
|
|
|
|
previewContainer.style.overflowY = "auto";
|
2026-01-16 20:57:27 +01:00
|
|
|
|
previewContainer.style.position = "relative";
|
|
|
|
|
|
|
2026-01-16 22:32:36 +01:00
|
|
|
|
// Add "Back to Edit" button wrapper (outside preview content, so it doesn't get cleared)
|
|
|
|
|
|
const backToEditWrapper = editorContainer.createEl("div", {
|
|
|
|
|
|
cls: "preview-back-button-wrapper",
|
|
|
|
|
|
});
|
|
|
|
|
|
backToEditWrapper.style.display = isPreviewMode ? "block" : "none";
|
|
|
|
|
|
backToEditWrapper.style.position = "absolute";
|
|
|
|
|
|
backToEditWrapper.style.top = "0.5em";
|
|
|
|
|
|
backToEditWrapper.style.right = "0.5em";
|
|
|
|
|
|
backToEditWrapper.style.zIndex = "20";
|
|
|
|
|
|
|
|
|
|
|
|
const backToEditBtn = backToEditWrapper.createEl("button", {
|
|
|
|
|
|
text: "✏️ Zurück zum Bearbeiten",
|
|
|
|
|
|
cls: "mod-cta",
|
|
|
|
|
|
});
|
|
|
|
|
|
backToEditBtn.onclick = () => {
|
|
|
|
|
|
// Get current value from draft (it's already saved)
|
|
|
|
|
|
const currentLoopState = this.state.loopRuntimeStates.get(loopKey);
|
|
|
|
|
|
const currentValue = currentLoopState?.draft[nestedStep.key] || existingValue;
|
|
|
|
|
|
// Update draft with current value (ensure it's saved)
|
|
|
|
|
|
onFieldChange(nestedStep.key, String(currentValue));
|
|
|
|
|
|
|
|
|
|
|
|
// Toggle preview mode off
|
|
|
|
|
|
this.previewMode.set(previewKey, false);
|
|
|
|
|
|
|
|
|
|
|
|
// Re-render to show editor
|
|
|
|
|
|
this.renderStep();
|
|
|
|
|
|
};
|
2026-01-16 20:53:02 +01:00
|
|
|
|
|
|
|
|
|
|
// Editor container
|
2026-01-16 19:25:55 +01:00
|
|
|
|
const textEditorContainer = editorContainer.createEl("div", {
|
|
|
|
|
|
cls: "markdown-editor-wrapper",
|
|
|
|
|
|
});
|
2026-01-16 20:53:02 +01:00
|
|
|
|
textEditorContainer.style.display = isPreviewMode ? "none" : "block";
|
2026-01-16 19:25:55 +01:00
|
|
|
|
textEditorContainer.style.width = "100%";
|
2026-01-16 18:26:01 +01:00
|
|
|
|
|
2026-01-16 19:25:55 +01:00
|
|
|
|
const textSetting = new Setting(textEditorContainer);
|
|
|
|
|
|
textSetting.settingEl.style.width = "100%";
|
|
|
|
|
|
textSetting.controlEl.style.width = "100%";
|
2026-01-16 18:26:01 +01:00
|
|
|
|
|
2026-01-16 19:25:55 +01:00
|
|
|
|
// Hide the default label
|
|
|
|
|
|
const settingNameEl = textSetting.settingEl.querySelector(".setting-item-name") as HTMLElement | null;
|
|
|
|
|
|
if (settingNameEl) {
|
|
|
|
|
|
settingNameEl.style.display = "none";
|
|
|
|
|
|
}
|
2026-01-16 18:26:01 +01:00
|
|
|
|
|
2026-01-16 20:53:02 +01:00
|
|
|
|
let textareaRef: HTMLTextAreaElement | null = null;
|
|
|
|
|
|
|
2026-01-16 19:25:55 +01:00
|
|
|
|
textSetting.addTextArea((text) => {
|
2026-01-16 20:53:02 +01:00
|
|
|
|
textareaRef = text.inputEl;
|
2026-01-16 19:25:55 +01:00
|
|
|
|
text.setValue(existingValue);
|
2026-01-27 11:18:56 +01:00
|
|
|
|
|
|
|
|
|
|
// WP-26: Speichere Fokus-Info vor onChange, um Fokus-Verlust zu vermeiden
|
|
|
|
|
|
let hadFocus = false;
|
|
|
|
|
|
let selectionStart = 0;
|
|
|
|
|
|
let selectionEnd = 0;
|
|
|
|
|
|
|
2026-01-16 19:25:55 +01:00
|
|
|
|
text.onChange((value) => {
|
2026-01-27 11:18:56 +01:00
|
|
|
|
// WP-26: Speichere Fokus-Info vor State-Update
|
2026-01-30 12:37:06 +01:00
|
|
|
|
if (textareaRef && document.activeElement === textareaRef) {
|
2026-01-27 11:18:56 +01:00
|
|
|
|
hadFocus = true;
|
|
|
|
|
|
selectionStart = textareaRef.selectionStart;
|
|
|
|
|
|
selectionEnd = textareaRef.selectionEnd;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-16 19:25:55 +01:00
|
|
|
|
onFieldChange(nestedStep.key, value);
|
2026-01-27 11:18:56 +01:00
|
|
|
|
|
|
|
|
|
|
// WP-26: Stelle Fokus wieder her, wenn er vorher vorhanden war
|
|
|
|
|
|
if (hadFocus && textareaRef) {
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
if (textareaRef && document.body.contains(textareaRef)) {
|
|
|
|
|
|
textareaRef.focus();
|
|
|
|
|
|
textareaRef.setSelectionRange(selectionStart, selectionEnd);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, 0);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-16 20:53:02 +01:00
|
|
|
|
// Update preview if in preview mode
|
|
|
|
|
|
const currentPreviewMode = this.previewMode.get(previewKey) || false;
|
|
|
|
|
|
if (currentPreviewMode) {
|
|
|
|
|
|
this.updatePreview(previewContainer, value);
|
|
|
|
|
|
}
|
2026-01-16 19:25:55 +01:00
|
|
|
|
});
|
|
|
|
|
|
text.inputEl.rows = 8;
|
|
|
|
|
|
text.inputEl.style.width = "100%";
|
|
|
|
|
|
text.inputEl.style.minHeight = "150px";
|
|
|
|
|
|
text.inputEl.style.boxSizing = "border-box";
|
|
|
|
|
|
});
|
2026-01-16 12:27:44 +01:00
|
|
|
|
|
2026-01-16 20:57:27 +01:00
|
|
|
|
// Add toolbar with preview toggle (only show in edit mode)
|
|
|
|
|
|
if (!isPreviewMode) {
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
const textarea = textEditorContainer.querySelector("textarea");
|
|
|
|
|
|
if (textarea) {
|
|
|
|
|
|
const itemToolbar = createMarkdownToolbar(
|
|
|
|
|
|
textarea,
|
2026-01-17 22:15:11 +01:00
|
|
|
|
undefined, // No preview toggle for loop items
|
2026-01-17 06:28:36 +01:00
|
|
|
|
(app: App) => {
|
2026-01-17 22:15:11 +01:00
|
|
|
|
// Entity picker for loop items
|
2026-01-17 06:28:36 +01:00
|
|
|
|
if (!this.noteIndex) {
|
|
|
|
|
|
new Notice("Note index not available");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
new EntityPickerModal(
|
|
|
|
|
|
app,
|
|
|
|
|
|
this.noteIndex,
|
2026-01-17 11:54:14 +01:00
|
|
|
|
async (result: EntityPickerResult) => {
|
2026-01-17 22:15:11 +01:00
|
|
|
|
// Check if inline micro edging is enabled
|
2026-01-17 11:54:14 +01:00
|
|
|
|
const edgingMode = this.profile.edging?.mode;
|
|
|
|
|
|
const shouldRunInlineMicro =
|
|
|
|
|
|
(edgingMode === "inline_micro" || edgingMode === "both") &&
|
|
|
|
|
|
this.settings?.inlineMicroEnabled !== false;
|
|
|
|
|
|
|
|
|
|
|
|
let linkText = `[[${result.basename}]]`;
|
|
|
|
|
|
|
|
|
|
|
|
if (shouldRunInlineMicro) {
|
2026-01-17 22:15:11 +01:00
|
|
|
|
// nestedStep is already available in this scope
|
2026-01-17 11:54:14 +01:00
|
|
|
|
const edgeType = await this.handleInlineMicroEdging(nestedStep, result.basename, result.path);
|
|
|
|
|
|
if (edgeType && typeof edgeType === "string") {
|
|
|
|
|
|
linkText = `[[rel:${edgeType}|${result.basename}]]`;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const innerLink = linkText.replace(/^\[\[/, "").replace(/\]\]$/, "");
|
|
|
|
|
|
insertWikilinkIntoTextarea(textarea, innerLink);
|
2026-01-17 06:28:36 +01:00
|
|
|
|
}
|
|
|
|
|
|
).open();
|
2026-01-17 22:15:11 +01:00
|
|
|
|
},
|
|
|
|
|
|
async (app: App, textarea: HTMLTextAreaElement) => {
|
|
|
|
|
|
// Edge-Type-Selektor für Loop-Items
|
|
|
|
|
|
await this.handleEdgeTypeSelectorForTextarea(app, textarea, nestedStep, textEditorContainer);
|
2026-01-16 20:57:27 +01:00
|
|
|
|
}
|
|
|
|
|
|
);
|
|
|
|
|
|
textEditorContainer.insertBefore(itemToolbar, textEditorContainer.firstChild);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, 10);
|
|
|
|
|
|
}
|
2026-01-16 20:53:02 +01:00
|
|
|
|
|
|
|
|
|
|
// Render preview if in preview mode
|
|
|
|
|
|
if (isPreviewMode && existingValue) {
|
2026-01-16 22:32:36 +01:00
|
|
|
|
this.updatePreview(previewContainer, existingValue).then(() => {
|
|
|
|
|
|
// After preview is rendered, ensure back button is visible
|
|
|
|
|
|
backToEditWrapper.style.display = "block";
|
|
|
|
|
|
});
|
2026-01-16 20:53:02 +01:00
|
|
|
|
}
|
2026-01-16 19:25:55 +01:00
|
|
|
|
} else if (nestedStep.type === "capture_text_line") {
|
|
|
|
|
|
// Field container
|
|
|
|
|
|
const fieldContainer = containerEl.createEl("div", {
|
|
|
|
|
|
cls: "mindnet-field",
|
|
|
|
|
|
});
|
2026-01-16 14:11:04 +01:00
|
|
|
|
|
2026-01-16 19:25:55 +01:00
|
|
|
|
// Label
|
|
|
|
|
|
if (nestedStep.label) {
|
|
|
|
|
|
const labelEl = fieldContainer.createEl("div", {
|
|
|
|
|
|
cls: "mindnet-field__label",
|
|
|
|
|
|
text: nestedStep.label,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2026-01-16 14:11:04 +01:00
|
|
|
|
|
2026-01-16 19:25:55 +01:00
|
|
|
|
// Description/Prompt
|
|
|
|
|
|
if (nestedStep.prompt) {
|
|
|
|
|
|
const descEl = fieldContainer.createEl("div", {
|
|
|
|
|
|
cls: "mindnet-field__desc",
|
|
|
|
|
|
text: nestedStep.prompt,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2026-01-16 14:11:04 +01:00
|
|
|
|
|
2026-01-16 22:05:36 +01:00
|
|
|
|
// Input container with heading level selector
|
2026-01-16 19:25:55 +01:00
|
|
|
|
const inputContainer = fieldContainer.createEl("div", {
|
|
|
|
|
|
cls: "mindnet-field__input",
|
|
|
|
|
|
});
|
|
|
|
|
|
inputContainer.style.width = "100%";
|
2026-01-16 22:05:36 +01:00
|
|
|
|
inputContainer.style.display = "flex";
|
|
|
|
|
|
inputContainer.style.gap = "0.5em";
|
|
|
|
|
|
inputContainer.style.alignItems = "center";
|
|
|
|
|
|
|
|
|
|
|
|
// Heading level dropdown (if enabled)
|
|
|
|
|
|
const headingLevelDraftKey = `${nestedStep.key}_heading_level`;
|
|
|
|
|
|
// Get loopState from the loopKey to access draft
|
|
|
|
|
|
const currentLoopState = this.state.loopRuntimeStates.get(loopKey);
|
|
|
|
|
|
let headingLevelDraftValue: number | undefined = undefined;
|
|
|
|
|
|
if (currentLoopState) {
|
|
|
|
|
|
headingLevelDraftValue = currentLoopState.draft[headingLevelDraftKey] as number | undefined;
|
|
|
|
|
|
// If not in draft and we're editing, try to get from the item being edited
|
|
|
|
|
|
if (headingLevelDraftValue === undefined && currentLoopState.editIndex !== null) {
|
|
|
|
|
|
const itemBeingEdited = currentLoopState.items[currentLoopState.editIndex];
|
|
|
|
|
|
if (itemBeingEdited && typeof itemBeingEdited === "object") {
|
|
|
|
|
|
headingLevelDraftValue = (itemBeingEdited as Record<string, unknown>)[headingLevelDraftKey] as number | undefined;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
let headingLevel: number | null = null;
|
|
|
|
|
|
if (headingLevelDraftValue !== undefined && typeof headingLevelDraftValue === "number") {
|
|
|
|
|
|
headingLevel = headingLevelDraftValue;
|
|
|
|
|
|
} else if (nestedStep.heading_level?.enabled) {
|
|
|
|
|
|
headingLevel = nestedStep.heading_level.default || 2;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (nestedStep.heading_level?.enabled) {
|
|
|
|
|
|
const headingSelectorContainer = inputContainer.createEl("div");
|
|
|
|
|
|
headingSelectorContainer.style.flexShrink = "0";
|
|
|
|
|
|
|
|
|
|
|
|
const headingLabel = headingSelectorContainer.createEl("label", {
|
|
|
|
|
|
text: "H",
|
|
|
|
|
|
attr: { for: `heading-level-${loopKey}-${nestedStep.key}` },
|
|
|
|
|
|
});
|
|
|
|
|
|
headingLabel.style.marginRight = "0.25em";
|
|
|
|
|
|
headingLabel.style.fontSize = "0.9em";
|
|
|
|
|
|
headingLabel.style.color = "var(--text-muted)";
|
|
|
|
|
|
|
|
|
|
|
|
const headingSelect = headingSelectorContainer.createEl("select", {
|
|
|
|
|
|
attr: { id: `heading-level-${loopKey}-${nestedStep.key}` },
|
|
|
|
|
|
});
|
|
|
|
|
|
headingSelect.style.padding = "0.25em 0.5em";
|
|
|
|
|
|
headingSelect.style.border = "1px solid var(--background-modifier-border)";
|
|
|
|
|
|
headingSelect.style.borderRadius = "4px";
|
|
|
|
|
|
headingSelect.style.background = "var(--background-primary)";
|
|
|
|
|
|
headingSelect.style.fontSize = "0.9em";
|
|
|
|
|
|
headingSelect.style.minWidth = "3em";
|
|
|
|
|
|
|
|
|
|
|
|
// Add options H1-H6
|
|
|
|
|
|
for (let level = 1; level <= 6; level++) {
|
|
|
|
|
|
const option = headingSelect.createEl("option", {
|
|
|
|
|
|
text: `H${level}`,
|
|
|
|
|
|
attr: { value: String(level) },
|
|
|
|
|
|
});
|
|
|
|
|
|
if (headingLevel === level) {
|
|
|
|
|
|
option.selected = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
headingSelect.onchange = () => {
|
|
|
|
|
|
const selectedLevel = parseInt(headingSelect.value, 10);
|
|
|
|
|
|
onFieldChange(headingLevelDraftKey, selectedLevel);
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Text input (takes remaining space)
|
|
|
|
|
|
const textInputContainer = inputContainer.createEl("div");
|
|
|
|
|
|
textInputContainer.style.flex = "1";
|
|
|
|
|
|
textInputContainer.style.minWidth = "0";
|
2026-01-16 14:11:04 +01:00
|
|
|
|
|
2026-01-16 22:05:36 +01:00
|
|
|
|
const fieldSetting = new Setting(textInputContainer);
|
2026-01-16 19:25:55 +01:00
|
|
|
|
fieldSetting.settingEl.style.width = "100%";
|
|
|
|
|
|
fieldSetting.controlEl.style.width = "100%";
|
|
|
|
|
|
|
|
|
|
|
|
// Hide the default label
|
|
|
|
|
|
const settingNameEl = fieldSetting.settingEl.querySelector(".setting-item-name") as HTMLElement | null;
|
|
|
|
|
|
if (settingNameEl) {
|
|
|
|
|
|
settingNameEl.style.display = "none";
|
2026-01-16 12:27:44 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-27 11:18:56 +01:00
|
|
|
|
// WP-26: Block-ID-Anzeige für capture_text_line mit heading_level
|
|
|
|
|
|
let blockIdDisplay: HTMLElement | null = null;
|
|
|
|
|
|
if (nestedStep.heading_level?.enabled) {
|
|
|
|
|
|
const captureStep = nestedStep as import("../interview/types").CaptureTextLineStep;
|
|
|
|
|
|
let blockId: string | null = null;
|
|
|
|
|
|
if (captureStep.block_id) {
|
|
|
|
|
|
blockId = captureStep.block_id;
|
|
|
|
|
|
} else if (captureStep.generate_block_id) {
|
|
|
|
|
|
blockId = slugify(`${loopKey}-${captureStep.key}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (blockId) {
|
|
|
|
|
|
blockIdDisplay = fieldSetting.settingEl.createEl("div", {
|
|
|
|
|
|
cls: "block-id-display",
|
|
|
|
|
|
text: `Block-ID: ^${blockId}`,
|
|
|
|
|
|
});
|
|
|
|
|
|
blockIdDisplay.style.fontSize = "0.85em";
|
|
|
|
|
|
blockIdDisplay.style.color = "var(--text-muted)";
|
|
|
|
|
|
blockIdDisplay.style.marginTop = "0.25em";
|
|
|
|
|
|
blockIdDisplay.style.fontStyle = "italic";
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-16 19:25:55 +01:00
|
|
|
|
fieldSetting.addText((text) => {
|
|
|
|
|
|
text.setValue(existingValue);
|
|
|
|
|
|
text.onChange((value) => {
|
|
|
|
|
|
onFieldChange(nestedStep.key, value);
|
|
|
|
|
|
});
|
|
|
|
|
|
text.inputEl.style.width = "100%";
|
|
|
|
|
|
text.inputEl.style.boxSizing = "border-box";
|
2026-01-16 12:27:44 +01:00
|
|
|
|
});
|
2026-01-16 19:25:55 +01:00
|
|
|
|
} else if (nestedStep.type === "capture_frontmatter") {
|
|
|
|
|
|
// Field container
|
|
|
|
|
|
const fieldContainer = containerEl.createEl("div", {
|
|
|
|
|
|
cls: "mindnet-field",
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Label
|
|
|
|
|
|
const labelText = nestedStep.label || nestedStep.field || nestedStep.key;
|
|
|
|
|
|
if (labelText) {
|
|
|
|
|
|
const labelEl = fieldContainer.createEl("div", {
|
|
|
|
|
|
cls: "mindnet-field__label",
|
|
|
|
|
|
text: labelText,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Description/Prompt
|
|
|
|
|
|
if (nestedStep.prompt) {
|
|
|
|
|
|
const descEl = fieldContainer.createEl("div", {
|
|
|
|
|
|
cls: "mindnet-field__desc",
|
|
|
|
|
|
text: nestedStep.prompt,
|
2026-01-16 12:27:44 +01:00
|
|
|
|
});
|
|
|
|
|
|
}
|
2026-01-16 19:25:55 +01:00
|
|
|
|
|
|
|
|
|
|
// Input container
|
|
|
|
|
|
const inputContainer = fieldContainer.createEl("div", {
|
|
|
|
|
|
cls: "mindnet-field__input",
|
|
|
|
|
|
});
|
|
|
|
|
|
inputContainer.style.width = "100%";
|
|
|
|
|
|
|
|
|
|
|
|
const fieldSetting = new Setting(inputContainer);
|
|
|
|
|
|
fieldSetting.settingEl.style.width = "100%";
|
|
|
|
|
|
fieldSetting.controlEl.style.width = "100%";
|
|
|
|
|
|
|
|
|
|
|
|
// Hide the default label
|
|
|
|
|
|
const settingNameEl = fieldSetting.settingEl.querySelector(".setting-item-name") as HTMLElement | null;
|
|
|
|
|
|
if (settingNameEl) {
|
|
|
|
|
|
settingNameEl.style.display = "none";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fieldSetting.addText((text) => {
|
|
|
|
|
|
text.setValue(existingValue);
|
|
|
|
|
|
text.onChange((value) => {
|
|
|
|
|
|
onFieldChange(nestedStep.key, value);
|
|
|
|
|
|
});
|
|
|
|
|
|
text.inputEl.style.width = "100%";
|
|
|
|
|
|
text.inputEl.style.boxSizing = "border-box";
|
|
|
|
|
|
});
|
2026-01-16 20:10:08 +01:00
|
|
|
|
} else if (nestedStep.type === "loop") {
|
2026-01-16 20:53:02 +01:00
|
|
|
|
// Nested loop: render as a button to enter fullscreen mode
|
2026-01-16 20:10:08 +01:00
|
|
|
|
const nestedLoopItems = Array.isArray(draftValue) ? draftValue : [];
|
2026-01-16 20:53:02 +01:00
|
|
|
|
const nestedLoopKey = `${loopKey}.${nestedStep.key}`;
|
2026-01-16 20:10:08 +01:00
|
|
|
|
|
|
|
|
|
|
// Get or create nested loop state
|
|
|
|
|
|
let nestedLoopState = this.state.loopRuntimeStates.get(nestedLoopKey);
|
|
|
|
|
|
if (!nestedLoopState) {
|
|
|
|
|
|
nestedLoopState = {
|
|
|
|
|
|
items: nestedLoopItems,
|
|
|
|
|
|
draft: {},
|
|
|
|
|
|
editIndex: null,
|
|
|
|
|
|
activeItemStepIndex: 0,
|
|
|
|
|
|
};
|
|
|
|
|
|
this.state.loopRuntimeStates.set(nestedLoopKey, nestedLoopState);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-16 20:53:02 +01:00
|
|
|
|
// Field container
|
|
|
|
|
|
const fieldContainer = containerEl.createEl("div", {
|
|
|
|
|
|
cls: "mindnet-field",
|
2026-01-16 20:10:08 +01:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-01-16 20:53:02 +01:00
|
|
|
|
// Label
|
2026-01-16 20:10:08 +01:00
|
|
|
|
if (nestedStep.label) {
|
2026-01-16 20:53:02 +01:00
|
|
|
|
const labelEl = fieldContainer.createEl("div", {
|
2026-01-16 20:10:08 +01:00
|
|
|
|
cls: "mindnet-field__label",
|
|
|
|
|
|
text: nestedStep.label,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-16 20:53:02 +01:00
|
|
|
|
// Show item count
|
|
|
|
|
|
const countText = nestedLoopState.items.length > 0
|
|
|
|
|
|
? `${nestedLoopState.items.length} ${nestedLoopState.items.length === 1 ? "Eintrag" : "Einträge"}`
|
|
|
|
|
|
: "Keine Einträge";
|
|
|
|
|
|
const countEl = fieldContainer.createEl("div", {
|
|
|
|
|
|
cls: "mindnet-field__desc",
|
|
|
|
|
|
text: countText,
|
2026-01-16 20:10:08 +01:00
|
|
|
|
});
|
2026-01-16 20:53:02 +01:00
|
|
|
|
countEl.style.marginBottom = "0.5em";
|
2026-01-16 20:10:08 +01:00
|
|
|
|
|
2026-01-16 20:53:02 +01:00
|
|
|
|
// Button to enter nested loop (fullscreen mode)
|
|
|
|
|
|
const enterBtn = fieldContainer.createEl("button", {
|
|
|
|
|
|
text: nestedLoopState.items.length > 0 ? "Bearbeiten" : "Hinzufügen",
|
|
|
|
|
|
cls: "mod-cta",
|
2026-01-16 20:10:08 +01:00
|
|
|
|
});
|
2026-01-16 20:53:02 +01:00
|
|
|
|
enterBtn.style.width = "100%";
|
|
|
|
|
|
enterBtn.onclick = () => {
|
|
|
|
|
|
// Enter nested loop: add to activeLoopPath
|
|
|
|
|
|
this.state.activeLoopPath.push(nestedLoopKey);
|
|
|
|
|
|
this.renderStep();
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Render breadcrumb navigation showing loop hierarchy.
|
|
|
|
|
|
*/
|
|
|
|
|
|
private renderBreadcrumb(containerEl: HTMLElement, currentStep: InterviewStep): void {
|
|
|
|
|
|
const breadcrumbContainer = containerEl.createEl("div", {
|
|
|
|
|
|
cls: "loop-breadcrumb",
|
|
|
|
|
|
});
|
|
|
|
|
|
breadcrumbContainer.style.display = "flex";
|
|
|
|
|
|
breadcrumbContainer.style.alignItems = "center";
|
|
|
|
|
|
breadcrumbContainer.style.gap = "0.5em";
|
|
|
|
|
|
breadcrumbContainer.style.marginBottom = "1em";
|
|
|
|
|
|
breadcrumbContainer.style.padding = "0.5em";
|
|
|
|
|
|
breadcrumbContainer.style.background = "var(--background-secondary)";
|
|
|
|
|
|
breadcrumbContainer.style.borderRadius = "4px";
|
|
|
|
|
|
|
|
|
|
|
|
// Build breadcrumb path
|
|
|
|
|
|
const path: Array<{ key: string; label: string }> = [];
|
|
|
|
|
|
|
|
|
|
|
|
// Find all parent loops
|
|
|
|
|
|
for (let i = 0; i < this.state.activeLoopPath.length; i++) {
|
|
|
|
|
|
const loopKey = this.state.activeLoopPath[i];
|
|
|
|
|
|
if (!loopKey) continue;
|
2026-01-16 20:10:08 +01:00
|
|
|
|
|
2026-01-16 20:53:02 +01:00
|
|
|
|
// Extract step key from loop key (e.g., "items.item_list" -> "item_list")
|
|
|
|
|
|
const parts = loopKey.split(".");
|
|
|
|
|
|
const stepKey = parts[parts.length - 1];
|
|
|
|
|
|
if (!stepKey) continue;
|
|
|
|
|
|
|
|
|
|
|
|
// Find the step in the profile
|
|
|
|
|
|
const step = this.findStepByKey(stepKey);
|
|
|
|
|
|
if (step && step.type === "loop") {
|
|
|
|
|
|
path.push({ key: loopKey, label: step.label || stepKey });
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Render breadcrumb
|
|
|
|
|
|
path.forEach((item, index) => {
|
|
|
|
|
|
if (index > 0) {
|
|
|
|
|
|
breadcrumbContainer.createEl("span", { text: "›" });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const breadcrumbItem = breadcrumbContainer.createEl("button", {
|
|
|
|
|
|
text: item.label,
|
|
|
|
|
|
cls: "breadcrumb-item",
|
2026-01-16 20:10:08 +01:00
|
|
|
|
});
|
2026-01-16 20:53:02 +01:00
|
|
|
|
breadcrumbItem.style.background = "transparent";
|
|
|
|
|
|
breadcrumbItem.style.border = "none";
|
|
|
|
|
|
breadcrumbItem.style.cursor = "pointer";
|
|
|
|
|
|
breadcrumbItem.style.textDecoration = index < path.length - 1 ? "underline" : "none";
|
2026-01-16 20:10:08 +01:00
|
|
|
|
|
2026-01-16 20:53:02 +01:00
|
|
|
|
if (index < path.length - 1) {
|
|
|
|
|
|
breadcrumbItem.onclick = () => {
|
|
|
|
|
|
// Navigate to this level
|
|
|
|
|
|
this.state.activeLoopPath = this.state.activeLoopPath.slice(0, index + 1);
|
|
|
|
|
|
this.renderStep();
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Back button to parent level
|
|
|
|
|
|
if (this.state.activeLoopPath.length > 0) {
|
|
|
|
|
|
const backBtn = breadcrumbContainer.createEl("button", {
|
|
|
|
|
|
text: "← Zurück",
|
|
|
|
|
|
cls: "mod-cta",
|
|
|
|
|
|
});
|
|
|
|
|
|
backBtn.style.marginLeft = "auto";
|
|
|
|
|
|
backBtn.onclick = () => {
|
|
|
|
|
|
this.state.activeLoopPath.pop();
|
|
|
|
|
|
this.renderStep();
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Find a step by its key in the profile (recursive search).
|
|
|
|
|
|
*/
|
|
|
|
|
|
private findStepByKey(key: string): InterviewStep | null {
|
|
|
|
|
|
const searchInSteps = (steps: InterviewStep[]): InterviewStep | null => {
|
|
|
|
|
|
for (const step of steps) {
|
|
|
|
|
|
if (step.key === key) {
|
|
|
|
|
|
return step;
|
2026-01-16 20:10:08 +01:00
|
|
|
|
}
|
2026-01-16 20:53:02 +01:00
|
|
|
|
if (step.type === "loop") {
|
|
|
|
|
|
const found = searchInSteps(step.items);
|
|
|
|
|
|
if (found) return found;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return null;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
return searchInSteps(this.state.profile.steps);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Render a nested loop in fullscreen mode (uses full width).
|
|
|
|
|
|
*/
|
|
|
|
|
|
private renderNestedLoopFullscreen(
|
|
|
|
|
|
parentStep: InterviewStep,
|
|
|
|
|
|
containerEl: HTMLElement,
|
|
|
|
|
|
nestedLoopKey: string
|
|
|
|
|
|
): void {
|
|
|
|
|
|
// Extract the nested step from parent step
|
|
|
|
|
|
const nestedStepKey = nestedLoopKey.split(".").pop();
|
|
|
|
|
|
if (!nestedStepKey) return;
|
|
|
|
|
|
|
|
|
|
|
|
// parentStep must be a LoopStep to have items
|
|
|
|
|
|
if (parentStep.type !== "loop") return;
|
|
|
|
|
|
|
|
|
|
|
|
const loopStep = parentStep as LoopStep;
|
|
|
|
|
|
const nestedStep = loopStep.items.find((s: InterviewStep) => s.key === nestedStepKey);
|
|
|
|
|
|
if (!nestedStep || nestedStep.type !== "loop") return;
|
|
|
|
|
|
|
|
|
|
|
|
// Get nested loop state
|
|
|
|
|
|
let nestedLoopState = this.state.loopRuntimeStates.get(nestedLoopKey);
|
|
|
|
|
|
if (!nestedLoopState) {
|
|
|
|
|
|
nestedLoopState = {
|
|
|
|
|
|
items: [],
|
|
|
|
|
|
draft: {},
|
|
|
|
|
|
editIndex: null,
|
|
|
|
|
|
activeItemStepIndex: 0,
|
|
|
|
|
|
};
|
|
|
|
|
|
this.state.loopRuntimeStates.set(nestedLoopKey, nestedLoopState);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Breadcrumb
|
|
|
|
|
|
this.renderBreadcrumb(containerEl, nestedStep);
|
|
|
|
|
|
|
|
|
|
|
|
// Context header: Show parent loop item context
|
|
|
|
|
|
const contextHeader = containerEl.createEl("div", {
|
|
|
|
|
|
cls: "nested-loop-context",
|
|
|
|
|
|
});
|
|
|
|
|
|
contextHeader.style.padding = "1em";
|
|
|
|
|
|
contextHeader.style.background = "var(--background-secondary)";
|
|
|
|
|
|
contextHeader.style.borderRadius = "6px";
|
|
|
|
|
|
contextHeader.style.marginBottom = "1.5em";
|
|
|
|
|
|
contextHeader.style.border = "1px solid var(--background-modifier-border)";
|
|
|
|
|
|
|
|
|
|
|
|
// Find parent loop context
|
|
|
|
|
|
const lastDotIndex = nestedLoopKey.lastIndexOf(".");
|
|
|
|
|
|
if (lastDotIndex > 0) {
|
|
|
|
|
|
const parentLoopKey = nestedLoopKey.substring(0, lastDotIndex);
|
|
|
|
|
|
const parentLoopState = this.state.loopRuntimeStates.get(parentLoopKey);
|
|
|
|
|
|
|
|
|
|
|
|
if (parentLoopState) {
|
|
|
|
|
|
// Find the top-level loop step
|
|
|
|
|
|
const topLevelLoopKey = nestedLoopKey.split(".")[0];
|
|
|
|
|
|
if (!topLevelLoopKey) {
|
|
|
|
|
|
// Fallback if no top-level key found
|
|
|
|
|
|
const contextTitle = contextHeader.createEl("div", {
|
|
|
|
|
|
cls: "context-title",
|
|
|
|
|
|
text: "📍 Kontext: " + (parentStep.label || "Parent Loop"),
|
|
|
|
|
|
});
|
|
|
|
|
|
contextTitle.style.fontWeight = "bold";
|
|
|
|
|
|
contextTitle.style.marginBottom = "0.5em";
|
|
|
|
|
|
contextTitle.style.fontSize = "0.9em";
|
|
|
|
|
|
contextTitle.style.color = "var(--text-muted)";
|
|
|
|
|
|
} else {
|
|
|
|
|
|
const topLevelLoopState = this.state.loopRuntimeStates.get(topLevelLoopKey);
|
|
|
|
|
|
const topLevelStep = this.findStepByKey(topLevelLoopKey);
|
|
|
|
|
|
|
|
|
|
|
|
// Build context path: top-level loop > nested loop (current)
|
|
|
|
|
|
const contextPath: string[] = [];
|
|
|
|
|
|
if (topLevelStep && topLevelStep.type === "loop") {
|
|
|
|
|
|
contextPath.push(topLevelStep.label || topLevelLoopKey);
|
2026-01-16 20:10:08 +01:00
|
|
|
|
}
|
2026-01-16 20:53:02 +01:00
|
|
|
|
// Add the nested loop label (the one we're currently in)
|
|
|
|
|
|
if (nestedStep && nestedStep.type === "loop") {
|
|
|
|
|
|
contextPath.push(nestedStep.label || nestedStepKey || "");
|
2026-01-16 20:10:08 +01:00
|
|
|
|
}
|
2026-01-16 20:53:02 +01:00
|
|
|
|
|
|
|
|
|
|
const contextTitle = contextHeader.createEl("div", {
|
|
|
|
|
|
cls: "context-title",
|
|
|
|
|
|
});
|
|
|
|
|
|
contextTitle.style.fontWeight = "bold";
|
|
|
|
|
|
contextTitle.style.marginBottom = "0.5em";
|
|
|
|
|
|
contextTitle.style.fontSize = "0.9em";
|
|
|
|
|
|
contextTitle.style.color = "var(--text-muted)";
|
|
|
|
|
|
|
|
|
|
|
|
if (contextPath.length > 0) {
|
|
|
|
|
|
contextTitle.textContent = "📍 Kontext: " + contextPath.join(" › ");
|
|
|
|
|
|
} else {
|
|
|
|
|
|
contextTitle.textContent = "📍 Kontext: " + (parentStep.label || "Parent Loop");
|
2026-01-16 20:10:08 +01:00
|
|
|
|
}
|
2026-01-16 20:53:02 +01:00
|
|
|
|
}
|
2026-01-16 20:10:08 +01:00
|
|
|
|
|
2026-01-16 20:53:02 +01:00
|
|
|
|
// Show which parent item we're editing
|
|
|
|
|
|
if (parentLoopState.editIndex !== null) {
|
|
|
|
|
|
const parentItem = parentLoopState.items[parentLoopState.editIndex];
|
|
|
|
|
|
if (parentItem && typeof parentItem === "object") {
|
|
|
|
|
|
const parentItemObj = parentItem as Record<string, unknown>;
|
|
|
|
|
|
const contextInfo = contextHeader.createEl("div", {
|
|
|
|
|
|
cls: "context-info",
|
|
|
|
|
|
});
|
|
|
|
|
|
contextInfo.style.fontSize = "0.85em";
|
|
|
|
|
|
contextInfo.style.color = "var(--text-normal)";
|
|
|
|
|
|
|
|
|
|
|
|
// Show parent item fields (excluding the nested loop field itself)
|
|
|
|
|
|
const parentFields: string[] = [];
|
|
|
|
|
|
for (const [key, value] of Object.entries(parentItemObj)) {
|
|
|
|
|
|
if (nestedStepKey && key !== nestedStepKey && value && typeof value === "string" && value.trim() !== "") {
|
|
|
|
|
|
const step = loopStep.items.find(s => s.key === key);
|
|
|
|
|
|
const label = step?.label || key;
|
|
|
|
|
|
parentFields.push(`${label}: ${value.trim().substring(0, 60)}${value.trim().length > 60 ? "..." : ""}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (parentFields.length > 0) {
|
|
|
|
|
|
contextInfo.textContent = `Item ${parentLoopState.editIndex + 1} - ${parentFields.join(" | ")}`;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
contextInfo.textContent = `Item ${parentLoopState.editIndex + 1} (bearbeiten)`;
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
const contextInfo = contextHeader.createEl("div", {
|
|
|
|
|
|
cls: "context-info",
|
|
|
|
|
|
text: `Item ${parentLoopState.editIndex + 1} (bearbeiten)`,
|
|
|
|
|
|
});
|
|
|
|
|
|
contextInfo.style.fontSize = "0.85em";
|
|
|
|
|
|
contextInfo.style.color = "var(--text-normal)";
|
2026-01-16 20:10:08 +01:00
|
|
|
|
}
|
2026-01-16 20:53:02 +01:00
|
|
|
|
} else {
|
|
|
|
|
|
// New item in parent loop
|
|
|
|
|
|
const contextInfo = contextHeader.createEl("div", {
|
|
|
|
|
|
cls: "context-info",
|
|
|
|
|
|
text: "Neues Item (wird erstellt)",
|
|
|
|
|
|
});
|
|
|
|
|
|
contextInfo.style.fontSize = "0.85em";
|
|
|
|
|
|
contextInfo.style.color = "var(--text-muted)";
|
|
|
|
|
|
contextInfo.style.fontStyle = "italic";
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Title
|
|
|
|
|
|
containerEl.createEl("h2", {
|
|
|
|
|
|
text: nestedStep.label || "Verschachtelter Loop",
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Show editing indicator
|
|
|
|
|
|
if (nestedLoopState.editIndex !== null) {
|
|
|
|
|
|
const indicator = containerEl.createEl("div", {
|
|
|
|
|
|
cls: "loop-editing-indicator",
|
|
|
|
|
|
text: `✏️ Editing item ${nestedLoopState.editIndex + 1}`,
|
2026-01-16 20:10:08 +01:00
|
|
|
|
});
|
2026-01-16 20:53:02 +01:00
|
|
|
|
indicator.style.padding = "0.5em";
|
|
|
|
|
|
indicator.style.background = "var(--background-modifier-border-hover)";
|
|
|
|
|
|
indicator.style.borderRadius = "4px";
|
|
|
|
|
|
indicator.style.marginBottom = "1em";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Full-width 2-pane container
|
|
|
|
|
|
const paneContainer = containerEl.createEl("div", {
|
|
|
|
|
|
cls: "loop-pane-container",
|
|
|
|
|
|
});
|
|
|
|
|
|
paneContainer.style.display = "flex";
|
|
|
|
|
|
paneContainer.style.gap = "1em";
|
|
|
|
|
|
paneContainer.style.width = "100%";
|
|
|
|
|
|
|
|
|
|
|
|
// Left pane: Items list (30% for fullscreen mode)
|
|
|
|
|
|
const leftPane = paneContainer.createEl("div", {
|
|
|
|
|
|
cls: "loop-items-pane",
|
|
|
|
|
|
});
|
|
|
|
|
|
leftPane.style.width = "30%";
|
|
|
|
|
|
leftPane.style.borderRight = "1px solid var(--background-modifier-border)";
|
|
|
|
|
|
leftPane.style.paddingRight = "1em";
|
|
|
|
|
|
leftPane.style.maxHeight = "70vh";
|
|
|
|
|
|
leftPane.style.overflowY = "auto";
|
|
|
|
|
|
|
|
|
|
|
|
const itemsTitle = leftPane.createEl("h3", {
|
|
|
|
|
|
text: "Einträge",
|
|
|
|
|
|
});
|
|
|
|
|
|
itemsTitle.style.marginBottom = "0.5em";
|
|
|
|
|
|
|
|
|
|
|
|
// Render items list (same as normal loop)
|
|
|
|
|
|
nestedLoopState.items.forEach((item, i) => {
|
|
|
|
|
|
const itemEl = leftPane.createEl("div", {
|
|
|
|
|
|
cls: "loop-item",
|
2026-01-16 20:10:08 +01:00
|
|
|
|
});
|
2026-01-16 20:53:02 +01:00
|
|
|
|
itemEl.style.padding = "0.75em";
|
|
|
|
|
|
itemEl.style.marginBottom = "0.5em";
|
|
|
|
|
|
itemEl.style.background = "var(--background-secondary)";
|
|
|
|
|
|
itemEl.style.borderRadius = "4px";
|
|
|
|
|
|
itemEl.style.cursor = "pointer";
|
2026-01-16 20:10:08 +01:00
|
|
|
|
|
2026-01-16 20:53:02 +01:00
|
|
|
|
// Extract first non-empty field value for display
|
|
|
|
|
|
let itemText = `Item ${i + 1}`;
|
|
|
|
|
|
if (typeof item === "object" && item !== null) {
|
|
|
|
|
|
const itemObj = item as Record<string, unknown>;
|
|
|
|
|
|
for (const [key, value] of Object.entries(itemObj)) {
|
|
|
|
|
|
if (value && typeof value === "string" && value.trim() !== "") {
|
|
|
|
|
|
itemText = value.trim();
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if (item) {
|
|
|
|
|
|
itemText = String(item);
|
|
|
|
|
|
}
|
|
|
|
|
|
itemEl.textContent = itemText.length > 50 ? itemText.substring(0, 50) + "..." : itemText;
|
|
|
|
|
|
|
|
|
|
|
|
// Action buttons (same as normal loop)
|
|
|
|
|
|
const buttonContainer = itemEl.createEl("div", {
|
|
|
|
|
|
cls: "loop-item-actions",
|
2026-01-16 20:10:08 +01:00
|
|
|
|
});
|
2026-01-16 20:53:02 +01:00
|
|
|
|
buttonContainer.style.display = "flex";
|
|
|
|
|
|
buttonContainer.style.gap = "0.25em";
|
|
|
|
|
|
buttonContainer.style.marginTop = "0.5em";
|
|
|
|
|
|
buttonContainer.style.justifyContent = "flex-end";
|
|
|
|
|
|
|
|
|
|
|
|
const editBtn = buttonContainer.createEl("button", { text: "✏️ Edit" });
|
|
|
|
|
|
editBtn.onclick = () => {
|
|
|
|
|
|
let newState = startEdit(nestedLoopState!, i);
|
|
|
|
|
|
newState = resetItemWizard(newState);
|
|
|
|
|
|
this.state.loopRuntimeStates.set(nestedLoopKey, newState);
|
|
|
|
|
|
this.renderStep();
|
|
|
|
|
|
};
|
2026-01-16 20:10:08 +01:00
|
|
|
|
|
2026-01-16 20:53:02 +01:00
|
|
|
|
const moveUpBtn = buttonContainer.createEl("button", { text: "↑" });
|
|
|
|
|
|
moveUpBtn.disabled = i === 0;
|
|
|
|
|
|
moveUpBtn.onclick = () => {
|
|
|
|
|
|
const newState = moveItemUp(nestedLoopState!, i);
|
|
|
|
|
|
this.state.loopRuntimeStates.set(nestedLoopKey, newState);
|
|
|
|
|
|
this.renderStep();
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const moveDownBtn = buttonContainer.createEl("button", { text: "↓" });
|
|
|
|
|
|
moveDownBtn.disabled = i >= nestedLoopState.items.length - 1;
|
|
|
|
|
|
moveDownBtn.onclick = () => {
|
|
|
|
|
|
const newState = moveItemDown(nestedLoopState!, i);
|
|
|
|
|
|
this.state.loopRuntimeStates.set(nestedLoopKey, newState);
|
|
|
|
|
|
this.renderStep();
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const deleteBtn = buttonContainer.createEl("button", { text: "🗑️" });
|
|
|
|
|
|
deleteBtn.onclick = () => {
|
|
|
|
|
|
const newState = deleteItem(nestedLoopState!, i);
|
|
|
|
|
|
this.state.loopRuntimeStates.set(nestedLoopKey, newState);
|
|
|
|
|
|
this.renderStep();
|
|
|
|
|
|
};
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Right pane: Editor (70% for fullscreen mode)
|
|
|
|
|
|
const rightPane = paneContainer.createEl("div", {
|
|
|
|
|
|
cls: "loop-editor-pane",
|
|
|
|
|
|
});
|
|
|
|
|
|
rightPane.style.width = "70%";
|
|
|
|
|
|
rightPane.style.flex = "1";
|
|
|
|
|
|
|
|
|
|
|
|
// Subwizard header
|
|
|
|
|
|
const itemTitle = rightPane.createEl("h3");
|
|
|
|
|
|
const itemTitleText = nestedLoopState.editIndex !== null
|
|
|
|
|
|
? `Item: ${nestedLoopState.editIndex + 1} (editing)`
|
|
|
|
|
|
: "Item: New";
|
|
|
|
|
|
const stepCounter = nestedStep.items.length > 0
|
|
|
|
|
|
? ` - Step ${nestedLoopState.activeItemStepIndex + 1}/${nestedStep.items.length}`
|
|
|
|
|
|
: "";
|
|
|
|
|
|
itemTitle.textContent = itemTitleText + stepCounter;
|
|
|
|
|
|
|
|
|
|
|
|
// Render active nested step
|
|
|
|
|
|
if (nestedStep.items.length > 0) {
|
|
|
|
|
|
const activeStepIndex = Math.min(nestedLoopState.activeItemStepIndex, nestedStep.items.length - 1);
|
|
|
|
|
|
const activeNestedStep = nestedStep.items[activeStepIndex];
|
|
|
|
|
|
|
|
|
|
|
|
if (activeNestedStep) {
|
|
|
|
|
|
const editorContainer = rightPane.createEl("div", {
|
|
|
|
|
|
cls: "loop-item-editor",
|
|
|
|
|
|
});
|
2026-01-16 20:10:08 +01:00
|
|
|
|
|
2026-01-16 20:53:02 +01:00
|
|
|
|
const draftValue = nestedLoopState.draft[activeNestedStep.key];
|
|
|
|
|
|
this.renderLoopNestedStep(
|
|
|
|
|
|
activeNestedStep,
|
|
|
|
|
|
editorContainer,
|
|
|
|
|
|
nestedLoopKey,
|
|
|
|
|
|
draftValue,
|
|
|
|
|
|
(fieldId, value) => {
|
|
|
|
|
|
const currentState = this.state.loopRuntimeStates.get(nestedLoopKey);
|
|
|
|
|
|
if (currentState) {
|
|
|
|
|
|
const newState = setDraftField(currentState, fieldId, value);
|
|
|
|
|
|
this.state.loopRuntimeStates.set(nestedLoopKey, newState);
|
|
|
|
|
|
// Update parent draft
|
|
|
|
|
|
const lastDotIndex = nestedLoopKey.lastIndexOf(".");
|
|
|
|
|
|
if (lastDotIndex > 0) {
|
|
|
|
|
|
const parentLoopKey = nestedLoopKey.substring(0, lastDotIndex);
|
|
|
|
|
|
if (parentLoopKey) {
|
|
|
|
|
|
const parentState = this.state.loopRuntimeStates.get(parentLoopKey);
|
|
|
|
|
|
if (parentState && nestedStepKey) {
|
|
|
|
|
|
const updatedParentDraft = {
|
|
|
|
|
|
...parentState.draft,
|
|
|
|
|
|
[nestedStepKey]: newState.items,
|
|
|
|
|
|
};
|
|
|
|
|
|
const updatedParentState = setDraftField(parentState, nestedStepKey, newState.items);
|
|
|
|
|
|
this.state.loopRuntimeStates.set(parentLoopKey, updatedParentState);
|
|
|
|
|
|
}
|
2026-01-16 20:10:08 +01:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-16 20:53:02 +01:00
|
|
|
|
}
|
|
|
|
|
|
);
|
2026-01-16 20:10:08 +01:00
|
|
|
|
|
2026-01-16 20:53:02 +01:00
|
|
|
|
// Navigation buttons (same as normal loop)
|
|
|
|
|
|
const subwizardNav = rightPane.createEl("div", {
|
|
|
|
|
|
cls: "loop-subwizard-navigation",
|
2026-01-16 20:10:08 +01:00
|
|
|
|
});
|
2026-01-16 20:53:02 +01:00
|
|
|
|
subwizardNav.style.display = "flex";
|
|
|
|
|
|
subwizardNav.style.gap = "0.5em";
|
|
|
|
|
|
subwizardNav.style.marginTop = "1em";
|
|
|
|
|
|
subwizardNav.style.justifyContent = "space-between";
|
2026-01-16 20:10:08 +01:00
|
|
|
|
|
2026-01-16 20:53:02 +01:00
|
|
|
|
const itemNavLeft = subwizardNav.createEl("div");
|
|
|
|
|
|
itemNavLeft.style.display = "flex";
|
|
|
|
|
|
itemNavLeft.style.gap = "0.5em";
|
2026-01-16 20:10:08 +01:00
|
|
|
|
|
2026-01-16 20:53:02 +01:00
|
|
|
|
const itemBackBtn = itemNavLeft.createEl("button", { text: "← Item Back" });
|
|
|
|
|
|
itemBackBtn.disabled = activeStepIndex === 0;
|
|
|
|
|
|
itemBackBtn.onclick = () => {
|
|
|
|
|
|
const currentState = this.state.loopRuntimeStates.get(nestedLoopKey);
|
|
|
|
|
|
if (currentState) {
|
|
|
|
|
|
const newState = itemPrevStep(currentState);
|
2026-01-16 20:10:08 +01:00
|
|
|
|
this.state.loopRuntimeStates.set(nestedLoopKey, newState);
|
|
|
|
|
|
this.renderStep();
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-01-16 20:53:02 +01:00
|
|
|
|
const itemNextBtn = itemNavLeft.createEl("button", { text: "Item Next →" });
|
|
|
|
|
|
itemNextBtn.disabled = activeStepIndex >= nestedStep.items.length - 1;
|
|
|
|
|
|
itemNextBtn.onclick = () => {
|
|
|
|
|
|
const currentState = this.state.loopRuntimeStates.get(nestedLoopKey);
|
|
|
|
|
|
if (currentState) {
|
|
|
|
|
|
const newState = itemNextStep(currentState, nestedStep.items.length);
|
2026-01-16 20:10:08 +01:00
|
|
|
|
this.state.loopRuntimeStates.set(nestedLoopKey, newState);
|
|
|
|
|
|
this.renderStep();
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-01-16 20:53:02 +01:00
|
|
|
|
const itemNavRight = subwizardNav.createEl("div");
|
|
|
|
|
|
itemNavRight.style.display = "flex";
|
|
|
|
|
|
itemNavRight.style.gap = "0.5em";
|
2026-01-16 20:10:08 +01:00
|
|
|
|
|
2026-01-16 20:53:02 +01:00
|
|
|
|
const doneBtn = itemNavRight.createEl("button", {
|
|
|
|
|
|
text: nestedLoopState.editIndex !== null ? "Save Item" : "Done",
|
2026-01-16 20:10:08 +01:00
|
|
|
|
cls: "mod-cta",
|
|
|
|
|
|
});
|
2026-01-16 20:53:02 +01:00
|
|
|
|
doneBtn.onclick = () => {
|
|
|
|
|
|
const currentState = this.state.loopRuntimeStates.get(nestedLoopKey);
|
|
|
|
|
|
if (currentState && isDraftDirty(currentState.draft)) {
|
|
|
|
|
|
const newState = commitDraft(currentState);
|
2026-01-16 20:10:08 +01:00
|
|
|
|
this.state.loopRuntimeStates.set(nestedLoopKey, newState);
|
|
|
|
|
|
// Update parent draft
|
2026-01-16 20:53:02 +01:00
|
|
|
|
const lastDotIndex = nestedLoopKey.lastIndexOf(".");
|
|
|
|
|
|
if (lastDotIndex > 0 && nestedStepKey) {
|
|
|
|
|
|
const parentLoopKey = nestedLoopKey.substring(0, lastDotIndex);
|
|
|
|
|
|
if (parentLoopKey) {
|
|
|
|
|
|
const parentState = this.state.loopRuntimeStates.get(parentLoopKey);
|
|
|
|
|
|
if (parentState) {
|
|
|
|
|
|
const updatedParentState = setDraftField(parentState, nestedStepKey, newState.items);
|
|
|
|
|
|
this.state.loopRuntimeStates.set(parentLoopKey, updatedParentState);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-16 20:10:08 +01:00
|
|
|
|
this.renderStep();
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
if (nestedLoopState.editIndex !== null || isDraftDirty(nestedLoopState.draft)) {
|
2026-01-16 20:53:02 +01:00
|
|
|
|
const clearBtn = itemNavRight.createEl("button", { text: "Clear" });
|
|
|
|
|
|
clearBtn.onclick = () => {
|
|
|
|
|
|
const currentState = this.state.loopRuntimeStates.get(nestedLoopKey);
|
|
|
|
|
|
if (currentState) {
|
|
|
|
|
|
const newState = clearDraft(currentState);
|
2026-01-16 20:10:08 +01:00
|
|
|
|
this.state.loopRuntimeStates.set(nestedLoopKey, newState);
|
|
|
|
|
|
this.renderStep();
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-16 12:27:44 +01:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
renderLLMDialogStep(step: InterviewStep, containerEl: HTMLElement): void {
|
|
|
|
|
|
if (step.type !== "llm_dialog") return;
|
|
|
|
|
|
|
2026-01-16 14:11:04 +01:00
|
|
|
|
// Field container with vertical layout
|
|
|
|
|
|
const fieldContainer = containerEl.createEl("div", {
|
|
|
|
|
|
cls: "mindnet-field",
|
2026-01-16 12:27:44 +01:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-01-16 14:11:04 +01:00
|
|
|
|
// Label
|
|
|
|
|
|
if (step.label) {
|
|
|
|
|
|
const labelEl = fieldContainer.createEl("div", {
|
|
|
|
|
|
cls: "mindnet-field__label",
|
|
|
|
|
|
text: step.label,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Description/Prompt
|
|
|
|
|
|
if (step.prompt) {
|
|
|
|
|
|
const descEl = fieldContainer.createEl("div", {
|
|
|
|
|
|
cls: "mindnet-field__desc",
|
|
|
|
|
|
text: `Prompt: ${step.prompt}`,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2026-01-16 12:27:44 +01:00
|
|
|
|
|
|
|
|
|
|
const existingValue =
|
|
|
|
|
|
(this.state.collectedData.get(step.key) as string) || "";
|
|
|
|
|
|
|
2026-01-16 14:11:04 +01:00
|
|
|
|
// Editor container for LLM response
|
|
|
|
|
|
const llmEditorContainer = fieldContainer.createEl("div", {
|
|
|
|
|
|
cls: "markdown-editor-wrapper",
|
|
|
|
|
|
});
|
|
|
|
|
|
llmEditorContainer.style.width = "100%";
|
|
|
|
|
|
|
|
|
|
|
|
const llmSetting = new Setting(llmEditorContainer);
|
|
|
|
|
|
llmSetting.settingEl.style.width = "100%";
|
|
|
|
|
|
llmSetting.controlEl.style.width = "100%";
|
|
|
|
|
|
|
|
|
|
|
|
// Hide the default label from Setting component
|
|
|
|
|
|
const settingNameEl = llmSetting.settingEl.querySelector(".setting-item-name") as HTMLElement;
|
|
|
|
|
|
if (settingNameEl) {
|
|
|
|
|
|
settingNameEl.style.display = "none";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let llmTextareaRef: HTMLTextAreaElement | null = null;
|
|
|
|
|
|
|
|
|
|
|
|
llmSetting.addTextArea((text) => {
|
|
|
|
|
|
llmTextareaRef = text.inputEl;
|
|
|
|
|
|
text.setValue(existingValue);
|
|
|
|
|
|
this.currentInputValues.set(step.key, existingValue);
|
|
|
|
|
|
|
|
|
|
|
|
text.onChange((value) => {
|
|
|
|
|
|
this.currentInputValues.set(step.key, value);
|
|
|
|
|
|
this.state.collectedData.set(step.key, value);
|
2026-01-16 12:27:44 +01:00
|
|
|
|
});
|
2026-01-16 14:11:04 +01:00
|
|
|
|
text.inputEl.rows = 10;
|
|
|
|
|
|
text.inputEl.style.width = "100%";
|
|
|
|
|
|
text.inputEl.style.minHeight = "240px";
|
|
|
|
|
|
text.inputEl.style.boxSizing = "border-box";
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Add toolbar for LLM response
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
const textarea = llmEditorContainer.querySelector("textarea");
|
|
|
|
|
|
if (textarea) {
|
2026-01-17 06:28:36 +01:00
|
|
|
|
const llmToolbar = createMarkdownToolbar(
|
|
|
|
|
|
textarea,
|
|
|
|
|
|
undefined,
|
|
|
|
|
|
(app: App) => {
|
|
|
|
|
|
// Open entity picker modal for LLM dialog
|
|
|
|
|
|
if (!this.noteIndex) {
|
|
|
|
|
|
new Notice("Note index not available");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
new EntityPickerModal(
|
|
|
|
|
|
this.app,
|
|
|
|
|
|
this.noteIndex,
|
|
|
|
|
|
(result: EntityPickerResult) => {
|
|
|
|
|
|
insertWikilinkIntoTextarea(textarea, result.basename);
|
|
|
|
|
|
}
|
|
|
|
|
|
).open();
|
2026-01-17 22:15:11 +01:00
|
|
|
|
},
|
|
|
|
|
|
async (app: App, textarea: HTMLTextAreaElement) => {
|
|
|
|
|
|
// Edge-Type-Selektor für LLM-Dialog (optional)
|
|
|
|
|
|
const currentStep = getCurrentStep(this.state);
|
|
|
|
|
|
if (currentStep) {
|
|
|
|
|
|
await this.handleEdgeTypeSelectorForTextarea(app, textarea, currentStep, llmEditorContainer);
|
|
|
|
|
|
}
|
2026-01-17 06:28:36 +01:00
|
|
|
|
}
|
|
|
|
|
|
);
|
2026-01-16 14:11:04 +01:00
|
|
|
|
llmEditorContainer.insertBefore(llmToolbar, llmEditorContainer.firstChild);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, 10);
|
2026-01-16 12:27:44 +01:00
|
|
|
|
|
|
|
|
|
|
containerEl.createEl("p", {
|
|
|
|
|
|
text: "Note: LLM dialog requires manual input in this version",
|
|
|
|
|
|
cls: "interview-note",
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
renderReviewStep(step: InterviewStep, containerEl: HTMLElement): void {
|
|
|
|
|
|
if (step.type !== "review") return;
|
|
|
|
|
|
|
|
|
|
|
|
containerEl.createEl("h2", {
|
|
|
|
|
|
text: step.label || "Review",
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
containerEl.createEl("p", {
|
|
|
|
|
|
text: "Review collected data and patches:",
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Show collected data
|
|
|
|
|
|
const dataList = containerEl.createEl("ul");
|
|
|
|
|
|
for (const [key, value] of this.state.collectedData.entries()) {
|
|
|
|
|
|
const li = dataList.createEl("li");
|
|
|
|
|
|
li.createEl("strong", { text: `${key}: ` });
|
|
|
|
|
|
li.createSpan({ text: String(value) });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-16 19:25:55 +01:00
|
|
|
|
// Show loop items (from runtime states)
|
|
|
|
|
|
for (const [loopKey, loopState] of this.state.loopRuntimeStates.entries()) {
|
|
|
|
|
|
const loopLi = dataList.createEl("li");
|
|
|
|
|
|
loopLi.createEl("strong", { text: `${loopKey} (${loopState.items.length} items): ` });
|
|
|
|
|
|
loopLi.createSpan({ text: `${loopState.items.length} committed items` });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-16 12:27:44 +01:00
|
|
|
|
// Show patches
|
|
|
|
|
|
const patchesList = containerEl.createEl("ul");
|
|
|
|
|
|
for (const patch of this.state.patches) {
|
|
|
|
|
|
const li = patchesList.createEl("li");
|
|
|
|
|
|
if (patch.type === "frontmatter") {
|
|
|
|
|
|
li.createSpan({
|
|
|
|
|
|
text: `Frontmatter ${patch.field}: ${String(patch.value)}`,
|
|
|
|
|
|
});
|
|
|
|
|
|
} else {
|
|
|
|
|
|
li.createSpan({ text: `Content patch` });
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
renderNavigation(containerEl: HTMLElement): void {
|
2026-01-16 14:11:04 +01:00
|
|
|
|
// Navigation buttons in a flex row
|
2026-01-16 12:27:44 +01:00
|
|
|
|
const navContainer = containerEl.createEl("div", {
|
|
|
|
|
|
cls: "interview-navigation",
|
|
|
|
|
|
});
|
2026-01-16 14:11:04 +01:00
|
|
|
|
navContainer.style.display = "flex";
|
|
|
|
|
|
navContainer.style.gap = "0.5em";
|
|
|
|
|
|
navContainer.style.justifyContent = "flex-end";
|
|
|
|
|
|
navContainer.style.flexWrap = "wrap";
|
2026-01-16 12:27:44 +01:00
|
|
|
|
|
|
|
|
|
|
// Back button
|
|
|
|
|
|
new Setting(navContainer)
|
|
|
|
|
|
.addButton((button) => {
|
|
|
|
|
|
button.setButtonText("Back").setDisabled(!canGoBack(this.state));
|
|
|
|
|
|
button.onClick(() => {
|
|
|
|
|
|
this.goBack();
|
|
|
|
|
|
});
|
|
|
|
|
|
})
|
|
|
|
|
|
.addButton((button) => {
|
|
|
|
|
|
const step = getCurrentStep(this.state);
|
|
|
|
|
|
const isReview = step?.type === "review";
|
|
|
|
|
|
const isLoop = step?.type === "loop";
|
|
|
|
|
|
|
2026-01-16 19:25:55 +01:00
|
|
|
|
// For loop steps, check if we have items (from runtime state or legacy context)
|
|
|
|
|
|
let loopItems: unknown[] = [];
|
|
|
|
|
|
if (isLoop) {
|
|
|
|
|
|
const loopState = this.state.loopRuntimeStates.get(step.key);
|
|
|
|
|
|
loopItems = loopState ? loopState.items : (this.state.loopContexts.get(step.key) || []);
|
|
|
|
|
|
}
|
2026-01-16 12:27:44 +01:00
|
|
|
|
const canProceedLoop = !isLoop || loopItems.length > 0;
|
|
|
|
|
|
|
|
|
|
|
|
button
|
|
|
|
|
|
.setButtonText(isReview ? "Apply & Finish" : "Next")
|
|
|
|
|
|
.setCta()
|
|
|
|
|
|
.setDisabled((!canGoNext(this.state) && !isReview) || !canProceedLoop);
|
2026-01-17 10:31:44 +01:00
|
|
|
|
button.onClick(async () => {
|
2026-01-16 12:27:44 +01:00
|
|
|
|
if (isReview) {
|
|
|
|
|
|
console.log("=== FINISH WIZARD (Apply & Finish) ===");
|
|
|
|
|
|
// Save current step data before finishing
|
|
|
|
|
|
const currentStep = getCurrentStep(this.state);
|
|
|
|
|
|
if (currentStep) {
|
|
|
|
|
|
this.saveCurrentStepData(currentStep);
|
|
|
|
|
|
}
|
2026-01-17 10:31:44 +01:00
|
|
|
|
|
2026-01-30 12:37:06 +01:00
|
|
|
|
// WP-26: Zeige Übersichts-Modal für Sektions-Edges (nur wenn Sections vorhanden)
|
|
|
|
|
|
let sectionEdgeTypes: Map<string, Map<string, string>> | undefined = undefined;
|
|
|
|
|
|
if (this.state.sectionSequence.length > 1 && this.vocabulary) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const graphSchema = this.plugin?.ensureGraphSchemaLoaded
|
|
|
|
|
|
? await this.plugin.ensureGraphSchemaLoaded()
|
|
|
|
|
|
: null;
|
|
|
|
|
|
|
|
|
|
|
|
const overviewModal = new SectionEdgesOverviewModal(
|
|
|
|
|
|
this.app,
|
|
|
|
|
|
this.state.sectionSequence,
|
|
|
|
|
|
this.vocabulary,
|
|
|
|
|
|
graphSchema,
|
|
|
|
|
|
this.state.collectedData // Übergebe gesammelte Daten für Note-Link-Extraktion
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const result = await overviewModal.show();
|
|
|
|
|
|
|
|
|
|
|
|
if (!result.cancelled && (result.sectionEdges.size > 0 || result.noteEdges.size > 0)) {
|
|
|
|
|
|
sectionEdgeTypes = result.sectionEdges;
|
|
|
|
|
|
console.log("[WP-26] Edge-Types vom Nutzer geändert:", {
|
|
|
|
|
|
sectionEdgesCount: result.sectionEdges.size,
|
|
|
|
|
|
noteEdgesCount: result.noteEdges.size,
|
|
|
|
|
|
sectionEdges: Array.from(result.sectionEdges.entries()).map(([from, toMap]) => ({
|
|
|
|
|
|
from,
|
|
|
|
|
|
to: Array.from(toMap.entries()),
|
|
|
|
|
|
})),
|
|
|
|
|
|
noteEdges: Array.from(result.noteEdges.entries()).map(([from, toMap]) => ({
|
|
|
|
|
|
from,
|
|
|
|
|
|
to: Array.from(toMap.entries()),
|
|
|
|
|
|
})),
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// WP-26: Speichere Note-Edges für späteres Post-Run-Edging
|
|
|
|
|
|
// Diese werden in pendingEdgeAssignments gespeichert
|
|
|
|
|
|
for (const [fromBlockId, toMap] of result.noteEdges.entries()) {
|
|
|
|
|
|
for (const [toNote, edgeType] of toMap.entries()) {
|
|
|
|
|
|
// Finde Section-Key für fromBlockId
|
|
|
|
|
|
const sectionInfo = fromBlockId === "ROOT"
|
|
|
|
|
|
? null
|
|
|
|
|
|
: this.state.sectionSequence.find(s => s.blockId === fromBlockId);
|
|
|
|
|
|
const sectionKey = sectionInfo
|
|
|
|
|
|
? `H${sectionInfo.heading.match(/^#+/)?.length || 2}:${sectionInfo.heading.replace(/^#+\s+/, "")}`
|
|
|
|
|
|
: "ROOT";
|
|
|
|
|
|
|
|
|
|
|
|
this.state.pendingEdgeAssignments.push({
|
|
|
|
|
|
filePath: this.file.path,
|
|
|
|
|
|
sectionKey: sectionKey,
|
|
|
|
|
|
linkBasename: toNote,
|
|
|
|
|
|
chosenRawType: edgeType,
|
|
|
|
|
|
createdAt: Date.now(),
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.warn("[WP-26] Fehler beim Anzeigen des Section-Edges-Übersichts-Modals:", e);
|
|
|
|
|
|
// Continue without section edge types
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
await this.applyPatches(sectionEdgeTypes);
|
|
|
|
|
|
|
|
|
|
|
|
// WP-26: Post-Run-Edging nur noch für Note-Edges (nicht für Section-Edges)
|
|
|
|
|
|
// Section-Edges werden jetzt vollständig über die Übersichtsseite verwaltet
|
|
|
|
|
|
// Note-Edges werden weiterhin über post_run edging verarbeitet
|
2026-01-17 11:54:14 +01:00
|
|
|
|
const edgingMode = this.profile.edging?.mode;
|
2026-01-17 10:31:44 +01:00
|
|
|
|
console.log("[Mindnet] Checking edging mode:", {
|
|
|
|
|
|
profileKey: this.profile.key,
|
2026-01-17 11:54:14 +01:00
|
|
|
|
edgingMode: edgingMode,
|
2026-01-17 10:31:44 +01:00
|
|
|
|
hasEdging: !!this.profile.edging,
|
2026-01-17 11:54:14 +01:00
|
|
|
|
pendingAssignments: this.state.pendingEdgeAssignments.length,
|
2026-01-17 10:31:44 +01:00
|
|
|
|
});
|
2026-01-17 11:54:14 +01:00
|
|
|
|
|
|
|
|
|
|
// Support: post_run, both (inline_micro + post_run)
|
2026-01-30 12:37:06 +01:00
|
|
|
|
// Nur ausführen, wenn Note-Edges vorhanden sind (nicht für Section-Edges)
|
|
|
|
|
|
const shouldRunPostRun = (edgingMode === "post_run" || edgingMode === "both") &&
|
|
|
|
|
|
this.state.pendingEdgeAssignments.length > 0;
|
2026-01-17 11:54:14 +01:00
|
|
|
|
if (shouldRunPostRun) {
|
2026-01-30 12:37:06 +01:00
|
|
|
|
console.log("[Mindnet] Starting post-run edging für Note-Edges");
|
2026-01-17 10:31:44 +01:00
|
|
|
|
await this.runPostRunEdging();
|
|
|
|
|
|
} else {
|
2026-01-30 12:37:06 +01:00
|
|
|
|
console.log("[Mindnet] Post-run edging skipped (mode:", edgingMode || "none", ", pending:", this.state.pendingEdgeAssignments.length, ")");
|
2026-01-17 10:31:44 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-16 12:27:44 +01:00
|
|
|
|
this.onSubmit({ applied: true, patches: this.state.patches });
|
|
|
|
|
|
this.close();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.goNext();
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
})
|
|
|
|
|
|
.addButton((button) => {
|
|
|
|
|
|
button.setButtonText("Skip").onClick(() => {
|
|
|
|
|
|
this.goNext();
|
|
|
|
|
|
});
|
|
|
|
|
|
})
|
|
|
|
|
|
.addButton((button) => {
|
2026-01-30 12:37:06 +01:00
|
|
|
|
button.setButtonText("Save & Exit").onClick(async () => {
|
2026-01-16 12:27:44 +01:00
|
|
|
|
console.log("=== SAVE & EXIT ===");
|
2026-01-30 12:37:06 +01:00
|
|
|
|
await this.applyPatches();
|
2026-01-16 12:27:44 +01:00
|
|
|
|
this.onSaveAndExit({
|
|
|
|
|
|
applied: true,
|
|
|
|
|
|
patches: this.state.patches,
|
|
|
|
|
|
});
|
|
|
|
|
|
this.close();
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-17 11:54:14 +01:00
|
|
|
|
/**
|
|
|
|
|
|
* Handle inline micro edging after entity picker selection.
|
|
|
|
|
|
* Returns the selected edge type, or null if skipped/cancelled.
|
|
|
|
|
|
*/
|
2026-01-17 22:15:11 +01:00
|
|
|
|
/**
|
|
|
|
|
|
* Handle edge type selector for textarea in interview mode.
|
|
|
|
|
|
*/
|
|
|
|
|
|
private async handleEdgeTypeSelectorForTextarea(
|
|
|
|
|
|
app: App,
|
|
|
|
|
|
textarea: HTMLTextAreaElement,
|
|
|
|
|
|
step: InterviewStep,
|
|
|
|
|
|
containerEl: HTMLElement
|
|
|
|
|
|
): Promise<void> {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const content = textarea.value;
|
|
|
|
|
|
const context = detectEdgeSelectorContext(textarea, content);
|
|
|
|
|
|
|
|
|
|
|
|
if (!context) {
|
|
|
|
|
|
new Notice("Kontext konnte nicht erkannt werden");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Update callback to sync with state
|
|
|
|
|
|
const onUpdate = (newContent: string) => {
|
|
|
|
|
|
textarea.value = newContent;
|
|
|
|
|
|
// Update stored value
|
|
|
|
|
|
this.currentInputValues.set(step.key, newContent);
|
|
|
|
|
|
this.state.collectedData.set(step.key, newContent);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// For interview mode, we don't have a file, so pass null
|
|
|
|
|
|
// We'll need to get source/target types differently
|
|
|
|
|
|
if (!this.settings) {
|
|
|
|
|
|
new Notice("Einstellungen nicht verfügbar");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
await changeEdgeTypeForLinks(
|
|
|
|
|
|
app,
|
|
|
|
|
|
textarea,
|
|
|
|
|
|
null, // No file in interview mode
|
|
|
|
|
|
this.settings,
|
|
|
|
|
|
context,
|
|
|
|
|
|
this.plugin?.ensureGraphSchemaLoaded ? { ensureGraphSchemaLoaded: async () => {
|
|
|
|
|
|
if (this.plugin?.ensureGraphSchemaLoaded) {
|
|
|
|
|
|
return await this.plugin.ensureGraphSchemaLoaded();
|
|
|
|
|
|
}
|
|
|
|
|
|
return null;
|
|
|
|
|
|
} } : undefined,
|
|
|
|
|
|
onUpdate
|
|
|
|
|
|
);
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
const msg = e instanceof Error ? e.message : String(e);
|
|
|
|
|
|
new Notice(`Fehler beim Ändern des Edge-Types: ${msg}`);
|
|
|
|
|
|
console.error(e);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-17 11:54:14 +01:00
|
|
|
|
private async handleInlineMicroEdging(
|
|
|
|
|
|
step: InterviewStep,
|
|
|
|
|
|
linkBasename: string,
|
|
|
|
|
|
linkPath: string
|
|
|
|
|
|
): Promise<string | null> {
|
|
|
|
|
|
if (!this.settings) {
|
|
|
|
|
|
console.warn("[Mindnet] Cannot run inline micro edging: settings not provided");
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
// Load vocabulary if not already loaded
|
|
|
|
|
|
if (!this.vocabulary) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const vocabText = await VocabularyLoader.loadText(
|
|
|
|
|
|
this.app,
|
|
|
|
|
|
this.settings.edgeVocabularyPath
|
|
|
|
|
|
);
|
|
|
|
|
|
this.vocabulary = parseEdgeVocabulary(vocabText);
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.warn("[Mindnet] Failed to load vocabulary for inline micro:", e);
|
|
|
|
|
|
// Continue without vocabulary
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Get graph schema
|
|
|
|
|
|
let graphSchema = null;
|
|
|
|
|
|
if (this.plugin?.ensureGraphSchemaLoaded) {
|
|
|
|
|
|
graphSchema = await this.plugin.ensureGraphSchemaLoaded();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-27 11:18:56 +01:00
|
|
|
|
// WP-26: Prüfe, ob es eine Block-ID-Referenz ist ([[#^block-id]])
|
2026-01-17 11:54:14 +01:00
|
|
|
|
let sourceType: string | undefined;
|
2026-01-27 11:18:56 +01:00
|
|
|
|
let targetType: string | undefined;
|
|
|
|
|
|
let sourceNoteId: string | undefined;
|
|
|
|
|
|
let targetNoteId: string | undefined;
|
|
|
|
|
|
|
|
|
|
|
|
const blockIdMatch = linkBasename.match(/^#\^(.+)$/);
|
|
|
|
|
|
if (blockIdMatch && blockIdMatch[1]) {
|
|
|
|
|
|
// Intra-Note-Edge: Block-ID-Referenz
|
|
|
|
|
|
const blockId = blockIdMatch[1];
|
|
|
|
|
|
console.log(`[WP-26] Block-ID-Referenz erkannt: ${blockId}`);
|
|
|
|
|
|
|
|
|
|
|
|
// Finde Source-Section (aktuelle Section aus Step)
|
|
|
|
|
|
const currentStep = step as import("../interview/types").CaptureTextStep | import("../interview/types").CaptureTextLineStep;
|
|
|
|
|
|
let currentBlockId: string | null = null;
|
|
|
|
|
|
if (currentStep.block_id) {
|
|
|
|
|
|
currentBlockId = currentStep.block_id;
|
|
|
|
|
|
} else if (currentStep.generate_block_id) {
|
|
|
|
|
|
currentBlockId = slugify(currentStep.key);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (currentBlockId) {
|
|
|
|
|
|
const sourceSection = this.state.generatedBlockIds.get(currentBlockId);
|
|
|
|
|
|
if (sourceSection) {
|
2026-01-30 12:37:06 +01:00
|
|
|
|
// WP-26: Verwende effektiven Section-Type (mit Heading-Level-basierter Fallback-Logik)
|
|
|
|
|
|
const sectionIndex = this.state.sectionSequence.findIndex(s => s.blockId === currentBlockId);
|
|
|
|
|
|
if (sectionIndex >= 0) {
|
|
|
|
|
|
sourceType = this.getEffectiveSectionType(sourceSection, sectionIndex);
|
|
|
|
|
|
console.log(`[WP-26] Source-Type aus Section (effektiv): ${sourceType}`);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
sourceType = sourceSection.sectionType || sourceSection.noteType;
|
|
|
|
|
|
console.log(`[WP-26] Source-Type aus Section: ${sourceType}`);
|
|
|
|
|
|
}
|
2026-01-27 11:18:56 +01:00
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Fallback: Verwende Note-Type
|
|
|
|
|
|
sourceType = this.state.profile.note_type;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Finde Target-Section (Block-ID aus generatedBlockIds)
|
|
|
|
|
|
const targetSection = this.state.generatedBlockIds.get(blockId);
|
|
|
|
|
|
if (targetSection) {
|
|
|
|
|
|
targetType = targetSection.sectionType || targetSection.noteType;
|
|
|
|
|
|
console.log(`[WP-26] Target-Type aus Section: ${targetType}`);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.warn(`[WP-26] Block-ID "${blockId}" nicht in generatedBlockIds gefunden`);
|
|
|
|
|
|
// Fallback: Verwende Note-Type
|
|
|
|
|
|
targetType = this.state.profile.note_type;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Für Intra-Note-Edges: sourceNoteId = targetNoteId (gleiche Note)
|
|
|
|
|
|
const sourceContent = this.fileContent;
|
|
|
|
|
|
const { extractFrontmatterId } = await import("../parser/parseFrontmatter");
|
|
|
|
|
|
sourceNoteId = extractFrontmatterId(sourceContent) || undefined;
|
|
|
|
|
|
targetNoteId = sourceNoteId; // Gleiche Note
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Inter-Note-Edge: Normale Wikilink-Referenz
|
2026-01-30 12:37:06 +01:00
|
|
|
|
// WP-26: Verwende Section-Type statt Note-Type
|
|
|
|
|
|
const currentStep = step as import("../interview/types").CaptureTextStep | import("../interview/types").CaptureTextLineStep;
|
|
|
|
|
|
let currentBlockId: string | null = null;
|
|
|
|
|
|
if (currentStep.block_id) {
|
|
|
|
|
|
currentBlockId = currentStep.block_id;
|
|
|
|
|
|
} else if (currentStep.generate_block_id) {
|
|
|
|
|
|
currentBlockId = slugify(currentStep.key);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Versuche Section-Type zu ermitteln
|
|
|
|
|
|
if (currentBlockId) {
|
|
|
|
|
|
const sourceSection = this.state.generatedBlockIds.get(currentBlockId);
|
|
|
|
|
|
if (sourceSection) {
|
|
|
|
|
|
// WP-26: Verwende effektiven Section-Type (mit Heading-Level-basierter Fallback-Logik)
|
|
|
|
|
|
const sectionIndex = this.state.sectionSequence.findIndex(s => s.blockId === currentBlockId);
|
|
|
|
|
|
if (sectionIndex >= 0) {
|
|
|
|
|
|
sourceType = this.getEffectiveSectionType(sourceSection, sectionIndex);
|
|
|
|
|
|
console.log(`[WP-26] Source-Type aus Section (effektiv): ${sourceType}`);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
sourceType = sourceSection.sectionType || sourceSection.noteType;
|
|
|
|
|
|
console.log(`[WP-26] Source-Type aus Section: ${sourceType}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Versuche Section-Type aus sectionSequence zu finden (auch wenn keine Block-ID vorhanden)
|
|
|
|
|
|
const sectionInfo = this.state.sectionSequence.find(s => s.stepKey === step.key);
|
|
|
|
|
|
if (sectionInfo) {
|
|
|
|
|
|
const sectionIndex = this.state.sectionSequence.findIndex(s => s.stepKey === step.key);
|
|
|
|
|
|
if (sectionIndex >= 0) {
|
|
|
|
|
|
sourceType = this.getEffectiveSectionType(sectionInfo, sectionIndex);
|
|
|
|
|
|
console.log(`[WP-26] Source-Type aus sectionSequence (effektiv): ${sourceType}`);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
sourceType = sectionInfo.sectionType || sectionInfo.noteType;
|
|
|
|
|
|
console.log(`[WP-26] Source-Type aus sectionSequence: ${sourceType}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Versuche Section-Type aus sectionSequence zu finden (auch wenn keine Block-ID vorhanden)
|
|
|
|
|
|
const sectionInfo = this.state.sectionSequence.find(s => s.stepKey === step.key);
|
|
|
|
|
|
if (sectionInfo) {
|
|
|
|
|
|
const sectionIndex = this.state.sectionSequence.findIndex(s => s.stepKey === step.key);
|
|
|
|
|
|
if (sectionIndex >= 0) {
|
|
|
|
|
|
sourceType = this.getEffectiveSectionType(sectionInfo, sectionIndex);
|
|
|
|
|
|
console.log(`[WP-26] Source-Type aus sectionSequence (effektiv, kein Block-ID): ${sourceType}`);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
sourceType = sectionInfo.sectionType || sectionInfo.noteType;
|
|
|
|
|
|
console.log(`[WP-26] Source-Type aus sectionSequence (kein Block-ID): ${sourceType}`);
|
|
|
|
|
|
}
|
2026-01-27 11:18:56 +01:00
|
|
|
|
}
|
2026-01-17 11:54:14 +01:00
|
|
|
|
}
|
2026-01-30 12:37:06 +01:00
|
|
|
|
|
|
|
|
|
|
// Fallback: Verwende Note-Type aus Frontmatter
|
|
|
|
|
|
if (!sourceType) {
|
|
|
|
|
|
const sourceContent = this.fileContent;
|
|
|
|
|
|
const { extractFrontmatterId } = await import("../parser/parseFrontmatter");
|
|
|
|
|
|
sourceNoteId = extractFrontmatterId(sourceContent) || undefined;
|
|
|
|
|
|
const sourceFrontmatter = sourceContent.match(/^---\n([\s\S]*?)\n---/);
|
|
|
|
|
|
if (sourceFrontmatter && sourceFrontmatter[1]) {
|
|
|
|
|
|
const typeMatch = sourceFrontmatter[1].match(/^type:\s*(.+)$/m);
|
|
|
|
|
|
if (typeMatch && typeMatch[1]) {
|
|
|
|
|
|
sourceType = typeMatch[1].trim();
|
|
|
|
|
|
console.log(`[WP-26] Source-Type aus Frontmatter (Fallback): ${sourceType}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// sourceNoteId bereits setzen, auch wenn Section-Type gefunden wurde
|
|
|
|
|
|
const sourceContent = this.fileContent;
|
|
|
|
|
|
const { extractFrontmatterId } = await import("../parser/parseFrontmatter");
|
|
|
|
|
|
sourceNoteId = extractFrontmatterId(sourceContent) || undefined;
|
|
|
|
|
|
}
|
2026-01-17 11:54:14 +01:00
|
|
|
|
|
2026-01-27 11:18:56 +01:00
|
|
|
|
// Get target note ID and type
|
|
|
|
|
|
try {
|
|
|
|
|
|
const targetFile = this.app.vault.getAbstractFileByPath(linkPath);
|
|
|
|
|
|
if (targetFile && targetFile instanceof TFile) {
|
|
|
|
|
|
const targetContent = await this.app.vault.read(targetFile);
|
|
|
|
|
|
targetNoteId = extractFrontmatterId(targetContent) || undefined;
|
|
|
|
|
|
const targetFrontmatter = targetContent.match(/^---\n([\s\S]*?)\n---/);
|
|
|
|
|
|
if (targetFrontmatter && targetFrontmatter[1]) {
|
|
|
|
|
|
const typeMatch = targetFrontmatter[1].match(/^type:\s*(.+)$/m);
|
|
|
|
|
|
if (typeMatch && typeMatch[1]) {
|
|
|
|
|
|
targetType = typeMatch[1].trim();
|
|
|
|
|
|
}
|
2026-01-17 11:54:14 +01:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-27 11:18:56 +01:00
|
|
|
|
} catch (e) {
|
|
|
|
|
|
// Target note might not exist yet, that's OK
|
|
|
|
|
|
console.debug("[Mindnet] Could not read target note for inline micro:", e);
|
2026-01-17 11:54:14 +01:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Show inline edge type modal
|
|
|
|
|
|
const modal = new InlineEdgeTypeModal(
|
|
|
|
|
|
this.app,
|
|
|
|
|
|
linkBasename,
|
|
|
|
|
|
this.vocabulary,
|
|
|
|
|
|
graphSchema,
|
|
|
|
|
|
this.settings,
|
|
|
|
|
|
sourceNoteId,
|
|
|
|
|
|
targetNoteId,
|
|
|
|
|
|
sourceType,
|
|
|
|
|
|
targetType
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const result: InlineEdgeTypeResult = await modal.show();
|
|
|
|
|
|
|
|
|
|
|
|
// Handle result
|
|
|
|
|
|
if (result.cancelled) {
|
|
|
|
|
|
// Cancel: keep link but no assignment
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (result.chosenRawType) {
|
|
|
|
|
|
console.log("[Mindnet] Selected edge type for inline link:", {
|
|
|
|
|
|
linkBasename,
|
|
|
|
|
|
edgeType: result.chosenRawType,
|
|
|
|
|
|
});
|
|
|
|
|
|
return result.chosenRawType;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Skip: no assignment created
|
|
|
|
|
|
return null;
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
const msg = e instanceof Error ? e.message : String(e);
|
|
|
|
|
|
console.error("[Mindnet] Failed to handle inline micro edging:", e);
|
|
|
|
|
|
new Notice(`Failed to handle inline edge type selection: ${msg}`);
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-17 10:31:44 +01:00
|
|
|
|
/**
|
|
|
|
|
|
* 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,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-01-17 11:54:14 +01:00
|
|
|
|
// Run semantic mapping builder with pending assignments
|
2026-01-17 10:31:44 +01:00
|
|
|
|
const result: BuildResult = await buildSemanticMappings(
|
|
|
|
|
|
this.app,
|
|
|
|
|
|
this.file,
|
|
|
|
|
|
edgingSettings,
|
|
|
|
|
|
false, // allowOverwrite: false (respect existing)
|
2026-01-17 11:54:14 +01:00
|
|
|
|
this.plugin,
|
|
|
|
|
|
{
|
|
|
|
|
|
pendingAssignments: this.state.pendingEdgeAssignments,
|
|
|
|
|
|
}
|
2026-01-17 10:31:44 +01:00
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-16 12:27:44 +01:00
|
|
|
|
goNext(): void {
|
|
|
|
|
|
const currentStep = getCurrentStep(this.state);
|
|
|
|
|
|
|
2026-01-16 19:25:55 +01:00
|
|
|
|
// Handle loop commit mode
|
|
|
|
|
|
if (currentStep && currentStep.type === "loop") {
|
|
|
|
|
|
const loopState = this.state.loopRuntimeStates.get(currentStep.key);
|
|
|
|
|
|
const commitMode = currentStep.ui?.commit || "explicit_add";
|
|
|
|
|
|
|
|
|
|
|
|
// In on_next mode, auto-commit dirty draft
|
|
|
|
|
|
if (commitMode === "on_next" && loopState && isDraftDirty(loopState.draft)) {
|
|
|
|
|
|
const newState = commitDraft(loopState);
|
|
|
|
|
|
this.state.loopRuntimeStates.set(currentStep.key, newState);
|
|
|
|
|
|
// Update answers
|
|
|
|
|
|
this.state.loopContexts.set(currentStep.key, newState.items);
|
|
|
|
|
|
console.log("Auto-committed draft on Next", {
|
|
|
|
|
|
stepKey: currentStep.key,
|
|
|
|
|
|
itemsCount: newState.items.length,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-16 12:27:44 +01:00
|
|
|
|
// Save current step data before navigating
|
|
|
|
|
|
if (currentStep) {
|
|
|
|
|
|
this.saveCurrentStepData(currentStep);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const nextIndex = getNextStepIndex(this.state);
|
|
|
|
|
|
|
|
|
|
|
|
console.log("Navigate: Next", {
|
|
|
|
|
|
fromIndex: this.state.currentStepIndex,
|
|
|
|
|
|
toIndex: nextIndex,
|
|
|
|
|
|
currentStepKey: currentStep?.key,
|
|
|
|
|
|
currentStepType: currentStep?.type,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (nextIndex !== null) {
|
|
|
|
|
|
this.state.stepHistory.push(this.state.currentStepIndex);
|
|
|
|
|
|
this.state.currentStepIndex = nextIndex;
|
|
|
|
|
|
this.renderStep();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.log("Cannot go next: already at last step");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Save data from current step before navigating away.
|
|
|
|
|
|
*/
|
|
|
|
|
|
private saveCurrentStepData(step: InterviewStep): void {
|
|
|
|
|
|
const currentValue = this.currentInputValues.get(step.key);
|
|
|
|
|
|
|
|
|
|
|
|
if (currentValue !== undefined) {
|
|
|
|
|
|
console.log("Save current step data before navigation", {
|
|
|
|
|
|
stepKey: step.key,
|
|
|
|
|
|
stepType: step.type,
|
|
|
|
|
|
value: typeof currentValue === "string"
|
|
|
|
|
|
? (currentValue.length > 50 ? currentValue.substring(0, 50) + "..." : currentValue)
|
|
|
|
|
|
: currentValue,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
this.state.collectedData.set(step.key, currentValue);
|
|
|
|
|
|
|
|
|
|
|
|
// For frontmatter steps, also update patch
|
|
|
|
|
|
if (step.type === "capture_frontmatter" && step.field) {
|
|
|
|
|
|
const existingPatchIndex = this.state.patches.findIndex(
|
|
|
|
|
|
p => p.type === "frontmatter" && p.field === step.field
|
|
|
|
|
|
);
|
|
|
|
|
|
const patch = {
|
|
|
|
|
|
type: "frontmatter" as const,
|
|
|
|
|
|
field: step.field,
|
|
|
|
|
|
value: currentValue,
|
|
|
|
|
|
};
|
|
|
|
|
|
if (existingPatchIndex >= 0) {
|
|
|
|
|
|
this.state.patches[existingPatchIndex] = patch;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.state.patches.push(patch);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
goBack(): void {
|
|
|
|
|
|
const prevIndex = getPreviousStepIndex(this.state);
|
|
|
|
|
|
if (prevIndex !== null) {
|
|
|
|
|
|
this.state.currentStepIndex = prevIndex;
|
|
|
|
|
|
this.state.stepHistory.pop();
|
|
|
|
|
|
this.renderStep();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-17 06:28:36 +01:00
|
|
|
|
renderEntityPickerStep(step: InterviewStep, containerEl: HTMLElement): void {
|
|
|
|
|
|
if (step.type !== "entity_picker") return;
|
|
|
|
|
|
|
|
|
|
|
|
const existingValue = this.state.collectedData.get(step.key) as string | undefined;
|
|
|
|
|
|
const selectedBasename = existingValue || "";
|
|
|
|
|
|
|
|
|
|
|
|
// Field container
|
|
|
|
|
|
const fieldContainer = containerEl.createEl("div", {
|
|
|
|
|
|
cls: "mindnet-field",
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Label
|
|
|
|
|
|
if (step.label) {
|
|
|
|
|
|
const labelEl = fieldContainer.createEl("div", {
|
|
|
|
|
|
cls: "mindnet-field__label",
|
|
|
|
|
|
text: step.label,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Description/Prompt
|
|
|
|
|
|
if (step.prompt) {
|
|
|
|
|
|
const descEl = fieldContainer.createEl("div", {
|
|
|
|
|
|
cls: "mindnet-field__desc",
|
|
|
|
|
|
text: step.prompt,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Input container
|
|
|
|
|
|
const inputContainer = fieldContainer.createEl("div", {
|
|
|
|
|
|
cls: "mindnet-field__input",
|
|
|
|
|
|
});
|
|
|
|
|
|
inputContainer.style.width = "100%";
|
|
|
|
|
|
inputContainer.style.display = "flex";
|
|
|
|
|
|
inputContainer.style.gap = "0.5em";
|
|
|
|
|
|
inputContainer.style.alignItems = "center";
|
|
|
|
|
|
|
|
|
|
|
|
// Readonly display of selected note
|
|
|
|
|
|
const displayEl = inputContainer.createEl("div", {
|
|
|
|
|
|
cls: "entity-picker-display",
|
|
|
|
|
|
});
|
|
|
|
|
|
displayEl.style.flex = "1";
|
|
|
|
|
|
displayEl.style.padding = "0.5em";
|
|
|
|
|
|
displayEl.style.border = "1px solid var(--background-modifier-border)";
|
|
|
|
|
|
displayEl.style.borderRadius = "4px";
|
|
|
|
|
|
displayEl.style.background = "var(--background-secondary)";
|
|
|
|
|
|
displayEl.style.minHeight = "2.5em";
|
|
|
|
|
|
displayEl.style.display = "flex";
|
|
|
|
|
|
displayEl.style.alignItems = "center";
|
|
|
|
|
|
|
|
|
|
|
|
if (selectedBasename) {
|
|
|
|
|
|
displayEl.textContent = `[[${selectedBasename}]]`;
|
|
|
|
|
|
displayEl.style.color = "var(--text-normal)";
|
|
|
|
|
|
} else {
|
|
|
|
|
|
displayEl.textContent = "(No note selected)";
|
|
|
|
|
|
displayEl.style.color = "var(--text-muted)";
|
|
|
|
|
|
displayEl.style.fontStyle = "italic";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Pick button
|
|
|
|
|
|
const pickBtn = inputContainer.createEl("button", {
|
|
|
|
|
|
text: selectedBasename ? "Change…" : "Pick note…",
|
|
|
|
|
|
cls: "mod-cta",
|
|
|
|
|
|
});
|
|
|
|
|
|
pickBtn.style.flexShrink = "0";
|
|
|
|
|
|
pickBtn.onclick = () => {
|
|
|
|
|
|
if (!this.noteIndex) {
|
|
|
|
|
|
new Notice("Note index not available");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
new EntityPickerModal(
|
|
|
|
|
|
this.app,
|
|
|
|
|
|
this.noteIndex,
|
2026-01-17 11:54:14 +01:00
|
|
|
|
async (result: EntityPickerResult) => {
|
|
|
|
|
|
// Check if inline micro edging is enabled
|
|
|
|
|
|
// Support: inline_micro, both (inline_micro + post_run)
|
|
|
|
|
|
const edgingMode = this.profile.edging?.mode;
|
|
|
|
|
|
const shouldRunInlineMicro =
|
|
|
|
|
|
(edgingMode === "inline_micro" || edgingMode === "both") &&
|
|
|
|
|
|
this.settings?.inlineMicroEnabled !== false;
|
|
|
|
|
|
|
|
|
|
|
|
console.log("[Mindnet] Entity picker result:", {
|
|
|
|
|
|
edgingMode,
|
|
|
|
|
|
shouldRunInlineMicro,
|
|
|
|
|
|
inlineMicroEnabled: this.settings?.inlineMicroEnabled,
|
|
|
|
|
|
stepKey: step.key,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
let linkText = `[[${result.basename}]]`;
|
|
|
|
|
|
|
|
|
|
|
|
if (shouldRunInlineMicro) {
|
|
|
|
|
|
console.log("[Mindnet] Starting inline micro edging");
|
|
|
|
|
|
const edgeType = await this.handleInlineMicroEdging(step, result.basename, result.path);
|
|
|
|
|
|
if (edgeType && typeof edgeType === "string") {
|
|
|
|
|
|
// Use [[rel:type|link]] format
|
|
|
|
|
|
linkText = `[[rel:${edgeType}|${result.basename}]]`;
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.log("[Mindnet] Inline micro edging skipped", {
|
|
|
|
|
|
edgingMode,
|
|
|
|
|
|
inlineMicroEnabled: this.settings?.inlineMicroEnabled,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Store link text (with rel: prefix if edge type was selected)
|
|
|
|
|
|
this.state.collectedData.set(step.key, linkText);
|
2026-01-17 06:28:36 +01:00
|
|
|
|
// Optionally store path for future use
|
|
|
|
|
|
this.state.collectedData.set(`${step.key}_path`, result.path);
|
2026-01-17 11:54:14 +01:00
|
|
|
|
|
2026-01-17 06:28:36 +01:00
|
|
|
|
this.renderStep();
|
|
|
|
|
|
}
|
|
|
|
|
|
).open();
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-30 12:37:06 +01:00
|
|
|
|
async applyPatches(sectionEdgeTypes?: Map<string, Map<string, string>>): Promise<void> {
|
2026-01-16 12:27:44 +01:00
|
|
|
|
console.log("=== APPLY PATCHES ===", {
|
|
|
|
|
|
patchCount: this.state.patches.length,
|
|
|
|
|
|
patches: this.state.patches.map(p => ({
|
|
|
|
|
|
type: p.type,
|
|
|
|
|
|
field: p.field,
|
|
|
|
|
|
value: typeof p.value === "string"
|
|
|
|
|
|
? (p.value.length > 50 ? p.value.substring(0, 50) + "..." : p.value)
|
|
|
|
|
|
: p.value,
|
|
|
|
|
|
})),
|
|
|
|
|
|
collectedDataKeys: Array.from(this.state.collectedData.keys()),
|
|
|
|
|
|
loopContexts: Array.from(this.state.loopContexts.entries()).map(([key, items]) => ({
|
|
|
|
|
|
loopKey: key,
|
|
|
|
|
|
itemsCount: items.length,
|
|
|
|
|
|
})),
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
let updatedContent = this.fileContent;
|
|
|
|
|
|
|
|
|
|
|
|
// Apply frontmatter patches
|
|
|
|
|
|
for (const patch of this.state.patches) {
|
|
|
|
|
|
if (patch.type === "frontmatter" && patch.field) {
|
|
|
|
|
|
console.log("Apply frontmatter patch", {
|
|
|
|
|
|
field: patch.field,
|
|
|
|
|
|
value: patch.value,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
updatedContent = this.applyFrontmatterPatch(
|
|
|
|
|
|
updatedContent,
|
|
|
|
|
|
patch.field,
|
|
|
|
|
|
patch.value
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-16 19:25:55 +01:00
|
|
|
|
// Sync loopRuntimeStates to loopContexts for renderer
|
|
|
|
|
|
for (const [loopKey, loopState] of this.state.loopRuntimeStates.entries()) {
|
|
|
|
|
|
this.state.loopContexts.set(loopKey, loopState.items);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-27 11:18:56 +01:00
|
|
|
|
// WP-26: Lade Vocabulary und GraphSchema für automatische Edge-Vorschläge
|
|
|
|
|
|
let vocabulary: Vocabulary | null = null;
|
|
|
|
|
|
let graphSchema: GraphSchema | null = null;
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
// Lade Vocabulary
|
|
|
|
|
|
if (!this.vocabulary && this.settings) {
|
|
|
|
|
|
const vocabText = await VocabularyLoader.loadText(
|
|
|
|
|
|
this.app,
|
|
|
|
|
|
this.settings.edgeVocabularyPath
|
|
|
|
|
|
);
|
|
|
|
|
|
this.vocabulary = parseEdgeVocabulary(vocabText);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (this.vocabulary) {
|
|
|
|
|
|
vocabulary = new Vocabulary(this.vocabulary);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Lade GraphSchema
|
|
|
|
|
|
if (this.plugin?.ensureGraphSchemaLoaded) {
|
|
|
|
|
|
graphSchema = await this.plugin.ensureGraphSchemaLoaded();
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.warn("[WP-26] Fehler beim Laden von Vocabulary/GraphSchema für Renderer:", e);
|
|
|
|
|
|
// Continue without vocabulary/schema - Renderer funktioniert auch ohne
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-16 18:26:01 +01:00
|
|
|
|
// Use renderer to generate markdown from collected data
|
|
|
|
|
|
const answers: RenderAnswers = {
|
|
|
|
|
|
collectedData: this.state.collectedData,
|
|
|
|
|
|
loopContexts: this.state.loopContexts,
|
2026-01-27 11:18:56 +01:00
|
|
|
|
sectionSequence: Array.from(this.state.sectionSequence), // WP-26: Section-Sequenz übergeben
|
2026-01-16 18:26:01 +01:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-01-27 11:18:56 +01:00
|
|
|
|
// WP-26: Render-Optionen für Section-Types und Edge-Vorschläge
|
|
|
|
|
|
const renderOptions: RenderOptions = {
|
|
|
|
|
|
graphSchema: graphSchema,
|
|
|
|
|
|
vocabulary: vocabulary,
|
|
|
|
|
|
noteType: this.state.profile.note_type,
|
2026-01-30 12:37:06 +01:00
|
|
|
|
sectionEdgeTypes: sectionEdgeTypes, // WP-26: Manuell geänderte Edge-Types vom Übersichts-Modal
|
2026-01-27 11:18:56 +01:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-01-30 12:37:06 +01:00
|
|
|
|
// WP-26: Debug-Log für Section-Sequenz
|
|
|
|
|
|
console.log(`[WP-26] Section-Sequenz vor Rendering:`, {
|
|
|
|
|
|
count: this.state.sectionSequence.length,
|
|
|
|
|
|
sections: this.state.sectionSequence.map(s => ({
|
|
|
|
|
|
stepKey: s.stepKey,
|
|
|
|
|
|
blockId: s.blockId,
|
|
|
|
|
|
sectionType: s.sectionType,
|
|
|
|
|
|
heading: s.heading,
|
|
|
|
|
|
})),
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-01-27 11:18:56 +01:00
|
|
|
|
const renderedMarkdown = renderProfileToMarkdown(this.state.profile, answers, renderOptions);
|
2026-01-16 18:26:01 +01:00
|
|
|
|
|
|
|
|
|
|
if (renderedMarkdown.trim()) {
|
|
|
|
|
|
// Append rendered markdown to file
|
|
|
|
|
|
updatedContent = updatedContent.trimEnd() + "\n\n" + renderedMarkdown;
|
|
|
|
|
|
|
|
|
|
|
|
console.log("Apply rendered markdown", {
|
|
|
|
|
|
contentLength: renderedMarkdown.length,
|
|
|
|
|
|
preview: renderedMarkdown.substring(0, 200) + "...",
|
|
|
|
|
|
});
|
2026-01-30 12:37:06 +01:00
|
|
|
|
|
|
|
|
|
|
// WP-26: Debug-Log für generiertes Markdown
|
|
|
|
|
|
console.log(`[WP-26] Vollständiges generiertes Markdown:`, renderedMarkdown);
|
2026-01-16 12:27:44 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Write updated content
|
|
|
|
|
|
console.log("Write file", {
|
|
|
|
|
|
file: this.file.path,
|
|
|
|
|
|
contentLength: updatedContent.length,
|
|
|
|
|
|
contentPreview: updatedContent.substring(0, 200) + "...",
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
await this.app.vault.modify(this.file, updatedContent);
|
|
|
|
|
|
this.fileContent = updatedContent;
|
|
|
|
|
|
|
|
|
|
|
|
console.log("=== PATCHES APPLIED ===");
|
|
|
|
|
|
new Notice("Changes applied");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
applyFrontmatterPatch(
|
|
|
|
|
|
content: string,
|
|
|
|
|
|
field: string,
|
|
|
|
|
|
value: unknown
|
|
|
|
|
|
): string {
|
|
|
|
|
|
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
|
|
|
|
if (!frontmatterMatch || !frontmatterMatch[1]) {
|
|
|
|
|
|
return content;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const frontmatter = frontmatterMatch[1];
|
|
|
|
|
|
const fieldRegex = new RegExp(`^${field}\\s*:.*$`, "m");
|
|
|
|
|
|
|
|
|
|
|
|
let updatedFrontmatter: string;
|
|
|
|
|
|
if (fieldRegex.test(frontmatter)) {
|
|
|
|
|
|
// Update existing field
|
|
|
|
|
|
updatedFrontmatter = frontmatter.replace(
|
|
|
|
|
|
fieldRegex,
|
|
|
|
|
|
`${field}: ${this.formatYamlValue(value)}`
|
|
|
|
|
|
);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Add new field
|
|
|
|
|
|
updatedFrontmatter = `${frontmatter}\n${field}: ${this.formatYamlValue(value)}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return content.replace(
|
|
|
|
|
|
/^---\n([\s\S]*?)\n---/,
|
|
|
|
|
|
`---\n${updatedFrontmatter}\n---`
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
formatYamlValue(value: unknown): string {
|
|
|
|
|
|
if (typeof value === "string") {
|
|
|
|
|
|
if (
|
|
|
|
|
|
value.includes(":") ||
|
|
|
|
|
|
value.includes('"') ||
|
|
|
|
|
|
value.includes("\n") ||
|
|
|
|
|
|
value.trim() !== value
|
|
|
|
|
|
) {
|
|
|
|
|
|
return `"${value.replace(/"/g, '\\"')}"`;
|
|
|
|
|
|
}
|
|
|
|
|
|
return value;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (typeof value === "number" || typeof value === "boolean") {
|
|
|
|
|
|
return String(value);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (value === null || value === undefined) {
|
|
|
|
|
|
return "null";
|
|
|
|
|
|
}
|
|
|
|
|
|
return JSON.stringify(value);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-27 11:18:56 +01:00
|
|
|
|
/**
|
|
|
|
|
|
* WP-26: Track Section-Info für Loop-Items.
|
|
|
|
|
|
* Wird aufgerufen, wenn ein Loop-Item gespeichert wird oder zwischen Steps navigiert wird.
|
|
|
|
|
|
*/
|
|
|
|
|
|
private trackSectionInfoForLoopItem(
|
|
|
|
|
|
nestedStep: InterviewStep,
|
|
|
|
|
|
loopKey: string,
|
|
|
|
|
|
draft: Record<string, unknown>
|
|
|
|
|
|
): void {
|
|
|
|
|
|
// Nur für capture_text und capture_text_line Steps
|
|
|
|
|
|
if (nestedStep.type !== "capture_text" && nestedStep.type !== "capture_text_line") {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-30 12:37:06 +01:00
|
|
|
|
// Type-Guards für sichere Zugriffe
|
|
|
|
|
|
const isCaptureText = nestedStep.type === "capture_text";
|
|
|
|
|
|
const isCaptureTextLine = nestedStep.type === "capture_text_line";
|
|
|
|
|
|
|
|
|
|
|
|
const captureTextStep = isCaptureText ? nestedStep as import("../interview/types").CaptureTextStep : null;
|
|
|
|
|
|
const captureTextLineStep = isCaptureTextLine ? nestedStep as import("../interview/types").CaptureTextLineStep : null;
|
2026-01-27 11:18:56 +01:00
|
|
|
|
|
|
|
|
|
|
// Für capture_text: Nur wenn Section vorhanden ist
|
2026-01-30 12:37:06 +01:00
|
|
|
|
if (isCaptureText && !captureTextStep?.section) {
|
2026-01-27 11:18:56 +01:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Für capture_text_line: Nur wenn heading_level enabled ist
|
2026-01-30 12:37:06 +01:00
|
|
|
|
if (isCaptureTextLine && !captureTextLineStep?.heading_level?.enabled) {
|
2026-01-27 11:18:56 +01:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-01-30 12:37:06 +01:00
|
|
|
|
|
|
|
|
|
|
// Verwende die richtige Step-Variable für die weitere Verarbeitung
|
|
|
|
|
|
const captureStep = isCaptureText ? captureTextStep! : captureTextLineStep!;
|
2026-01-27 11:18:56 +01:00
|
|
|
|
|
|
|
|
|
|
// WP-26: Block-ID ermitteln
|
2026-01-30 12:37:06 +01:00
|
|
|
|
// Für Loop-Items wird die Block-ID im Renderer mit Item-Index nummeriert
|
|
|
|
|
|
// Hier tracken wir nur die Basis-Block-ID ohne Index
|
2026-01-27 11:18:56 +01:00
|
|
|
|
let blockId: string | null = null;
|
|
|
|
|
|
if (captureStep.block_id) {
|
|
|
|
|
|
blockId = captureStep.block_id;
|
|
|
|
|
|
} else if (captureStep.generate_block_id) {
|
2026-01-30 12:37:06 +01:00
|
|
|
|
// Für Loop-Items: Verwende nur Step-Key (Index wird im Renderer hinzugefügt)
|
|
|
|
|
|
// Die tatsächliche Block-ID wird im Renderer mit Item-Index generiert
|
|
|
|
|
|
blockId = slugify(captureStep.key);
|
2026-01-27 11:18:56 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// WP-26: Section-Type ermitteln
|
|
|
|
|
|
const sectionType = captureStep.section_type || null;
|
|
|
|
|
|
const noteType = this.state.profile.note_type;
|
|
|
|
|
|
|
|
|
|
|
|
// WP-26: Heading-Text ermitteln
|
|
|
|
|
|
let heading = "";
|
2026-01-30 12:37:06 +01:00
|
|
|
|
if (isCaptureText && captureTextStep?.section) {
|
2026-01-27 11:18:56 +01:00
|
|
|
|
// Extrahiere Heading-Text aus Section (z.B. "## 📖 Kontext" -> "📖 Kontext")
|
2026-01-30 12:37:06 +01:00
|
|
|
|
heading = captureTextStep.section.replace(/^#+\s+/, "");
|
|
|
|
|
|
} else if (isCaptureTextLine) {
|
2026-01-27 11:18:56 +01:00
|
|
|
|
// Für capture_text_line: Heading aus Draft-Wert
|
|
|
|
|
|
const draftValue = draft[captureStep.key];
|
|
|
|
|
|
if (draftValue && typeof draftValue === "string") {
|
|
|
|
|
|
heading = draftValue;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
heading = captureStep.key;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// WP-26: Section-Info erstellen
|
|
|
|
|
|
const sectionInfo: SectionInfo = {
|
|
|
|
|
|
stepKey: `${loopKey}.${captureStep.key}`, // Eindeutiger Key für Loop-Items
|
|
|
|
|
|
sectionType: sectionType,
|
|
|
|
|
|
heading: heading,
|
|
|
|
|
|
blockId: blockId,
|
|
|
|
|
|
noteType: noteType,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// WP-26: Block-ID tracken (nur wenn vorhanden)
|
|
|
|
|
|
if (blockId) {
|
|
|
|
|
|
this.state.generatedBlockIds.set(blockId, sectionInfo);
|
|
|
|
|
|
console.log(`[WP-26] Block-ID für Loop-Item getrackt: ${blockId} für Step ${loopKey}.${captureStep.key}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// WP-26: Section-Sequenz aktualisieren (nur wenn Section vorhanden)
|
|
|
|
|
|
// Prüfe, ob diese Section bereits in der Sequenz ist (vermeide Duplikate)
|
|
|
|
|
|
const existingIndex = this.state.sectionSequence.findIndex(
|
|
|
|
|
|
s => s.stepKey === sectionInfo.stepKey && s.blockId === blockId
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
if (existingIndex === -1) {
|
|
|
|
|
|
// Neue Section hinzufügen
|
|
|
|
|
|
this.state.sectionSequence.push(sectionInfo);
|
|
|
|
|
|
console.log(`[WP-26] Section für Loop-Item zur Sequenz hinzugefügt: ${sectionInfo.stepKey} (Block-ID: ${blockId || "keine"})`);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Section aktualisieren (falls sich Block-ID oder Section-Type geändert hat)
|
|
|
|
|
|
this.state.sectionSequence[existingIndex] = sectionInfo;
|
|
|
|
|
|
console.log(`[WP-26] Section für Loop-Item aktualisiert: ${sectionInfo.stepKey}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* WP-26: Track Section-Info während des Wizard-Durchlaufs.
|
|
|
|
|
|
* Wird aufgerufen, wenn ein Step mit section/section_type gerendert wird.
|
|
|
|
|
|
*/
|
|
|
|
|
|
private trackSectionInfo(step: InterviewStep): void {
|
|
|
|
|
|
// Nur für capture_text und capture_text_line Steps mit section
|
|
|
|
|
|
if (step.type !== "capture_text" && step.type !== "capture_text_line") {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-30 12:37:06 +01:00
|
|
|
|
// Type-Guards für sichere Zugriffe
|
|
|
|
|
|
const isCaptureText = step.type === "capture_text";
|
|
|
|
|
|
const isCaptureTextLine = step.type === "capture_text_line";
|
|
|
|
|
|
|
|
|
|
|
|
const captureTextStep = isCaptureText ? step as import("../interview/types").CaptureTextStep : null;
|
|
|
|
|
|
const captureTextLineStep = isCaptureTextLine ? step as import("../interview/types").CaptureTextLineStep : null;
|
2026-01-27 11:18:56 +01:00
|
|
|
|
|
|
|
|
|
|
console.log(`[WP-26] trackSectionInfo aufgerufen für Step ${step.key}`, {
|
|
|
|
|
|
type: step.type,
|
2026-01-30 12:37:06 +01:00
|
|
|
|
hasSection: isCaptureText ? !!captureTextStep?.section : false,
|
|
|
|
|
|
section: isCaptureText ? captureTextStep?.section : undefined,
|
|
|
|
|
|
hasSectionType: isCaptureText ? !!captureTextStep?.section_type : !!captureTextLineStep?.section_type,
|
|
|
|
|
|
sectionType: isCaptureText ? captureTextStep?.section_type : captureTextLineStep?.section_type,
|
|
|
|
|
|
hasBlockId: isCaptureText ? !!captureTextStep?.block_id : !!captureTextLineStep?.block_id,
|
|
|
|
|
|
blockId: isCaptureText ? captureTextStep?.block_id : captureTextLineStep?.block_id,
|
|
|
|
|
|
generateBlockId: isCaptureText ? captureTextStep?.generate_block_id : captureTextLineStep?.generate_block_id,
|
|
|
|
|
|
hasHeadingLevel: isCaptureTextLine ? !!captureTextLineStep?.heading_level?.enabled : false,
|
2026-01-27 11:18:56 +01:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-01-30 12:37:06 +01:00
|
|
|
|
// Nur wenn Section vorhanden ist (für capture_text)
|
|
|
|
|
|
if (isCaptureText && !captureTextStep?.section) {
|
2026-01-27 11:18:56 +01:00
|
|
|
|
console.log(`[WP-26] Step ${step.key} hat keine section, überspringe`);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Für capture_text_line: Nur wenn heading_level enabled ist
|
2026-01-30 12:37:06 +01:00
|
|
|
|
if (isCaptureTextLine && !captureTextLineStep?.heading_level?.enabled) {
|
2026-01-27 11:18:56 +01:00
|
|
|
|
console.log(`[WP-26] Step ${step.key} hat kein heading_level enabled, überspringe`);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-01-30 12:37:06 +01:00
|
|
|
|
|
|
|
|
|
|
// Verwende die richtige Step-Variable für die weitere Verarbeitung
|
|
|
|
|
|
const captureStep = isCaptureText ? captureTextStep! : captureTextLineStep!;
|
2026-01-27 11:18:56 +01:00
|
|
|
|
|
|
|
|
|
|
// WP-26: Block-ID ermitteln
|
|
|
|
|
|
let blockId: string | null = null;
|
|
|
|
|
|
if (captureStep.block_id) {
|
|
|
|
|
|
blockId = captureStep.block_id;
|
|
|
|
|
|
} else if (captureStep.generate_block_id) {
|
|
|
|
|
|
blockId = slugify(captureStep.key);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// WP-26: Section-Type ermitteln
|
|
|
|
|
|
const sectionType = captureStep.section_type || null;
|
|
|
|
|
|
const noteType = this.state.profile.note_type;
|
|
|
|
|
|
|
|
|
|
|
|
// WP-26: Heading-Text ermitteln
|
|
|
|
|
|
let heading = "";
|
2026-01-30 12:37:06 +01:00
|
|
|
|
if (isCaptureText && captureTextStep?.section) {
|
2026-01-27 11:18:56 +01:00
|
|
|
|
// Extrahiere Heading-Text aus Section (z.B. "## 📖 Kontext" -> "📖 Kontext")
|
2026-01-30 12:37:06 +01:00
|
|
|
|
heading = captureTextStep.section.replace(/^#+\s+/, "");
|
|
|
|
|
|
} else if (isCaptureTextLine) {
|
2026-01-27 11:18:56 +01:00
|
|
|
|
// Für capture_text_line: Heading wird später aus dem eingegebenen Text generiert
|
|
|
|
|
|
// Verwende Step-Key als Platzhalter
|
2026-01-30 12:37:06 +01:00
|
|
|
|
heading = step.key;
|
2026-01-27 11:18:56 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// WP-26: Section-Info erstellen
|
|
|
|
|
|
const sectionInfo: SectionInfo = {
|
|
|
|
|
|
stepKey: captureStep.key,
|
|
|
|
|
|
sectionType: sectionType,
|
|
|
|
|
|
heading: heading,
|
|
|
|
|
|
blockId: blockId,
|
|
|
|
|
|
noteType: noteType,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// WP-26: Block-ID tracken (nur wenn vorhanden)
|
|
|
|
|
|
if (blockId) {
|
|
|
|
|
|
this.state.generatedBlockIds.set(blockId, sectionInfo);
|
|
|
|
|
|
console.log(`[WP-26] Block-ID getrackt: ${blockId} für Step ${captureStep.key}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// WP-26: Section-Sequenz aktualisieren (nur wenn Section vorhanden)
|
|
|
|
|
|
// Prüfe, ob diese Section bereits in der Sequenz ist (vermeide Duplikate)
|
|
|
|
|
|
const existingIndex = this.state.sectionSequence.findIndex(
|
|
|
|
|
|
s => s.stepKey === captureStep.key && s.blockId === blockId
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
if (existingIndex === -1) {
|
|
|
|
|
|
// Neue Section hinzufügen
|
|
|
|
|
|
this.state.sectionSequence.push(sectionInfo);
|
|
|
|
|
|
console.log(`[WP-26] Section zur Sequenz hinzugefügt: ${captureStep.key} (Block-ID: ${blockId || "keine"})`);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Section aktualisieren (falls sich Block-ID oder Section-Type geändert hat)
|
|
|
|
|
|
this.state.sectionSequence[existingIndex] = sectionInfo;
|
|
|
|
|
|
console.log(`[WP-26] Section aktualisiert: ${captureStep.key}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-30 12:37:06 +01:00
|
|
|
|
/**
|
|
|
|
|
|
* Ermittelt den effektiven Section-Type basierend auf Heading-Level.
|
|
|
|
|
|
* Wenn eine Section keinen expliziten Type hat, wird der Type der vorherigen Section
|
|
|
|
|
|
* auf dem gleichen oder höheren Level verwendet, sonst Note-Type.
|
|
|
|
|
|
*/
|
|
|
|
|
|
private getEffectiveSectionType(
|
|
|
|
|
|
section: SectionInfo,
|
|
|
|
|
|
index: number
|
|
|
|
|
|
): string {
|
|
|
|
|
|
// Wenn expliziter Section-Type vorhanden, verwende diesen
|
|
|
|
|
|
if (section.sectionType) {
|
|
|
|
|
|
return section.sectionType;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Extrahiere Heading-Level aus der Überschrift
|
|
|
|
|
|
const headingMatch = section.heading.match(/^(#{1,6})\s+/);
|
|
|
|
|
|
const currentLevel = headingMatch ? headingMatch[1]?.length || 0 : 0;
|
|
|
|
|
|
|
|
|
|
|
|
// Suche rückwärts nach der letzten Section mit explizitem Type auf gleichem oder höherem Level
|
|
|
|
|
|
for (let i = index - 1; i >= 0; i--) {
|
|
|
|
|
|
const prevSection = this.state.sectionSequence[i];
|
|
|
|
|
|
if (!prevSection) continue;
|
|
|
|
|
|
|
|
|
|
|
|
const prevHeadingMatch = prevSection.heading.match(/^(#{1,6})\s+/);
|
|
|
|
|
|
const prevLevel = prevHeadingMatch ? prevHeadingMatch[1]?.length || 0 : 0;
|
|
|
|
|
|
|
|
|
|
|
|
// Wenn vorherige Section auf gleichem oder höherem Level einen expliziten Type hat
|
|
|
|
|
|
if (prevLevel <= currentLevel && prevSection.sectionType) {
|
|
|
|
|
|
return prevSection.sectionType;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Wenn wir auf ein höheres Level stoßen, stoppe die Suche
|
|
|
|
|
|
if (prevLevel < currentLevel) {
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Fallback: Note-Type
|
|
|
|
|
|
return section.noteType;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-16 12:27:44 +01:00
|
|
|
|
onClose(): void {
|
|
|
|
|
|
const { contentEl } = this;
|
|
|
|
|
|
contentEl.empty();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|