Add heading level configuration to interview steps and rendering logic
Some checks are pending
Node.js build / build (20.x) (push) Waiting to run
Node.js build / build (22.x) (push) Waiting to run

- Introduced a new `heading_level` property in `CaptureTextLineStep` to enable customizable heading levels for interview steps.
- Updated `parseInterviewConfig` to parse and validate heading level settings.
- Enhanced rendering functions to prepend heading prefixes based on the configured heading level, improving text output formatting.
- Implemented a dropdown in the `InterviewWizardModal` for selecting heading levels, allowing users to easily adjust the heading for each step.
- Improved handling of nested loops to support heading level selection and display.
This commit is contained in:
Lars 2026-01-16 22:05:36 +01:00
parent 611ad37c42
commit 070cb853a9
4 changed files with 194 additions and 15 deletions

View File

@ -333,6 +333,17 @@ function parseStep(raw: Record<string, unknown>): InterviewStep | null {
step.prompt = raw.prompt.trim(); step.prompt = raw.prompt.trim();
} }
// Parse heading_level
if (raw.heading_level && typeof raw.heading_level === "object") {
const headingLevel = raw.heading_level as Record<string, unknown>;
step.heading_level = {
enabled: headingLevel.enabled === true,
default: typeof headingLevel.default === "number"
? Math.max(1, Math.min(6, headingLevel.default))
: 2,
};
}
// Parse output.template // Parse output.template
if (raw.output && typeof raw.output === "object") { if (raw.output && typeof raw.output === "object") {
const output = raw.output as Record<string, unknown>; const output = raw.output as Record<string, unknown>;

View File

@ -93,17 +93,33 @@ function renderCaptureTextLine(step: CaptureTextLineStep, answers: RenderAnswers
const text = String(value); const text = String(value);
// Get heading level if configured
const headingLevelKey = `${step.key}_heading_level`;
const headingLevel = answers.collectedData.get(headingLevelKey);
let headingPrefix = "";
if (step.heading_level?.enabled) {
if (typeof headingLevel === "number") {
const level = Math.max(1, Math.min(6, headingLevel));
headingPrefix = "#".repeat(level) + " ";
} else if (step.heading_level.default) {
// Fallback to default if not set
const level = Math.max(1, Math.min(6, step.heading_level.default));
headingPrefix = "#".repeat(level) + " ";
}
}
// Use template if provided // Use template if provided
if (step.output?.template) { if (step.output?.template) {
return renderTemplate(step.output.template, { return renderTemplate(step.output.template, {
text, text: headingPrefix + text,
field: step.key, field: step.key,
value: text, value: headingPrefix + text,
heading_level: headingLevel ? String(headingLevel) : "",
}); });
} }
// Default: just the text // Default: text with heading prefix if configured
return text; return headingPrefix + text;
} }
/** /**
@ -187,16 +203,32 @@ function renderLoopRecursive(step: LoopStep, answers: RenderAnswers, depth: numb
const text = String(fieldValue); const text = String(fieldValue);
const captureStep = nestedStep as CaptureTextLineStep; const captureStep = nestedStep as CaptureTextLineStep;
// Get heading level if configured (for nested loops, check in item data)
const headingLevelKey = `${nestedStep.key}_heading_level`;
const headingLevel = (item as Record<string, unknown>)[headingLevelKey];
let headingPrefix = "";
if (captureStep.heading_level?.enabled) {
if (typeof headingLevel === "number") {
const level = Math.max(1, Math.min(6, headingLevel));
headingPrefix = "#".repeat(level) + " ";
} else if (captureStep.heading_level.default) {
// Fallback to default if not set in item
const level = Math.max(1, Math.min(6, captureStep.heading_level.default));
headingPrefix = "#".repeat(level) + " ";
}
}
// Use template if provided // Use template if provided
if (captureStep.output?.template) { if (captureStep.output?.template) {
itemParts.push(renderTemplate(captureStep.output.template, { itemParts.push(renderTemplate(captureStep.output.template, {
text, text: headingPrefix + text,
field: nestedStep.key, field: nestedStep.key,
value: text, value: headingPrefix + text,
heading_level: headingLevel ? String(headingLevel) : (captureStep.heading_level?.default ? String(captureStep.heading_level.default) : ""),
})); }));
} else { } else {
// Default: just the text // Default: text with heading prefix if configured
itemParts.push(text); itemParts.push(headingPrefix + text);
} }
} }
} else if (nestedStep.type === "capture_frontmatter") { } else if (nestedStep.type === "capture_frontmatter") {
@ -267,9 +299,15 @@ function renderLoopRecursive(step: LoopStep, answers: RenderAnswers, depth: numb
* Render template string with token replacement. * Render template string with token replacement.
* Tokens: {text}, {field}, {value} * Tokens: {text}, {field}, {value}
*/ */
function renderTemplate(template: string, tokens: { text: string; field: string; value: string }): string { function renderTemplate(template: string, tokens: { text: string; field: string; value: string; heading_level?: string }): string {
return template let result = template
.replace(/\{text\}/g, tokens.text) .replace(/\{text\}/g, tokens.text)
.replace(/\{field\}/g, tokens.field) .replace(/\{field\}/g, tokens.field)
.replace(/\{value\}/g, tokens.value); .replace(/\{value\}/g, tokens.value);
if (tokens.heading_level !== undefined) {
result = result.replace(/\{heading_level\}/g, tokens.heading_level);
}
return result;
} }

View File

@ -80,8 +80,12 @@ export interface CaptureTextLineStep {
label?: string; label?: string;
required?: boolean; required?: boolean;
prompt?: string; // Optional prompt text prompt?: string; // Optional prompt text
heading_level?: {
enabled?: boolean; // Show heading level selector (default: false)
default?: number; // Default heading level 1-6 (default: 2)
};
output?: { output?: {
template?: string; // Template with tokens: {text}, {field}, {value} template?: string; // Template with tokens: {text}, {field}, {value}, {heading_level}
}; };
} }

View File

@ -425,13 +425,70 @@ export class InterviewWizardModal extends Modal {
}); });
} }
// Input container // Input container with heading level selector
const inputContainer = fieldContainer.createEl("div", { const inputContainer = fieldContainer.createEl("div", {
cls: "mindnet-field__input", cls: "mindnet-field__input",
}); });
inputContainer.style.width = "100%"; inputContainer.style.width = "100%";
inputContainer.style.display = "flex";
inputContainer.style.gap = "0.5em";
inputContainer.style.alignItems = "center";
const fieldSetting = new Setting(inputContainer); // 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)";
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);
fieldSetting.settingEl.style.width = "100%"; fieldSetting.settingEl.style.width = "100%";
fieldSetting.controlEl.style.width = "100%"; fieldSetting.controlEl.style.width = "100%";
@ -1146,13 +1203,82 @@ export class InterviewWizardModal extends Modal {
}); });
} }
// Input container // Input container with heading level selector
const inputContainer = fieldContainer.createEl("div", { const inputContainer = fieldContainer.createEl("div", {
cls: "mindnet-field__input", cls: "mindnet-field__input",
}); });
inputContainer.style.width = "100%"; inputContainer.style.width = "100%";
inputContainer.style.display = "flex";
inputContainer.style.gap = "0.5em";
inputContainer.style.alignItems = "center";
const fieldSetting = new Setting(inputContainer); // 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";
const fieldSetting = new Setting(textInputContainer);
fieldSetting.settingEl.style.width = "100%"; fieldSetting.settingEl.style.width = "100%";
fieldSetting.controlEl.style.width = "100%"; fieldSetting.controlEl.style.width = "100%";