MVP - Basis finalisiert. Standard Toolbar, Workflow
This commit is contained in:
parent
587ef3010a
commit
8ba098c780
|
|
@ -176,6 +176,14 @@ function parseStep(raw: Record<string, unknown>): InterviewStep | null {
|
|||
step.label = raw.label.trim();
|
||||
}
|
||||
|
||||
// Parse output.join
|
||||
if (raw.output && typeof raw.output === "object") {
|
||||
const output = raw.output as Record<string, unknown>;
|
||||
if (typeof output.join === "string") {
|
||||
step.output = { join: output.join };
|
||||
}
|
||||
}
|
||||
|
||||
for (const itemRaw of nestedSteps) {
|
||||
if (!itemRaw || typeof itemRaw !== "object") {
|
||||
continue;
|
||||
|
|
@ -246,6 +254,17 @@ function parseStep(raw: Record<string, unknown>): InterviewStep | null {
|
|||
if (typeof raw.required === "boolean") {
|
||||
step.required = raw.required;
|
||||
}
|
||||
if (typeof raw.prompt === "string" && raw.prompt.trim()) {
|
||||
step.prompt = raw.prompt.trim();
|
||||
}
|
||||
|
||||
// Parse output.template
|
||||
if (raw.output && typeof raw.output === "object") {
|
||||
const output = raw.output as Record<string, unknown>;
|
||||
if (typeof output.template === "string") {
|
||||
step.output = { template: output.template };
|
||||
}
|
||||
}
|
||||
|
||||
return step;
|
||||
}
|
||||
|
|
@ -274,6 +293,46 @@ function parseStep(raw: Record<string, unknown>): InterviewStep | null {
|
|||
step.prompt = raw.prompt.trim();
|
||||
}
|
||||
|
||||
// Parse output.template
|
||||
if (raw.output && typeof raw.output === "object") {
|
||||
const output = raw.output as Record<string, unknown>;
|
||||
if (typeof output.template === "string") {
|
||||
step.output = { template: output.template };
|
||||
}
|
||||
}
|
||||
|
||||
return step;
|
||||
}
|
||||
|
||||
if (type === "capture_text_line") {
|
||||
const key = getKey();
|
||||
if (!key) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const step: InterviewStep = {
|
||||
type: "capture_text_line",
|
||||
key: key,
|
||||
};
|
||||
|
||||
if (typeof raw.label === "string" && raw.label.trim()) {
|
||||
step.label = raw.label.trim();
|
||||
}
|
||||
if (typeof raw.required === "boolean") {
|
||||
step.required = raw.required;
|
||||
}
|
||||
if (typeof raw.prompt === "string" && raw.prompt.trim()) {
|
||||
step.prompt = raw.prompt.trim();
|
||||
}
|
||||
|
||||
// Parse output.template
|
||||
if (raw.output && typeof raw.output === "object") {
|
||||
const output = raw.output as Record<string, unknown>;
|
||||
if (typeof output.template === "string") {
|
||||
step.output = { template: output.template };
|
||||
}
|
||||
}
|
||||
|
||||
return step;
|
||||
}
|
||||
|
||||
|
|
|
|||
241
src/interview/renderer.ts
Normal file
241
src/interview/renderer.ts
Normal file
|
|
@ -0,0 +1,241 @@
|
|||
/**
|
||||
* Markdown renderer for interview profiles.
|
||||
* Converts collected answers into markdown output based on profile configuration.
|
||||
*/
|
||||
|
||||
import type { InterviewProfile, InterviewStep, CaptureTextStep, CaptureTextLineStep, CaptureFrontmatterStep, LoopStep } from "./types";
|
||||
|
||||
export interface RenderAnswers {
|
||||
collectedData: Map<string, unknown>;
|
||||
loopContexts: Map<string, unknown[]>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render profile answers to markdown string.
|
||||
* Deterministic and testable.
|
||||
*/
|
||||
export function renderProfileToMarkdown(
|
||||
profile: InterviewProfile,
|
||||
answers: RenderAnswers
|
||||
): string {
|
||||
const output: string[] = [];
|
||||
|
||||
for (const step of profile.steps) {
|
||||
const stepOutput = renderStep(step, answers);
|
||||
if (stepOutput) {
|
||||
output.push(stepOutput);
|
||||
}
|
||||
}
|
||||
|
||||
return output.join("\n\n").trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a single step to markdown.
|
||||
*/
|
||||
function renderStep(step: InterviewStep, answers: RenderAnswers): string | null {
|
||||
switch (step.type) {
|
||||
case "capture_text":
|
||||
return renderCaptureText(step, answers);
|
||||
case "capture_text_line":
|
||||
return renderCaptureTextLine(step, answers);
|
||||
case "capture_frontmatter":
|
||||
// Frontmatter is handled separately, skip here
|
||||
return null;
|
||||
case "loop":
|
||||
return renderLoop(step, answers);
|
||||
case "instruction":
|
||||
case "llm_dialog":
|
||||
case "review":
|
||||
// These don't produce markdown output
|
||||
return null;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render capture_text step.
|
||||
*/
|
||||
function renderCaptureText(step: CaptureTextStep, answers: RenderAnswers): string | null {
|
||||
const value = answers.collectedData.get(step.key);
|
||||
if (!value || String(value).trim() === "") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const text = String(value);
|
||||
|
||||
// Use template if provided
|
||||
if (step.output?.template) {
|
||||
return renderTemplate(step.output.template, {
|
||||
text,
|
||||
field: step.key,
|
||||
value: text,
|
||||
});
|
||||
}
|
||||
|
||||
// Default: use section if provided, otherwise just the text
|
||||
if (step.section) {
|
||||
return `${step.section}\n\n${text}`;
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render capture_text_line step.
|
||||
*/
|
||||
function renderCaptureTextLine(step: CaptureTextLineStep, answers: RenderAnswers): string | null {
|
||||
const value = answers.collectedData.get(step.key);
|
||||
if (!value || String(value).trim() === "") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const text = String(value);
|
||||
|
||||
// Use template if provided
|
||||
if (step.output?.template) {
|
||||
return renderTemplate(step.output.template, {
|
||||
text,
|
||||
field: step.key,
|
||||
value: text,
|
||||
});
|
||||
}
|
||||
|
||||
// Default: just the text
|
||||
return text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render capture_frontmatter step.
|
||||
* Note: This is typically handled separately, but included for completeness.
|
||||
*/
|
||||
function renderCaptureFrontmatter(step: CaptureFrontmatterStep, answers: RenderAnswers): string | null {
|
||||
const value = answers.collectedData.get(step.key);
|
||||
if (value === undefined || value === null || String(value).trim() === "") {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Use template if provided
|
||||
if (step.output?.template) {
|
||||
return renderTemplate(step.output.template, {
|
||||
text: String(value),
|
||||
field: step.field,
|
||||
value: String(value),
|
||||
});
|
||||
}
|
||||
|
||||
// Default: no markdown output (frontmatter is handled separately)
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render loop step.
|
||||
*/
|
||||
function renderLoop(step: LoopStep, answers: RenderAnswers): string | null {
|
||||
const items = answers.loopContexts.get(step.key);
|
||||
if (!items || items.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const itemOutputs: string[] = [];
|
||||
|
||||
for (const item of items) {
|
||||
if (!item || typeof item !== "object") {
|
||||
continue;
|
||||
}
|
||||
|
||||
const itemParts: string[] = [];
|
||||
|
||||
// Render each nested step for this item
|
||||
for (const nestedStep of step.items) {
|
||||
const fieldValue = (item as Record<string, unknown>)[nestedStep.key];
|
||||
|
||||
if (nestedStep.type === "capture_text") {
|
||||
if (fieldValue && String(fieldValue).trim()) {
|
||||
const text = String(fieldValue);
|
||||
const captureStep = nestedStep as CaptureTextStep;
|
||||
|
||||
// Use template if provided
|
||||
if (captureStep.output?.template) {
|
||||
itemParts.push(renderTemplate(captureStep.output.template, {
|
||||
text,
|
||||
field: nestedStep.key,
|
||||
value: text,
|
||||
}));
|
||||
} else {
|
||||
// Default: just the text
|
||||
itemParts.push(text);
|
||||
}
|
||||
}
|
||||
} else if (nestedStep.type === "capture_text_line") {
|
||||
if (fieldValue && String(fieldValue).trim()) {
|
||||
const text = String(fieldValue);
|
||||
const captureStep = nestedStep as CaptureTextLineStep;
|
||||
|
||||
// Use template if provided
|
||||
if (captureStep.output?.template) {
|
||||
itemParts.push(renderTemplate(captureStep.output.template, {
|
||||
text,
|
||||
field: nestedStep.key,
|
||||
value: text,
|
||||
}));
|
||||
} else {
|
||||
// Default: just the text
|
||||
itemParts.push(text);
|
||||
}
|
||||
}
|
||||
} else if (nestedStep.type === "capture_frontmatter") {
|
||||
if (fieldValue && String(fieldValue).trim()) {
|
||||
const captureStep = nestedStep as CaptureFrontmatterStep;
|
||||
|
||||
// Use template if provided
|
||||
if (captureStep.output?.template) {
|
||||
itemParts.push(renderTemplate(captureStep.output.template, {
|
||||
text: String(fieldValue),
|
||||
field: captureStep.field,
|
||||
value: String(fieldValue),
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (itemParts.length > 0) {
|
||||
itemOutputs.push(itemParts.join("\n\n"));
|
||||
}
|
||||
}
|
||||
|
||||
if (itemOutputs.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if first nested step has a section header (for backwards compatibility)
|
||||
const firstNestedStep = step.items[0];
|
||||
let sectionHeader: string | null = null;
|
||||
if (firstNestedStep && firstNestedStep.type === "capture_text" && firstNestedStep.section) {
|
||||
sectionHeader = firstNestedStep.section;
|
||||
}
|
||||
|
||||
// Join items with configured separator or default newline
|
||||
const joinStr = step.output?.join || "\n\n";
|
||||
const loopContent = itemOutputs.join(joinStr);
|
||||
|
||||
// Prepend section header if available
|
||||
if (sectionHeader) {
|
||||
return `${sectionHeader}\n\n${loopContent}`;
|
||||
}
|
||||
|
||||
return loopContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render template string with token replacement.
|
||||
* Tokens: {text}, {field}, {value}
|
||||
*/
|
||||
function renderTemplate(template: string, tokens: { text: string; field: string; value: string }): string {
|
||||
return template
|
||||
.replace(/\{text\}/g, tokens.text)
|
||||
.replace(/\{field\}/g, tokens.field)
|
||||
.replace(/\{value\}/g, tokens.value);
|
||||
}
|
||||
|
|
@ -22,6 +22,7 @@ export type InterviewStep =
|
|||
| LLMDialogStep
|
||||
| CaptureFrontmatterStep
|
||||
| CaptureTextStep
|
||||
| CaptureTextLineStep
|
||||
| InstructionStep
|
||||
| ReviewStep;
|
||||
|
||||
|
|
@ -30,6 +31,9 @@ export interface LoopStep {
|
|||
key: string;
|
||||
label?: string;
|
||||
items: InterviewStep[];
|
||||
output?: {
|
||||
join?: string; // String to join items (default: "\n\n")
|
||||
};
|
||||
}
|
||||
|
||||
export interface LLMDialogStep {
|
||||
|
|
@ -50,6 +54,9 @@ export interface CaptureFrontmatterStep {
|
|||
field: string;
|
||||
required?: boolean;
|
||||
prompt?: string; // Optional prompt text
|
||||
output?: {
|
||||
template?: string; // Template with tokens: {text}, {field}, {value}
|
||||
};
|
||||
}
|
||||
|
||||
export interface CaptureTextStep {
|
||||
|
|
@ -59,6 +66,20 @@ export interface CaptureTextStep {
|
|||
required?: boolean;
|
||||
section?: string; // Markdown section header (e.g., "## 🧩 Erlebnisse")
|
||||
prompt?: string; // Optional prompt text
|
||||
output?: {
|
||||
template?: string; // Template with tokens: {text}, {field}, {value}
|
||||
};
|
||||
}
|
||||
|
||||
export interface CaptureTextLineStep {
|
||||
type: "capture_text_line";
|
||||
key: string;
|
||||
label?: string;
|
||||
required?: boolean;
|
||||
prompt?: string; // Optional prompt text
|
||||
output?: {
|
||||
template?: string; // Template with tokens: {text}, {field}, {value}
|
||||
};
|
||||
}
|
||||
|
||||
export interface InstructionStep {
|
||||
|
|
|
|||
322
src/tests/interview/renderer.test.ts
Normal file
322
src/tests/interview/renderer.test.ts
Normal file
|
|
@ -0,0 +1,322 @@
|
|||
import { describe, it, expect } from "vitest";
|
||||
import { renderProfileToMarkdown, type RenderAnswers } from "../../interview/renderer";
|
||||
import type { InterviewProfile } from "../../interview/types";
|
||||
|
||||
describe("renderer", () => {
|
||||
describe("renderProfileToMarkdown", () => {
|
||||
it("should render experience_cluster profile without generic labels", () => {
|
||||
const profile: InterviewProfile = {
|
||||
key: "experience_cluster",
|
||||
label: "Experience Cluster",
|
||||
note_type: "experience",
|
||||
steps: [
|
||||
{
|
||||
type: "loop",
|
||||
key: "experiences",
|
||||
items: [
|
||||
{
|
||||
type: "capture_text",
|
||||
key: "experience_text",
|
||||
label: "Erlebnis",
|
||||
section: "## 🧩 Erlebnisse",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const answers: RenderAnswers = {
|
||||
collectedData: new Map(),
|
||||
loopContexts: new Map([
|
||||
[
|
||||
"experiences",
|
||||
[
|
||||
{ experience_text: "Erstes wichtiges Erlebnis mit Details" },
|
||||
{ experience_text: "Zweites Erlebnis mit mehr Kontext" },
|
||||
],
|
||||
],
|
||||
]),
|
||||
};
|
||||
|
||||
const result = renderProfileToMarkdown(profile, answers);
|
||||
|
||||
// Should contain the section header and both experiences
|
||||
expect(result).toContain("## 🧩 Erlebnisse");
|
||||
expect(result).toContain("Erstes wichtiges Erlebnis mit Details");
|
||||
expect(result).toContain("Zweites Erlebnis mit mehr Kontext");
|
||||
|
||||
// Should NOT contain generic labels
|
||||
expect(result).not.toContain("## Items");
|
||||
expect(result).not.toContain("Überschrift 1");
|
||||
expect(result).not.toContain("Liste 1");
|
||||
expect(result).not.toContain("Erlebnis 1");
|
||||
expect(result).not.toContain("Erlebnis 2");
|
||||
});
|
||||
|
||||
it("should render experience_hub minimal case", () => {
|
||||
const profile: InterviewProfile = {
|
||||
key: "experience_hub",
|
||||
label: "Experience Hub",
|
||||
note_type: "experience",
|
||||
steps: [
|
||||
{
|
||||
type: "loop",
|
||||
key: "items",
|
||||
items: [
|
||||
{
|
||||
type: "capture_text",
|
||||
key: "item_text",
|
||||
label: "Erlebnis",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const answers: RenderAnswers = {
|
||||
collectedData: new Map(),
|
||||
loopContexts: new Map([
|
||||
[
|
||||
"items",
|
||||
[{ item_text: "Ein einzelnes Erlebnis" }],
|
||||
],
|
||||
]),
|
||||
};
|
||||
|
||||
const result = renderProfileToMarkdown(profile, answers);
|
||||
|
||||
// Should contain the experience text
|
||||
expect(result).toContain("Ein einzelnes Erlebnis");
|
||||
|
||||
// Should NOT contain generic labels
|
||||
expect(result).not.toContain("## Items");
|
||||
expect(result).not.toContain("Überschrift");
|
||||
expect(result).not.toContain("Liste");
|
||||
});
|
||||
|
||||
it("should use output.template for capture_text when provided", () => {
|
||||
const profile: InterviewProfile = {
|
||||
key: "test_profile",
|
||||
label: "Test Profile",
|
||||
note_type: "test",
|
||||
steps: [
|
||||
{
|
||||
type: "capture_text",
|
||||
key: "custom_text",
|
||||
label: "Custom Text",
|
||||
output: {
|
||||
template: "### {field}\n\n{text}",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const answers: RenderAnswers = {
|
||||
collectedData: new Map([["custom_text", "Mein Textinhalt"]]),
|
||||
loopContexts: new Map(),
|
||||
};
|
||||
|
||||
const result = renderProfileToMarkdown(profile, answers);
|
||||
|
||||
expect(result).toContain("### custom_text");
|
||||
expect(result).toContain("Mein Textinhalt");
|
||||
});
|
||||
|
||||
it("should use output.join for loop when provided", () => {
|
||||
const profile: InterviewProfile = {
|
||||
key: "test_profile",
|
||||
label: "Test Profile",
|
||||
note_type: "test",
|
||||
steps: [
|
||||
{
|
||||
type: "loop",
|
||||
key: "items",
|
||||
items: [
|
||||
{
|
||||
type: "capture_text",
|
||||
key: "text",
|
||||
label: "Text",
|
||||
},
|
||||
],
|
||||
output: {
|
||||
join: "\n- ",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const answers: RenderAnswers = {
|
||||
collectedData: new Map(),
|
||||
loopContexts: new Map([
|
||||
[
|
||||
"items",
|
||||
[
|
||||
{ text: "Item 1" },
|
||||
{ text: "Item 2" },
|
||||
],
|
||||
],
|
||||
]),
|
||||
};
|
||||
|
||||
const result = renderProfileToMarkdown(profile, answers);
|
||||
|
||||
// Should use custom join separator
|
||||
expect(result).toContain("Item 1\n- Item 2");
|
||||
});
|
||||
|
||||
it("should skip empty values", () => {
|
||||
const profile: InterviewProfile = {
|
||||
key: "test_profile",
|
||||
label: "Test Profile",
|
||||
note_type: "test",
|
||||
steps: [
|
||||
{
|
||||
type: "capture_text",
|
||||
key: "empty_text",
|
||||
label: "Empty Text",
|
||||
},
|
||||
{
|
||||
type: "capture_text",
|
||||
key: "filled_text",
|
||||
label: "Filled Text",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const answers: RenderAnswers = {
|
||||
collectedData: new Map([
|
||||
["empty_text", ""],
|
||||
["filled_text", "Content here"],
|
||||
]),
|
||||
loopContexts: new Map(),
|
||||
};
|
||||
|
||||
const result = renderProfileToMarkdown(profile, answers);
|
||||
|
||||
expect(result).not.toContain("empty_text");
|
||||
expect(result).toContain("Content here");
|
||||
});
|
||||
|
||||
it("should handle section header for capture_text", () => {
|
||||
const profile: InterviewProfile = {
|
||||
key: "test_profile",
|
||||
label: "Test Profile",
|
||||
note_type: "test",
|
||||
steps: [
|
||||
{
|
||||
type: "capture_text",
|
||||
key: "sectioned_text",
|
||||
label: "Sectioned Text",
|
||||
section: "## My Section",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const answers: RenderAnswers = {
|
||||
collectedData: new Map([["sectioned_text", "Content under section"]]),
|
||||
loopContexts: new Map(),
|
||||
};
|
||||
|
||||
const result = renderProfileToMarkdown(profile, answers);
|
||||
|
||||
expect(result).toContain("## My Section");
|
||||
expect(result).toContain("Content under section");
|
||||
});
|
||||
|
||||
it("should render multiple loop items correctly", () => {
|
||||
const profile: InterviewProfile = {
|
||||
key: "test_profile",
|
||||
label: "Test Profile",
|
||||
note_type: "test",
|
||||
steps: [
|
||||
{
|
||||
type: "loop",
|
||||
key: "multi_items",
|
||||
items: [
|
||||
{
|
||||
type: "capture_text",
|
||||
key: "text",
|
||||
label: "Text",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const answers: RenderAnswers = {
|
||||
collectedData: new Map(),
|
||||
loopContexts: new Map([
|
||||
[
|
||||
"multi_items",
|
||||
[
|
||||
{ text: "First item" },
|
||||
{ text: "Second item" },
|
||||
{ text: "Third item" },
|
||||
],
|
||||
],
|
||||
]),
|
||||
};
|
||||
|
||||
const result = renderProfileToMarkdown(profile, answers);
|
||||
|
||||
expect(result).toContain("First item");
|
||||
expect(result).toContain("Second item");
|
||||
expect(result).toContain("Third item");
|
||||
// Should not have item numbers or generic labels
|
||||
expect(result).not.toMatch(/Item \d+/);
|
||||
expect(result).not.toContain("## Items");
|
||||
});
|
||||
|
||||
it("should render capture_text_line step", () => {
|
||||
const profile: InterviewProfile = {
|
||||
key: "test_profile",
|
||||
label: "Test Profile",
|
||||
note_type: "test",
|
||||
steps: [
|
||||
{
|
||||
type: "capture_text_line",
|
||||
key: "single_line",
|
||||
label: "Single Line",
|
||||
prompt: "Enter a single line of text",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const answers: RenderAnswers = {
|
||||
collectedData: new Map([["single_line", "This is a single line"]]),
|
||||
loopContexts: new Map(),
|
||||
};
|
||||
|
||||
const result = renderProfileToMarkdown(profile, answers);
|
||||
|
||||
expect(result).toContain("This is a single line");
|
||||
});
|
||||
|
||||
it("should use output.template for capture_text_line when provided", () => {
|
||||
const profile: InterviewProfile = {
|
||||
key: "test_profile",
|
||||
label: "Test Profile",
|
||||
note_type: "test",
|
||||
steps: [
|
||||
{
|
||||
type: "capture_text_line",
|
||||
key: "tagged_line",
|
||||
label: "Tagged Line",
|
||||
output: {
|
||||
template: "- {text}",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const answers: RenderAnswers = {
|
||||
collectedData: new Map([["tagged_line", "My tag"]]),
|
||||
loopContexts: new Map(),
|
||||
};
|
||||
|
||||
const result = renderProfileToMarkdown(profile, answers);
|
||||
|
||||
expect(result).toBe("- My tag");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -25,6 +25,7 @@ import { extractFrontmatterId } from "../parser/parseFrontmatter";
|
|||
import {
|
||||
createMarkdownToolbar,
|
||||
} from "./markdownToolbar";
|
||||
import { renderProfileToMarkdown, type RenderAnswers } from "../interview/renderer";
|
||||
|
||||
export interface WizardResult {
|
||||
applied: boolean;
|
||||
|
|
@ -189,6 +190,9 @@ export class InterviewWizardModal extends Modal {
|
|||
case "capture_text":
|
||||
this.renderCaptureTextStep(step, stepContentEl);
|
||||
break;
|
||||
case "capture_text_line":
|
||||
this.renderCaptureTextLineStep(step, stepContentEl);
|
||||
break;
|
||||
case "capture_frontmatter":
|
||||
this.renderCaptureFrontmatterStep(step, stepContentEl);
|
||||
break;
|
||||
|
|
@ -372,6 +376,76 @@ export class InterviewWizardModal extends Modal {
|
|||
}
|
||||
}
|
||||
|
||||
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();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update preview container with rendered markdown.
|
||||
*/
|
||||
|
|
@ -643,6 +717,58 @@ export class InterviewWizardModal extends Modal {
|
|||
textEditorContainer.insertBefore(itemToolbar, textEditorContainer.firstChild);
|
||||
}
|
||||
}, 10);
|
||||
} 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";
|
||||
});
|
||||
} else if (nestedStep.type === "capture_frontmatter") {
|
||||
const existingValue = (itemData.get(nestedStep.key) as string) || "";
|
||||
const inputKey = `${itemDataKey}_${nestedStep.key}`;
|
||||
|
|
@ -1038,60 +1164,23 @@ export class InterviewWizardModal extends Modal {
|
|||
}
|
||||
}
|
||||
|
||||
// Apply loop data to content
|
||||
for (const [loopKey, items] of this.state.loopContexts.entries()) {
|
||||
if (items.length > 0) {
|
||||
// Find the loop step to get section info
|
||||
const loopStep = this.state.profile.steps.find(s => s.type === "loop" && s.key === loopKey);
|
||||
if (loopStep && loopStep.type === "loop") {
|
||||
// Get section from first nested step if available (check for section property in YAML)
|
||||
const firstNestedStep = loopStep.items[0];
|
||||
let sectionHeader = "## Items";
|
||||
// Use renderer to generate markdown from collected data
|
||||
const answers: RenderAnswers = {
|
||||
collectedData: this.state.collectedData,
|
||||
loopContexts: this.state.loopContexts,
|
||||
};
|
||||
|
||||
// Check if nested step has section property
|
||||
if (firstNestedStep && firstNestedStep.type === "capture_text" && firstNestedStep.section) {
|
||||
sectionHeader = firstNestedStep.section;
|
||||
}
|
||||
const renderedMarkdown = renderProfileToMarkdown(this.state.profile, answers);
|
||||
|
||||
// Build content for loop items
|
||||
const loopContent: string[] = [];
|
||||
loopContent.push(sectionHeader);
|
||||
loopContent.push("");
|
||||
if (renderedMarkdown.trim()) {
|
||||
// Append rendered markdown to file
|
||||
updatedContent = updatedContent.trimEnd() + "\n\n" + renderedMarkdown;
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const item = items[i];
|
||||
if (item && typeof item === "object") {
|
||||
// Render each field from the item
|
||||
for (const [fieldKey, fieldValue] of Object.entries(item)) {
|
||||
if (fieldValue && String(fieldValue).trim()) {
|
||||
// Find nested step to get label
|
||||
const nestedStep = loopStep.items.find(s => s.key === fieldKey);
|
||||
const label = nestedStep?.label || fieldKey;
|
||||
|
||||
// For multiple items, add item number
|
||||
if (items.length > 1) {
|
||||
loopContent.push(`#### ${label} ${i + 1}`);
|
||||
}
|
||||
loopContent.push(String(fieldValue));
|
||||
loopContent.push("");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Append loop content to file
|
||||
const loopContentStr = loopContent.join("\n");
|
||||
updatedContent = updatedContent.trimEnd() + "\n\n" + loopContentStr;
|
||||
|
||||
console.log("Apply loop content", {
|
||||
loopKey: loopKey,
|
||||
itemsCount: items.length,
|
||||
contentLength: loopContentStr.length,
|
||||
sectionHeader: sectionHeader,
|
||||
console.log("Apply rendered markdown", {
|
||||
contentLength: renderedMarkdown.length,
|
||||
preview: renderedMarkdown.substring(0, 200) + "...",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Write updated content
|
||||
console.log("Write file", {
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user