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";
|
|
|
|
|
import type { InterviewProfile, InterviewStep } from "../interview/types";
|
|
|
|
|
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-16 18:26:01 +01:00
|
|
|
import { renderProfileToMarkdown, type RenderAnswers } from "../interview/renderer";
|
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;
|
|
|
|
|
profileKey: string;
|
|
|
|
|
// 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-16 12:27:44 +01:00
|
|
|
|
|
|
|
|
constructor(
|
|
|
|
|
app: App,
|
|
|
|
|
profile: InterviewProfile,
|
|
|
|
|
file: TFile,
|
|
|
|
|
fileContent: string,
|
|
|
|
|
onSubmit: (result: WizardResult) => void,
|
|
|
|
|
onSaveAndExit: (result: WizardResult) => void
|
|
|
|
|
) {
|
|
|
|
|
super(app);
|
|
|
|
|
|
|
|
|
|
// Validate profile
|
|
|
|
|
if (!profile) {
|
|
|
|
|
new Notice(`Interview profile not found`);
|
|
|
|
|
throw new Error("Profile is required");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.profileKey = profile.key;
|
|
|
|
|
|
|
|
|
|
// Log profile info
|
|
|
|
|
const stepKinds = profile.steps?.map(s => s.type) || [];
|
|
|
|
|
console.log("Wizard profile", {
|
|
|
|
|
key: profile.key,
|
|
|
|
|
stepCount: profile.steps?.length,
|
|
|
|
|
kinds: stepKinds,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
|
|
|
|
|
|
// 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,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
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";
|
|
|
|
|
|
|
|
|
|
// 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-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
|
|
|
|
|
|
|
|
// 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,
|
|
|
|
|
() => {
|
|
|
|
|
const newPreviewMode = !this.previewMode.get(step.key);
|
|
|
|
|
this.previewMode.set(step.key, newPreviewMode);
|
|
|
|
|
this.renderStep();
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
textEditorContainer.insertBefore(toolbar, textEditorContainer.firstChild);
|
|
|
|
|
}
|
|
|
|
|
}, 10);
|
|
|
|
|
|
|
|
|
|
// Render preview if in preview mode
|
|
|
|
|
if (isPreviewMode && existingValue) {
|
|
|
|
|
this.updatePreview(previewContainer, existingValue);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
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,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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 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> {
|
|
|
|
|
container.empty();
|
|
|
|
|
if (!markdown.trim()) {
|
|
|
|
|
container.createEl("p", {
|
|
|
|
|
text: "(empty)",
|
|
|
|
|
cls: "text-muted",
|
|
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Use Obsidian's MarkdownRenderer
|
|
|
|
|
// Create a component for the renderer
|
|
|
|
|
const component = new Component();
|
|
|
|
|
// Register it with the modal (Modal extends Component)
|
|
|
|
|
(this as any).addChild(component);
|
|
|
|
|
|
|
|
|
|
await MarkdownRenderer.render(
|
|
|
|
|
this.app,
|
|
|
|
|
markdown,
|
|
|
|
|
container,
|
|
|
|
|
this.file.path,
|
|
|
|
|
component
|
|
|
|
|
);
|
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;
|
|
|
|
|
|
|
|
|
|
const items = this.state.loopContexts.get(step.key) || [];
|
|
|
|
|
const currentItemIndex = items.length; // Next item index
|
|
|
|
|
|
|
|
|
|
console.log("Render loop step", {
|
|
|
|
|
stepKey: step.key,
|
|
|
|
|
stepLabel: step.label,
|
|
|
|
|
itemsCount: items.length,
|
|
|
|
|
nestedStepsCount: step.items.length,
|
|
|
|
|
nestedStepTypes: step.items.map(s => s.type),
|
|
|
|
|
currentItemIndex: currentItemIndex,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
containerEl.createEl("h2", {
|
|
|
|
|
text: step.label || "Loop",
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Show existing items
|
|
|
|
|
if (items.length > 0) {
|
|
|
|
|
const itemsList = containerEl.createEl("div", { cls: "loop-items-list" });
|
2026-01-16 14:11:04 +01:00
|
|
|
itemsList.style.width = "100%";
|
2026-01-16 12:27:44 +01:00
|
|
|
itemsList.createEl("h3", { text: `Gesammelte Items (${items.length}):` });
|
|
|
|
|
const ul = itemsList.createEl("ul");
|
|
|
|
|
for (let i = 0; i < items.length; i++) {
|
|
|
|
|
const item = items[i];
|
|
|
|
|
const li = ul.createEl("li");
|
|
|
|
|
li.createSpan({ text: `Item ${i + 1}: ` });
|
|
|
|
|
if (typeof item === "object" && item !== null) {
|
|
|
|
|
// Show readable format instead of JSON
|
|
|
|
|
const itemEntries = Object.entries(item);
|
|
|
|
|
if (itemEntries.length > 0) {
|
|
|
|
|
const itemText = itemEntries
|
|
|
|
|
.map(([key, value]) => {
|
|
|
|
|
const nestedStep = step.items.find(s => s.key === key);
|
|
|
|
|
const label = nestedStep?.label || key;
|
|
|
|
|
return `${label}: ${String(value).substring(0, 50)}${String(value).length > 50 ? "..." : ""}`;
|
|
|
|
|
})
|
|
|
|
|
.join(", ");
|
|
|
|
|
li.createSpan({ text: itemText });
|
|
|
|
|
} else {
|
|
|
|
|
li.createSpan({ text: "Empty" });
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
li.createSpan({ text: String(item) });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
itemsList.createEl("p", {
|
|
|
|
|
text: "Sie können weitere Items hinzufügen oder mit 'Next' fortfahren.",
|
|
|
|
|
cls: "interview-note",
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Render nested steps for current item
|
|
|
|
|
if (step.items.length > 0) {
|
2026-01-16 14:11:04 +01:00
|
|
|
const itemFormContainer = containerEl.createEl("div", {
|
|
|
|
|
cls: "loop-item-form",
|
|
|
|
|
});
|
|
|
|
|
itemFormContainer.style.width = "100%";
|
|
|
|
|
|
|
|
|
|
itemFormContainer.createEl("h3", {
|
2026-01-16 12:27:44 +01:00
|
|
|
text: items.length === 0 ? "First Item" : `Item ${items.length + 1}`,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Create a temporary state for the current loop item
|
|
|
|
|
const itemDataKey = `${step.key}_item_${currentItemIndex}`;
|
|
|
|
|
const itemData = new Map<string, unknown>();
|
|
|
|
|
|
|
|
|
|
// Render each nested step
|
|
|
|
|
for (const nestedStep of step.items) {
|
|
|
|
|
if (nestedStep.type === "capture_text") {
|
|
|
|
|
const existingValue = (itemData.get(nestedStep.key) as string) || "";
|
|
|
|
|
const inputKey = `${itemDataKey}_${nestedStep.key}`;
|
|
|
|
|
|
2026-01-16 14:11:04 +01:00
|
|
|
// Field container with vertical layout
|
|
|
|
|
const fieldContainer = itemFormContainer.createEl("div", {
|
|
|
|
|
cls: "mindnet-field",
|
2026-01-16 12:27:44 +01:00
|
|
|
});
|
2026-01-16 14:11:04 +01:00
|
|
|
|
|
|
|
|
// Label
|
|
|
|
|
if (nestedStep.label) {
|
|
|
|
|
const labelEl = fieldContainer.createEl("div", {
|
|
|
|
|
cls: "mindnet-field__label",
|
|
|
|
|
text: nestedStep.label,
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-01-16 12:27:44 +01:00
|
|
|
|
2026-01-16 14:11:04 +01:00
|
|
|
// Description/Prompt
|
2026-01-16 12:27:44 +01:00
|
|
|
if (nestedStep.prompt) {
|
2026-01-16 14:11:04 +01:00
|
|
|
const descEl = fieldContainer.createEl("div", {
|
|
|
|
|
cls: "mindnet-field__desc",
|
2026-01-16 12:27:44 +01:00
|
|
|
text: nestedStep.prompt,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-16 14:11:04 +01:00
|
|
|
// Editor container
|
|
|
|
|
const editorContainer = fieldContainer.createEl("div", {
|
|
|
|
|
cls: "markdown-editor-container",
|
|
|
|
|
});
|
|
|
|
|
editorContainer.style.width = "100%";
|
|
|
|
|
editorContainer.style.position = "relative";
|
|
|
|
|
|
|
|
|
|
const textEditorContainer = editorContainer.createEl("div", {
|
|
|
|
|
cls: "markdown-editor-wrapper",
|
|
|
|
|
});
|
|
|
|
|
textEditorContainer.style.width = "100%";
|
|
|
|
|
|
|
|
|
|
const textSetting = new Setting(textEditorContainer);
|
|
|
|
|
textSetting.settingEl.style.width = "100%";
|
|
|
|
|
textSetting.controlEl.style.width = "100%";
|
|
|
|
|
|
|
|
|
|
// Hide the default label from Setting component
|
|
|
|
|
const settingNameEl = textSetting.settingEl.querySelector(".setting-item-name") as HTMLElement;
|
|
|
|
|
if (settingNameEl) {
|
|
|
|
|
settingNameEl.style.display = "none";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
textSetting.addTextArea((text) => {
|
2026-01-16 12:27:44 +01:00
|
|
|
text.setValue(existingValue);
|
|
|
|
|
this.currentInputValues.set(inputKey, existingValue);
|
|
|
|
|
|
|
|
|
|
text.onChange((value) => {
|
|
|
|
|
this.currentInputValues.set(inputKey, value);
|
|
|
|
|
itemData.set(nestedStep.key, value);
|
|
|
|
|
});
|
|
|
|
|
text.inputEl.rows = 10;
|
|
|
|
|
text.inputEl.style.width = "100%";
|
2026-01-16 14:11:04 +01:00
|
|
|
text.inputEl.style.minHeight = "200px";
|
|
|
|
|
text.inputEl.style.boxSizing = "border-box";
|
2026-01-16 12:27:44 +01:00
|
|
|
});
|
2026-01-16 14:11:04 +01:00
|
|
|
|
|
|
|
|
// Add toolbar for loop item textarea
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
const textarea = textEditorContainer.querySelector("textarea");
|
|
|
|
|
if (textarea) {
|
|
|
|
|
const itemToolbar = createMarkdownToolbar(textarea);
|
|
|
|
|
textEditorContainer.insertBefore(itemToolbar, textEditorContainer.firstChild);
|
|
|
|
|
}
|
|
|
|
|
}, 10);
|
2026-01-16 18:26:01 +01:00
|
|
|
} else if (nestedStep.type === "capture_text_line") {
|
|
|
|
|
const existingValue = (itemData.get(nestedStep.key) as string) || "";
|
|
|
|
|
const inputKey = `${itemDataKey}_${nestedStep.key}`;
|
|
|
|
|
|
|
|
|
|
// Field container with vertical layout
|
|
|
|
|
const fieldContainer = itemFormContainer.createEl("div", {
|
|
|
|
|
cls: "mindnet-field",
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Label
|
|
|
|
|
if (nestedStep.label) {
|
|
|
|
|
const labelEl = fieldContainer.createEl("div", {
|
|
|
|
|
cls: "mindnet-field__label",
|
|
|
|
|
text: nestedStep.label,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Description/Prompt
|
|
|
|
|
if (nestedStep.prompt) {
|
|
|
|
|
const descEl = fieldContainer.createEl("div", {
|
|
|
|
|
cls: "mindnet-field__desc",
|
|
|
|
|
text: nestedStep.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 settingNameEl = fieldSetting.settingEl.querySelector(".setting-item-name") as HTMLElement | null;
|
|
|
|
|
if (settingNameEl) {
|
|
|
|
|
settingNameEl.style.display = "none";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fieldSetting.addText((text) => {
|
|
|
|
|
text.setValue(existingValue);
|
|
|
|
|
this.currentInputValues.set(inputKey, existingValue);
|
|
|
|
|
|
|
|
|
|
text.onChange((value) => {
|
|
|
|
|
this.currentInputValues.set(inputKey, value);
|
|
|
|
|
itemData.set(nestedStep.key, value);
|
|
|
|
|
});
|
|
|
|
|
text.inputEl.style.width = "100%";
|
|
|
|
|
text.inputEl.style.boxSizing = "border-box";
|
|
|
|
|
});
|
2026-01-16 12:27:44 +01:00
|
|
|
} else if (nestedStep.type === "capture_frontmatter") {
|
|
|
|
|
const existingValue = (itemData.get(nestedStep.key) as string) || "";
|
|
|
|
|
const inputKey = `${itemDataKey}_${nestedStep.key}`;
|
|
|
|
|
|
2026-01-16 14:11:04 +01:00
|
|
|
// Field container with vertical layout
|
|
|
|
|
const fieldContainer = itemFormContainer.createEl("div", {
|
|
|
|
|
cls: "mindnet-field",
|
2026-01-16 12:27:44 +01:00
|
|
|
});
|
|
|
|
|
|
2026-01-16 14:11:04 +01:00
|
|
|
// 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 14:11:04 +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 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);
|
|
|
|
|
this.currentInputValues.set(inputKey, existingValue);
|
|
|
|
|
|
|
|
|
|
text.onChange((value) => {
|
|
|
|
|
this.currentInputValues.set(inputKey, value);
|
|
|
|
|
itemData.set(nestedStep.key, value);
|
|
|
|
|
});
|
|
|
|
|
text.inputEl.style.width = "100%";
|
|
|
|
|
text.inputEl.style.boxSizing = "border-box";
|
|
|
|
|
});
|
2026-01-16 12:27:44 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Add item button
|
2026-01-16 14:11:04 +01:00
|
|
|
const addButtonSetting = new Setting(itemFormContainer);
|
|
|
|
|
addButtonSetting.settingEl.style.width = "100%";
|
|
|
|
|
addButtonSetting.addButton((button) => {
|
2026-01-16 12:27:44 +01:00
|
|
|
button
|
|
|
|
|
.setButtonText("Add Item")
|
|
|
|
|
.setCta()
|
|
|
|
|
.onClick(() => {
|
|
|
|
|
// Collect all item data
|
|
|
|
|
const itemDataObj: Record<string, unknown> = {};
|
|
|
|
|
let hasData = false;
|
|
|
|
|
|
|
|
|
|
for (const nestedStep of step.items) {
|
|
|
|
|
const inputKey = `${itemDataKey}_${nestedStep.key}`;
|
|
|
|
|
const value = this.currentInputValues.get(inputKey);
|
|
|
|
|
if (value !== undefined && String(value).trim()) {
|
|
|
|
|
itemDataObj[nestedStep.key] = value;
|
|
|
|
|
hasData = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!hasData) {
|
|
|
|
|
new Notice("Please enter at least one field before adding an item");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Add to loop context
|
|
|
|
|
items.push(itemDataObj);
|
|
|
|
|
this.state.loopContexts.set(step.key, items);
|
|
|
|
|
|
|
|
|
|
// Clear input values for this item
|
|
|
|
|
for (const nestedStep of step.items) {
|
|
|
|
|
const inputKey = `${itemDataKey}_${nestedStep.key}`;
|
|
|
|
|
this.currentInputValues.delete(inputKey);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.log("Loop item added", {
|
|
|
|
|
stepKey: step.key,
|
|
|
|
|
itemIndex: items.length - 1,
|
|
|
|
|
itemData: itemDataObj,
|
|
|
|
|
totalItems: items.length,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
new Notice(`Item ${items.length} added. You can add more or click Next to continue.`);
|
|
|
|
|
|
|
|
|
|
// Re-render to show new item and reset form
|
|
|
|
|
this.renderStep();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Show hint if no items yet
|
|
|
|
|
if (items.length === 0) {
|
2026-01-16 14:11:04 +01:00
|
|
|
itemFormContainer.createEl("p", {
|
2026-01-16 12:27:44 +01:00
|
|
|
text: "⚠️ Please add at least one item before continuing",
|
|
|
|
|
cls: "interview-warning",
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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) {
|
|
|
|
|
const llmToolbar = createMarkdownToolbar(textarea);
|
|
|
|
|
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) });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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";
|
|
|
|
|
|
|
|
|
|
// For loop steps, disable Next until at least one item is added
|
|
|
|
|
const loopItems = isLoop ? (this.state.loopContexts.get(step.key) || []) : [];
|
|
|
|
|
const canProceedLoop = !isLoop || loopItems.length > 0;
|
|
|
|
|
|
|
|
|
|
button
|
|
|
|
|
.setButtonText(isReview ? "Apply & Finish" : "Next")
|
|
|
|
|
.setCta()
|
|
|
|
|
.setDisabled((!canGoNext(this.state) && !isReview) || !canProceedLoop);
|
|
|
|
|
button.onClick(() => {
|
|
|
|
|
if (isReview) {
|
|
|
|
|
console.log("=== FINISH WIZARD (Apply & Finish) ===");
|
|
|
|
|
// Save current step data before finishing
|
|
|
|
|
const currentStep = getCurrentStep(this.state);
|
|
|
|
|
if (currentStep) {
|
|
|
|
|
this.saveCurrentStepData(currentStep);
|
|
|
|
|
}
|
|
|
|
|
this.applyPatches();
|
|
|
|
|
this.onSubmit({ applied: true, patches: this.state.patches });
|
|
|
|
|
this.close();
|
|
|
|
|
} else {
|
|
|
|
|
this.goNext();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
})
|
|
|
|
|
.addButton((button) => {
|
|
|
|
|
button.setButtonText("Skip").onClick(() => {
|
|
|
|
|
this.goNext();
|
|
|
|
|
});
|
|
|
|
|
})
|
|
|
|
|
.addButton((button) => {
|
|
|
|
|
button.setButtonText("Save & Exit").onClick(() => {
|
|
|
|
|
console.log("=== SAVE & EXIT ===");
|
|
|
|
|
this.applyPatches();
|
|
|
|
|
this.onSaveAndExit({
|
|
|
|
|
applied: true,
|
|
|
|
|
patches: this.state.patches,
|
|
|
|
|
});
|
|
|
|
|
this.close();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
goNext(): void {
|
|
|
|
|
const currentStep = getCurrentStep(this.state);
|
|
|
|
|
|
|
|
|
|
// 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();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async applyPatches(): Promise<void> {
|
|
|
|
|
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 18:26:01 +01:00
|
|
|
// Use renderer to generate markdown from collected data
|
|
|
|
|
const answers: RenderAnswers = {
|
|
|
|
|
collectedData: this.state.collectedData,
|
|
|
|
|
loopContexts: this.state.loopContexts,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const renderedMarkdown = renderProfileToMarkdown(this.state.profile, answers);
|
|
|
|
|
|
|
|
|
|
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-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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onClose(): void {
|
|
|
|
|
const { contentEl } = this;
|
|
|
|
|
contentEl.empty();
|
|
|
|
|
}
|
|
|
|
|
}
|