Implement WP-26 features for Section-Types, Block-IDs, and Edge Suggestions
- Enhanced the interview configuration parsing to support section_type, block_id, and generate_block_id properties. - Updated the renderer to incorporate Section-Types and Block-IDs, allowing for automatic edge suggestions during markdown rendering. - Introduced new RenderOptions for improved handling of graph schema and vocabulary in the rendering process. - Implemented tracking of Section-Info during the wizard flow, including updates for loop items and nested steps. - Enhanced the InterviewWizardModal to support Block-ID selection for intra-note links, improving user experience and functionality.
This commit is contained in:
parent
3be7d617fe
commit
b99416b67d
|
|
@ -320,6 +320,40 @@ function parseStep(raw: Record<string, unknown>): InterviewStep | null {
|
|||
step.prompt = raw.prompt.trim();
|
||||
}
|
||||
|
||||
// WP-26: Parse section_type
|
||||
if (typeof raw.section_type === "string" && raw.section_type.trim()) {
|
||||
step.section_type = raw.section_type.trim();
|
||||
}
|
||||
|
||||
// WP-26: Parse block_id
|
||||
if (typeof raw.block_id === "string" && raw.block_id.trim()) {
|
||||
step.block_id = raw.block_id.trim();
|
||||
}
|
||||
|
||||
// WP-26: Parse generate_block_id
|
||||
if (typeof raw.generate_block_id === "boolean") {
|
||||
step.generate_block_id = raw.generate_block_id;
|
||||
}
|
||||
|
||||
// WP-26: Parse references
|
||||
if (Array.isArray(raw.references)) {
|
||||
step.references = [];
|
||||
for (const refRaw of raw.references) {
|
||||
if (refRaw && typeof refRaw === "object") {
|
||||
const ref = refRaw as Record<string, unknown>;
|
||||
if (typeof ref.block_id === "string" && ref.block_id.trim()) {
|
||||
const reference: { block_id: string; edge_type?: string } = {
|
||||
block_id: ref.block_id.trim(),
|
||||
};
|
||||
if (typeof ref.edge_type === "string" && ref.edge_type.trim()) {
|
||||
reference.edge_type = ref.edge_type.trim();
|
||||
}
|
||||
step.references.push(reference);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse output.template
|
||||
if (raw.output && typeof raw.output === "object") {
|
||||
const output = raw.output as Record<string, unknown>;
|
||||
|
|
@ -363,6 +397,40 @@ function parseStep(raw: Record<string, unknown>): InterviewStep | null {
|
|||
};
|
||||
}
|
||||
|
||||
// WP-26: Parse section_type
|
||||
if (typeof raw.section_type === "string" && raw.section_type.trim()) {
|
||||
step.section_type = raw.section_type.trim();
|
||||
}
|
||||
|
||||
// WP-26: Parse block_id
|
||||
if (typeof raw.block_id === "string" && raw.block_id.trim()) {
|
||||
step.block_id = raw.block_id.trim();
|
||||
}
|
||||
|
||||
// WP-26: Parse generate_block_id
|
||||
if (typeof raw.generate_block_id === "boolean") {
|
||||
step.generate_block_id = raw.generate_block_id;
|
||||
}
|
||||
|
||||
// WP-26: Parse references
|
||||
if (Array.isArray(raw.references)) {
|
||||
step.references = [];
|
||||
for (const refRaw of raw.references) {
|
||||
if (refRaw && typeof refRaw === "object") {
|
||||
const ref = refRaw as Record<string, unknown>;
|
||||
if (typeof ref.block_id === "string" && ref.block_id.trim()) {
|
||||
const reference: { block_id: string; edge_type?: string } = {
|
||||
block_id: ref.block_id.trim(),
|
||||
};
|
||||
if (typeof ref.edge_type === "string" && ref.edge_type.trim()) {
|
||||
reference.edge_type = ref.edge_type.trim();
|
||||
}
|
||||
step.references.push(reference);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse output.template
|
||||
if (raw.output && typeof raw.output === "object") {
|
||||
const output = raw.output as Record<string, unknown>;
|
||||
|
|
|
|||
305
src/interview/renderer.test.ts
Normal file
305
src/interview/renderer.test.ts
Normal file
|
|
@ -0,0 +1,305 @@
|
|||
/**
|
||||
* Tests für WP-26 Interview-Wizard Renderer
|
||||
* Prüft Section-Types, Block-IDs, automatische Edge-Vorschläge und Selbstreferenz-Prüfung
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { renderProfileToMarkdown, type RenderAnswers, type RenderOptions } from "./renderer";
|
||||
import type { InterviewProfile } from "./types";
|
||||
import type { GraphSchema, EdgeTypeHints } from "../mapping/graphSchema";
|
||||
import { Vocabulary } from "../vocab/Vocabulary";
|
||||
import { parseEdgeVocabulary } from "../vocab/parseEdgeVocabulary";
|
||||
|
||||
describe("WP-26 Interview Renderer", () => {
|
||||
// Mock Vocabulary
|
||||
const mockVocabularyText = `| System-Typ (Canonical) | Inverser Typ | Erlaubte Aliasse (User) | Beschreibung |
|
||||
|------------------------|--------------|-------------------------|-------------|
|
||||
| resulted_in | resulted_from| führt zu, bewirkt | Forward edge |
|
||||
| resulted_from | resulted_in | stammt aus, kommt von | Inverse edge |
|
||||
| related_to | related_to | verbunden mit | Bidirectional |`;
|
||||
|
||||
const mockVocabulary = new Vocabulary(parseEdgeVocabulary(mockVocabularyText));
|
||||
|
||||
// Mock GraphSchema
|
||||
const mockGraphSchema: GraphSchema = {
|
||||
schema: new Map<string, Map<string, EdgeTypeHints>>([
|
||||
[
|
||||
"experience",
|
||||
new Map<string, EdgeTypeHints>([
|
||||
[
|
||||
"insight",
|
||||
{
|
||||
typical: ["resulted_in"],
|
||||
prohibited: [],
|
||||
},
|
||||
],
|
||||
]),
|
||||
],
|
||||
]),
|
||||
};
|
||||
|
||||
const mockOptions: RenderOptions = {
|
||||
graphSchema: mockGraphSchema,
|
||||
vocabulary: mockVocabulary,
|
||||
noteType: "experience",
|
||||
};
|
||||
|
||||
it("sollte keine Selbstreferenz bei automatischen Edges generieren", () => {
|
||||
const profile: InterviewProfile = {
|
||||
version: "2.0",
|
||||
frontmatterWhitelist: [],
|
||||
key: "test",
|
||||
label: "Test",
|
||||
note_type: "experience",
|
||||
steps: [
|
||||
{
|
||||
type: "capture_text",
|
||||
key: "context",
|
||||
section: "## Kontext",
|
||||
section_type: "experience",
|
||||
generate_block_id: true,
|
||||
},
|
||||
{
|
||||
type: "capture_text",
|
||||
key: "insight",
|
||||
section: "## Einsicht",
|
||||
section_type: "insight",
|
||||
generate_block_id: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const answers: RenderAnswers = {
|
||||
collectedData: new Map([
|
||||
["context", "Kontext-Text"],
|
||||
["insight", "Einsicht-Text"],
|
||||
]),
|
||||
loopContexts: new Map(),
|
||||
};
|
||||
|
||||
const result = renderProfileToMarkdown(profile, answers, mockOptions);
|
||||
|
||||
// Prüfe, dass keine Selbstreferenz vorhanden ist
|
||||
const contextSection = result.match(/## Kontext.*?## Einsicht/s)?.[0];
|
||||
expect(contextSection).toBeDefined();
|
||||
|
||||
// Prüfe, dass keine Edge auf sich selbst zeigt
|
||||
const selfReference = contextSection?.match(/\[\[#\^context\]\]/);
|
||||
expect(selfReference).toBeNull();
|
||||
|
||||
// Prüfe, dass Edges generiert wurden
|
||||
const edges = result.match(/> \[!edge\]/g);
|
||||
expect(edges?.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("sollte Edge-Types aus graph_schema verwenden", () => {
|
||||
const profile: InterviewProfile = {
|
||||
version: "2.0",
|
||||
frontmatterWhitelist: [],
|
||||
key: "test",
|
||||
label: "Test",
|
||||
note_type: "experience",
|
||||
steps: [
|
||||
{
|
||||
type: "capture_text",
|
||||
key: "experience",
|
||||
section: "## Erfahrung",
|
||||
section_type: "experience",
|
||||
generate_block_id: true,
|
||||
},
|
||||
{
|
||||
type: "capture_text",
|
||||
key: "insight",
|
||||
section: "## Einsicht",
|
||||
section_type: "insight",
|
||||
generate_block_id: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const answers: RenderAnswers = {
|
||||
collectedData: new Map([
|
||||
["experience", "Erfahrungs-Text"],
|
||||
["insight", "Einsicht-Text"],
|
||||
]),
|
||||
loopContexts: new Map(),
|
||||
};
|
||||
|
||||
const result = renderProfileToMarkdown(profile, answers, mockOptions);
|
||||
|
||||
// Debug: Zeige das Ergebnis
|
||||
console.log("Render-Ergebnis:", result);
|
||||
|
||||
// Prüfe, dass Edges generiert wurden
|
||||
const edges = result.match(/> \[!edge\]/g);
|
||||
expect(edges?.length).toBeGreaterThan(0);
|
||||
|
||||
// Prüfe, dass "resulted_in" oder "resulted_from" verwendet wird (aus graph_schema)
|
||||
// Falls graph_schema nicht verfügbar ist, wird "related_to" verwendet (Fallback)
|
||||
const hasResultedEdge = result.match(/resulted_(in|from)/);
|
||||
const hasRelatedEdge = result.match(/related_to/);
|
||||
|
||||
// Mindestens eine Edge sollte vorhanden sein
|
||||
expect(hasResultedEdge || hasRelatedEdge).toBeTruthy();
|
||||
|
||||
// Wenn graph_schema verfügbar ist, sollte "resulted_in" oder "resulted_from" verwendet werden
|
||||
if (mockOptions.graphSchema) {
|
||||
expect(hasResultedEdge).toBeTruthy();
|
||||
}
|
||||
});
|
||||
|
||||
it("sollte Backlinks automatisch generieren", () => {
|
||||
const profile: InterviewProfile = {
|
||||
version: "2.0",
|
||||
frontmatterWhitelist: [],
|
||||
key: "test",
|
||||
label: "Test",
|
||||
note_type: "experience",
|
||||
steps: [
|
||||
{
|
||||
type: "capture_text",
|
||||
key: "context",
|
||||
section: "## Kontext",
|
||||
section_type: "experience",
|
||||
generate_block_id: true,
|
||||
},
|
||||
{
|
||||
type: "capture_text",
|
||||
key: "insight",
|
||||
section: "## Einsicht",
|
||||
section_type: "insight",
|
||||
generate_block_id: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const answers: RenderAnswers = {
|
||||
collectedData: new Map([
|
||||
["context", "Kontext-Text"],
|
||||
["insight", "Einsicht-Text"],
|
||||
]),
|
||||
loopContexts: new Map(),
|
||||
};
|
||||
|
||||
const result = renderProfileToMarkdown(profile, answers, mockOptions);
|
||||
|
||||
// Prüfe, dass beide Edge-Types vorhanden sind (Forward und Backward)
|
||||
const forwardEdge = result.match(/resulted_in/);
|
||||
const backwardEdge = result.match(/resulted_from/);
|
||||
|
||||
expect(forwardEdge).toBeDefined();
|
||||
expect(backwardEdge).toBeDefined();
|
||||
});
|
||||
|
||||
it("sollte keine Selbstreferenz bei expliziten Referenzen erlauben", () => {
|
||||
const profile: InterviewProfile = {
|
||||
version: "2.0",
|
||||
frontmatterWhitelist: [],
|
||||
key: "test",
|
||||
label: "Test",
|
||||
note_type: "experience",
|
||||
steps: [
|
||||
{
|
||||
type: "capture_text",
|
||||
key: "context",
|
||||
section: "## Kontext",
|
||||
section_type: "experience",
|
||||
block_id: "context",
|
||||
references: [
|
||||
{
|
||||
block_id: "context", // Selbstreferenz - sollte verhindert werden
|
||||
edge_type: "related_to",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const answers: RenderAnswers = {
|
||||
collectedData: new Map([
|
||||
["context", "Kontext-Text"],
|
||||
]),
|
||||
loopContexts: new Map(),
|
||||
};
|
||||
|
||||
const result = renderProfileToMarkdown(profile, answers, mockOptions);
|
||||
|
||||
// Prüfe, dass keine Selbstreferenz generiert wurde
|
||||
const selfReference = result.match(/\[\[#\^context\]\]/);
|
||||
// Sollte null sein, da Selbstreferenz verhindert wird
|
||||
expect(selfReference).toBeNull();
|
||||
});
|
||||
|
||||
it("sollte Section-Type-Callout direkt nach Heading platzieren", () => {
|
||||
const profile: InterviewProfile = {
|
||||
version: "2.0",
|
||||
frontmatterWhitelist: [],
|
||||
key: "test",
|
||||
label: "Test",
|
||||
note_type: "experience",
|
||||
steps: [
|
||||
{
|
||||
type: "capture_text",
|
||||
key: "context",
|
||||
section: "## Kontext",
|
||||
section_type: "experience",
|
||||
generate_block_id: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const answers: RenderAnswers = {
|
||||
collectedData: new Map([
|
||||
["context", "Kontext-Text"],
|
||||
]),
|
||||
loopContexts: new Map(),
|
||||
};
|
||||
|
||||
const result = renderProfileToMarkdown(profile, answers, mockOptions);
|
||||
|
||||
// Prüfe, dass Section-Type-Callout direkt nach Heading steht
|
||||
const sectionPattern = /## Kontext.*?\n> \[!section\] experience/s;
|
||||
expect(result).toMatch(sectionPattern);
|
||||
});
|
||||
|
||||
it("sollte Edges am Ende der Sektion platzieren", () => {
|
||||
const profile: InterviewProfile = {
|
||||
version: "2.0",
|
||||
frontmatterWhitelist: [],
|
||||
key: "test",
|
||||
label: "Test",
|
||||
note_type: "experience",
|
||||
steps: [
|
||||
{
|
||||
type: "capture_text",
|
||||
key: "context",
|
||||
section: "## Kontext",
|
||||
section_type: "experience",
|
||||
generate_block_id: true,
|
||||
},
|
||||
{
|
||||
type: "capture_text",
|
||||
key: "insight",
|
||||
section: "## Einsicht",
|
||||
section_type: "insight",
|
||||
generate_block_id: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const answers: RenderAnswers = {
|
||||
collectedData: new Map([
|
||||
["context", "Kontext-Text"],
|
||||
["insight", "Einsicht-Text"],
|
||||
]),
|
||||
loopContexts: new Map(),
|
||||
};
|
||||
|
||||
const result = renderProfileToMarkdown(profile, answers, mockOptions);
|
||||
|
||||
// Prüfe, dass Edges am Ende der Einsicht-Sektion stehen (nach dem Text)
|
||||
const insightSection = result.match(/## Einsicht.*?Einsicht-Text\n\n(> \[!edge\].*)/s)?.[1];
|
||||
expect(insightSection).toBeDefined();
|
||||
expect(insightSection).toMatch(/> \[!edge\]/);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,27 +1,68 @@
|
|||
/**
|
||||
* Markdown renderer for interview profiles.
|
||||
* Converts collected answers into markdown output based on profile configuration.
|
||||
* WP-26: Erweitert um Section-Types, Block-IDs und automatische Edge-Vorschläge.
|
||||
*/
|
||||
|
||||
import type { InterviewProfile, InterviewStep, CaptureTextStep, CaptureTextLineStep, CaptureFrontmatterStep, LoopStep, EntityPickerStep } from "./types";
|
||||
import type { SectionInfo } from "./wizardState";
|
||||
import type { GraphSchema } from "../mapping/graphSchema";
|
||||
import type { Vocabulary } from "../vocab/Vocabulary";
|
||||
import { slugify } from "./slugify";
|
||||
import { getHints } from "../mapping/graphSchema";
|
||||
|
||||
export interface RenderAnswers {
|
||||
collectedData: Map<string, unknown>;
|
||||
loopContexts: Map<string, unknown[]>;
|
||||
// WP-26: Optional - Section-Sequenz für automatische Edge-Vorschläge
|
||||
sectionSequence?: SectionInfo[];
|
||||
}
|
||||
|
||||
// WP-26: Render-Optionen für Section-Types und Edge-Vorschläge
|
||||
export interface RenderOptions {
|
||||
graphSchema?: GraphSchema | null;
|
||||
vocabulary?: Vocabulary | null;
|
||||
noteType?: string; // Fallback für effective_type
|
||||
}
|
||||
|
||||
/**
|
||||
* Render profile answers to markdown string.
|
||||
* Deterministic and testable.
|
||||
* WP-26: Erweitert um Section-Types, Block-IDs und automatische Edge-Vorschläge.
|
||||
*/
|
||||
export function renderProfileToMarkdown(
|
||||
profile: InterviewProfile,
|
||||
answers: RenderAnswers
|
||||
answers: RenderAnswers,
|
||||
options?: RenderOptions
|
||||
): string {
|
||||
const output: string[] = [];
|
||||
|
||||
// WP-26: Verwende übergebene Section-Sequenz oder erstelle neue
|
||||
// Die Section-Sequenz wird während des interaktiven Wizard-Durchlaufs getrackt
|
||||
const sectionSequence: SectionInfo[] = answers.sectionSequence ? [...answers.sectionSequence] : [];
|
||||
const generatedBlockIds = new Map<string, SectionInfo>();
|
||||
|
||||
// WP-26: Wenn Section-Sequenz übergeben wurde, fülle generatedBlockIds aus der Sequenz
|
||||
if (answers.sectionSequence && answers.sectionSequence.length > 0) {
|
||||
for (const sectionInfo of answers.sectionSequence) {
|
||||
if (sectionInfo.blockId) {
|
||||
generatedBlockIds.set(sectionInfo.blockId, sectionInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WP-26: Track Section-Sequenz während des Renderns (für Steps, die noch nicht getrackt wurden)
|
||||
for (const step of profile.steps) {
|
||||
const stepOutput = renderStep(step, answers);
|
||||
const stepOutput = renderStep(
|
||||
step,
|
||||
answers,
|
||||
{
|
||||
profile,
|
||||
sectionSequence,
|
||||
generatedBlockIds,
|
||||
options: options || {},
|
||||
}
|
||||
);
|
||||
if (stepOutput) {
|
||||
output.push(stepOutput);
|
||||
}
|
||||
|
|
@ -30,20 +71,33 @@ export function renderProfileToMarkdown(
|
|||
return output.join("\n\n").trim();
|
||||
}
|
||||
|
||||
// WP-26: Render-Kontext für Section-Types und Block-IDs
|
||||
interface RenderContext {
|
||||
profile: InterviewProfile;
|
||||
sectionSequence: SectionInfo[];
|
||||
generatedBlockIds: Map<string, SectionInfo>;
|
||||
options: RenderOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a single step to markdown.
|
||||
* WP-26: Erweitert um RenderContext für Section-Types und Block-IDs.
|
||||
*/
|
||||
function renderStep(step: InterviewStep, answers: RenderAnswers): string | null {
|
||||
function renderStep(
|
||||
step: InterviewStep,
|
||||
answers: RenderAnswers,
|
||||
context: RenderContext
|
||||
): string | null {
|
||||
switch (step.type) {
|
||||
case "capture_text":
|
||||
return renderCaptureText(step, answers);
|
||||
return renderCaptureText(step, answers, context);
|
||||
case "capture_text_line":
|
||||
return renderCaptureTextLine(step, answers);
|
||||
return renderCaptureTextLine(step, answers, context);
|
||||
case "capture_frontmatter":
|
||||
// Frontmatter is handled separately, skip here
|
||||
return null;
|
||||
case "loop":
|
||||
return renderLoop(step, answers);
|
||||
return renderLoop(step, answers, context);
|
||||
case "entity_picker":
|
||||
return renderEntityPicker(step, answers);
|
||||
case "instruction":
|
||||
|
|
@ -58,8 +112,13 @@ function renderStep(step: InterviewStep, answers: RenderAnswers): string | null
|
|||
|
||||
/**
|
||||
* Render capture_text step.
|
||||
* WP-26: Erweitert um Section-Types, Block-IDs und Edge-Vorschläge.
|
||||
*/
|
||||
function renderCaptureText(step: CaptureTextStep, answers: RenderAnswers): string | null {
|
||||
function renderCaptureText(
|
||||
step: CaptureTextStep,
|
||||
answers: RenderAnswers,
|
||||
context: RenderContext
|
||||
): string | null {
|
||||
const value = answers.collectedData.get(step.key);
|
||||
if (!value || String(value).trim() === "") {
|
||||
return null;
|
||||
|
|
@ -67,18 +126,66 @@ function renderCaptureText(step: CaptureTextStep, answers: RenderAnswers): strin
|
|||
|
||||
const text = String(value);
|
||||
|
||||
// WP-26: Block-ID-Generierung
|
||||
let blockId: string | null = null;
|
||||
if (step.block_id) {
|
||||
blockId = step.block_id;
|
||||
} else if (step.generate_block_id) {
|
||||
blockId = slugify(step.key);
|
||||
}
|
||||
|
||||
// WP-26: Heading mit Block-ID erweitern
|
||||
let heading = step.section || "";
|
||||
if (heading && blockId) {
|
||||
// Füge Block-ID zu Heading hinzu: "## Heading" -> "## Heading ^block-id"
|
||||
heading = heading.replace(/^(\s*#{1,6}\s+)(.+)$/, `$1$2 ^${blockId}`);
|
||||
}
|
||||
|
||||
// WP-26: Section-Type-Callout generieren (direkt nach Heading)
|
||||
const sectionTypeCallout = step.section_type
|
||||
? `> [!section] ${step.section_type}`
|
||||
: "";
|
||||
|
||||
// WP-26: Section-Info für Tracking
|
||||
const sectionInfo: SectionInfo = {
|
||||
stepKey: step.key,
|
||||
sectionType: step.section_type || null,
|
||||
heading: heading || step.key,
|
||||
blockId: blockId,
|
||||
noteType: context.options.noteType || context.profile.note_type,
|
||||
};
|
||||
|
||||
// WP-26: Block-ID tracken
|
||||
if (blockId) {
|
||||
context.generatedBlockIds.set(blockId, sectionInfo);
|
||||
}
|
||||
|
||||
// WP-26: Section-Sequenz aktualisieren (nur wenn Section vorhanden)
|
||||
if (step.section) {
|
||||
context.sectionSequence.push(sectionInfo);
|
||||
}
|
||||
|
||||
// WP-26: Referenzen generieren (am Ende der Sektion)
|
||||
const references = renderReferences(step.references || [], context, sectionInfo);
|
||||
|
||||
// WP-26: Automatische Edge-Vorschläge generieren (am Ende der Sektion)
|
||||
const autoEdges = renderAutomaticEdges(sectionInfo, context);
|
||||
|
||||
// Use template if provided
|
||||
if (step.output?.template) {
|
||||
return renderTemplate(step.output.template, {
|
||||
const templateText = renderTemplate(step.output.template, {
|
||||
text,
|
||||
field: step.key,
|
||||
value: text,
|
||||
});
|
||||
|
||||
// WP-26: Template mit Heading, Section-Type und Edges kombinieren
|
||||
return combineSectionParts(heading, sectionTypeCallout, templateText, references, autoEdges);
|
||||
}
|
||||
|
||||
// Default: use section if provided, otherwise just the text
|
||||
if (step.section) {
|
||||
return `${step.section}\n\n${text}`;
|
||||
return combineSectionParts(heading, sectionTypeCallout, text, references, autoEdges);
|
||||
}
|
||||
|
||||
return text;
|
||||
|
|
@ -86,8 +193,13 @@ function renderCaptureText(step: CaptureTextStep, answers: RenderAnswers): strin
|
|||
|
||||
/**
|
||||
* Render capture_text_line step.
|
||||
* WP-26: Erweitert um Section-Types, Block-IDs und Edge-Vorschläge.
|
||||
*/
|
||||
function renderCaptureTextLine(step: CaptureTextLineStep, answers: RenderAnswers): string | null {
|
||||
function renderCaptureTextLine(
|
||||
step: CaptureTextLineStep,
|
||||
answers: RenderAnswers,
|
||||
context: RenderContext
|
||||
): string | null {
|
||||
const value = answers.collectedData.get(step.key);
|
||||
if (!value || String(value).trim() === "") {
|
||||
return null;
|
||||
|
|
@ -110,14 +222,98 @@ function renderCaptureTextLine(step: CaptureTextLineStep, answers: RenderAnswers
|
|||
}
|
||||
}
|
||||
|
||||
// WP-26: Block-ID-Generierung
|
||||
let blockId: string | null = null;
|
||||
if (step.block_id) {
|
||||
blockId = step.block_id;
|
||||
} else if (step.generate_block_id) {
|
||||
blockId = slugify(step.key);
|
||||
}
|
||||
|
||||
// WP-26: Heading mit Block-ID erweitern (wenn heading_prefix vorhanden)
|
||||
let heading = "";
|
||||
if (headingPrefix) {
|
||||
heading = headingPrefix + text;
|
||||
if (blockId) {
|
||||
// Füge Block-ID zu Heading hinzu
|
||||
heading = heading.replace(/^(\s*#{1,6}\s+)(.+)$/, `$1$2 ^${blockId}`);
|
||||
}
|
||||
}
|
||||
|
||||
// WP-26: Section-Type-Callout generieren (direkt nach Heading)
|
||||
const sectionTypeCallout = step.section_type
|
||||
? `> [!section] ${step.section_type}`
|
||||
: "";
|
||||
|
||||
// WP-26: Section-Info für Tracking (nur wenn Heading vorhanden)
|
||||
if (headingPrefix) {
|
||||
const sectionInfo: SectionInfo = {
|
||||
stepKey: step.key,
|
||||
sectionType: step.section_type || null,
|
||||
heading: heading || text,
|
||||
blockId: blockId,
|
||||
noteType: context.options.noteType || context.profile.note_type,
|
||||
};
|
||||
|
||||
// WP-26: Block-ID tracken
|
||||
if (blockId) {
|
||||
context.generatedBlockIds.set(blockId, sectionInfo);
|
||||
}
|
||||
|
||||
// WP-26: Section-Sequenz aktualisieren
|
||||
context.sectionSequence.push(sectionInfo);
|
||||
}
|
||||
|
||||
// WP-26: Section-Info für Tracking (nur wenn Heading vorhanden)
|
||||
let sectionInfo: SectionInfo | null = null;
|
||||
if (headingPrefix) {
|
||||
sectionInfo = {
|
||||
stepKey: step.key,
|
||||
sectionType: step.section_type || null,
|
||||
heading: heading || text,
|
||||
blockId: blockId,
|
||||
noteType: context.options.noteType || context.profile.note_type,
|
||||
};
|
||||
|
||||
// WP-26: Block-ID tracken
|
||||
if (blockId) {
|
||||
context.generatedBlockIds.set(blockId, sectionInfo);
|
||||
}
|
||||
|
||||
// WP-26: Section-Sequenz aktualisieren
|
||||
context.sectionSequence.push(sectionInfo);
|
||||
}
|
||||
|
||||
// WP-26: Referenzen generieren (am Ende der Sektion)
|
||||
const references = sectionInfo
|
||||
? renderReferences(step.references || [], context, sectionInfo)
|
||||
: "";
|
||||
|
||||
// WP-26: Automatische Edge-Vorschläge generieren (nur wenn Heading vorhanden)
|
||||
const autoEdges = sectionInfo
|
||||
? renderAutomaticEdges(sectionInfo, context)
|
||||
: "";
|
||||
|
||||
// Use template if provided
|
||||
if (step.output?.template) {
|
||||
return renderTemplate(step.output.template, {
|
||||
const templateText = renderTemplate(step.output.template, {
|
||||
text: headingPrefix + text,
|
||||
field: step.key,
|
||||
value: headingPrefix + text,
|
||||
heading_level: headingLevel ? String(headingLevel) : "",
|
||||
});
|
||||
|
||||
// WP-26: Template mit Heading, Section-Type und Edges kombinieren (wenn Heading vorhanden)
|
||||
if (headingPrefix) {
|
||||
return combineSectionParts(heading, sectionTypeCallout, "", references, autoEdges);
|
||||
}
|
||||
|
||||
return templateText;
|
||||
}
|
||||
|
||||
// WP-26: Wenn Heading vorhanden, mit Section-Type und Edges kombinieren
|
||||
if (headingPrefix) {
|
||||
return combineSectionParts(heading, sectionTypeCallout, "", references, autoEdges);
|
||||
}
|
||||
|
||||
// Default: text with heading prefix if configured
|
||||
|
|
@ -175,16 +371,23 @@ function renderCaptureFrontmatter(step: CaptureFrontmatterStep, answers: RenderA
|
|||
|
||||
/**
|
||||
* Render loop step (recursive, supports arbitrary nesting depth).
|
||||
* WP-26: Erweitert um RenderContext für Section-Types und Block-IDs.
|
||||
*/
|
||||
function renderLoop(step: LoopStep, answers: RenderAnswers): string | null {
|
||||
return renderLoopRecursive(step, answers, 0);
|
||||
function renderLoop(step: LoopStep, answers: RenderAnswers, context: RenderContext): string | null {
|
||||
return renderLoopRecursive(step, answers, context, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursive helper to render loop items with nested steps.
|
||||
* Supports arbitrary nesting depth (no hard limit, but practical limits apply due to stack size).
|
||||
* WP-26: Erweitert um RenderContext für Section-Types und Block-IDs.
|
||||
*/
|
||||
function renderLoopRecursive(step: LoopStep, answers: RenderAnswers, depth: number): string | null {
|
||||
function renderLoopRecursive(
|
||||
step: LoopStep,
|
||||
answers: RenderAnswers,
|
||||
context: RenderContext,
|
||||
depth: number
|
||||
): string | null {
|
||||
// Safety check: prevent infinite recursion (practical limit: 100 levels)
|
||||
if (depth > 100) {
|
||||
console.warn(`Loop nesting depth ${depth} exceeds practical limit, stopping recursion`);
|
||||
|
|
@ -214,49 +417,39 @@ function renderLoopRecursive(step: LoopStep, answers: RenderAnswers, depth: numb
|
|||
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);
|
||||
// WP-26: Verwende erweiterte renderCaptureText Funktion
|
||||
const rendered = renderCaptureText(captureStep, {
|
||||
collectedData: new Map([[nestedStep.key, fieldValue]]),
|
||||
loopContexts: answers.loopContexts,
|
||||
}, context);
|
||||
|
||||
if (rendered) {
|
||||
itemParts.push(rendered);
|
||||
}
|
||||
}
|
||||
} else if (nestedStep.type === "capture_text_line") {
|
||||
if (fieldValue && String(fieldValue).trim()) {
|
||||
const text = String(fieldValue);
|
||||
const captureStep = nestedStep as CaptureTextLineStep;
|
||||
|
||||
// Get heading level if configured (for nested loops, check in item data)
|
||||
// WP-26: Erstelle temporäres collectedData mit allen Item-Daten
|
||||
const itemData = new Map<string, unknown>();
|
||||
itemData.set(nestedStep.key, fieldValue);
|
||||
|
||||
// Füge heading_level hinzu falls vorhanden
|
||||
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) + " ";
|
||||
}
|
||||
if (headingLevel !== undefined) {
|
||||
itemData.set(headingLevelKey, headingLevel);
|
||||
}
|
||||
|
||||
// Use template if provided
|
||||
if (captureStep.output?.template) {
|
||||
itemParts.push(renderTemplate(captureStep.output.template, {
|
||||
text: headingPrefix + text,
|
||||
field: nestedStep.key,
|
||||
value: headingPrefix + text,
|
||||
heading_level: headingLevel ? String(headingLevel) : (captureStep.heading_level?.default ? String(captureStep.heading_level.default) : ""),
|
||||
}));
|
||||
} else {
|
||||
// Default: text with heading prefix if configured
|
||||
itemParts.push(headingPrefix + text);
|
||||
// WP-26: Verwende erweiterte renderCaptureTextLine Funktion
|
||||
const rendered = renderCaptureTextLine(captureStep, {
|
||||
collectedData: itemData,
|
||||
loopContexts: answers.loopContexts,
|
||||
}, context);
|
||||
|
||||
if (rendered) {
|
||||
itemParts.push(rendered);
|
||||
}
|
||||
}
|
||||
} else if (nestedStep.type === "capture_frontmatter") {
|
||||
|
|
@ -287,7 +480,7 @@ function renderLoopRecursive(step: LoopStep, answers: RenderAnswers, depth: numb
|
|||
nestedAnswers.loopContexts.set(nestedLoopStep.key, fieldValue);
|
||||
|
||||
// Recursively render the nested loop
|
||||
const nestedOutput = renderLoopRecursive(nestedLoopStep, nestedAnswers, depth + 1);
|
||||
const nestedOutput = renderLoopRecursive(nestedLoopStep, nestedAnswers, context, depth + 1);
|
||||
if (nestedOutput) {
|
||||
itemParts.push(nestedOutput);
|
||||
}
|
||||
|
|
@ -339,3 +532,166 @@ function renderTemplate(template: string, tokens: { text: string; field: string;
|
|||
|
||||
return result;
|
||||
}
|
||||
|
||||
// WP-26: Helper-Funktionen für Section-Types und Block-IDs
|
||||
|
||||
/**
|
||||
* Kombiniert Heading, Section-Type-Callout, Content und Edges zu einer Section.
|
||||
* Formatierungsregel: Section-Type direkt nach Heading, Edges am Ende.
|
||||
*/
|
||||
function combineSectionParts(
|
||||
heading: string,
|
||||
sectionTypeCallout: string,
|
||||
content: string,
|
||||
references: string,
|
||||
autoEdges: string
|
||||
): string {
|
||||
const parts: string[] = [];
|
||||
|
||||
if (heading) {
|
||||
parts.push(heading);
|
||||
}
|
||||
|
||||
if (sectionTypeCallout) {
|
||||
parts.push(sectionTypeCallout);
|
||||
}
|
||||
|
||||
if (content) {
|
||||
parts.push(content);
|
||||
}
|
||||
|
||||
// WP-26: Edges am Ende der Sektion (Referenzen + automatische Edges)
|
||||
const edges = [references, autoEdges].filter(e => e.trim()).join("\n");
|
||||
if (edges) {
|
||||
parts.push(edges);
|
||||
}
|
||||
|
||||
return parts.join("\n\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert Edge-Callouts für explizite Referenzen.
|
||||
* Format: > [!edge] <edge_type>\n> [[#^block-id]]
|
||||
*/
|
||||
function renderReferences(
|
||||
references: Array<{ block_id: string; edge_type?: string }>,
|
||||
context: RenderContext,
|
||||
currentSection: SectionInfo
|
||||
): string {
|
||||
if (!references || references.length === 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const edgeCallouts: string[] = [];
|
||||
|
||||
for (const ref of references) {
|
||||
// Prüfe, ob Block-ID existiert
|
||||
if (!context.generatedBlockIds.has(ref.block_id)) {
|
||||
console.warn(`[WP-26] Block-ID "${ref.block_id}" nicht gefunden für Referenz`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// WP-26: Prüfe auf Selbstreferenz
|
||||
if (ref.block_id === currentSection.blockId) {
|
||||
console.warn(`[WP-26] Selbstreferenz verhindert: Block-ID "${ref.block_id}" zeigt auf sich selbst`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const edgeType = ref.edge_type || "related_to";
|
||||
edgeCallouts.push(`> [!edge] ${edgeType}`);
|
||||
edgeCallouts.push(`> [[#^${ref.block_id}]]`);
|
||||
}
|
||||
|
||||
return edgeCallouts.join("\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert automatische Edge-Vorschläge zwischen Sections.
|
||||
* Verwendet graph_schema.md für typische Edge-Types und edge_vocabulary.md für inverse Edges.
|
||||
*
|
||||
* Formatierungsregel: Edges werden am Ende der aktuellen Section eingefügt.
|
||||
* Forward-Edge: prevSection -> currentSection (in aktueller Section)
|
||||
* Rückwärts-Edge: currentSection -> prevSection (in aktueller Section, inverser Edge-Type)
|
||||
*/
|
||||
function renderAutomaticEdges(
|
||||
currentSection: SectionInfo,
|
||||
context: RenderContext
|
||||
): string {
|
||||
// Nur wenn aktuelle Section eine Block-ID hat
|
||||
if (!currentSection.blockId) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// Nur wenn es vorherige Sections gibt
|
||||
if (context.sectionSequence.length <= 1) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// Aktuelle Section ist die letzte in der Sequenz
|
||||
// Wir generieren Edges von allen vorherigen Sections zur aktuellen Section
|
||||
const prevSections = context.sectionSequence.slice(0, -1);
|
||||
|
||||
const edgeCallouts: string[] = [];
|
||||
const { graphSchema, vocabulary } = context.options;
|
||||
|
||||
for (const prevSection of prevSections) {
|
||||
// Nur wenn vorherige Section auch eine Block-ID hat
|
||||
if (!prevSection.blockId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// WP-26: Prüfe auf Selbstreferenz (sollte nicht vorkommen, aber sicherheitshalber prüfen)
|
||||
if (prevSection.blockId === currentSection.blockId) {
|
||||
console.warn(`[WP-26] Selbstreferenz verhindert: Block-ID "${prevSection.blockId}" zeigt auf sich selbst`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Ermittle effective_types
|
||||
const prevType = prevSection.sectionType || prevSection.noteType;
|
||||
const currentType = currentSection.sectionType || currentSection.noteType;
|
||||
|
||||
// Lookup in graph_schema.md: prevType -> currentType
|
||||
let forwardEdgeType: string | null = null;
|
||||
if (graphSchema && prevType && currentType) {
|
||||
const hints = getHints(graphSchema, prevType, currentType);
|
||||
if (hints.typical.length > 0) {
|
||||
forwardEdgeType = hints.typical[0]; // Erster typischer Edge-Type aus graph_schema
|
||||
} else {
|
||||
// Debug: Log wenn keine typischen Edges gefunden wurden
|
||||
console.debug(`[WP-26] Keine typischen Edges gefunden für ${prevType} -> ${currentType}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: related_to
|
||||
if (!forwardEdgeType) {
|
||||
forwardEdgeType = "related_to";
|
||||
}
|
||||
|
||||
// WP-26: Automatische Edge-Vorschläge
|
||||
// Wir generieren beide Richtungen:
|
||||
// 1. Forward-Edge: prevSection -> currentSection (mit originalem Edge-Type)
|
||||
// 2. Rückwärts-Edge: currentSection -> prevSection (mit inversem Edge-Type)
|
||||
//
|
||||
// Beide Edges werden in der aktuellen Section eingefügt und zeigen zur prevSection
|
||||
// Der Backlink wird automatisch mitgesetzt
|
||||
|
||||
// Rückwärts-Edge: currentSection -> prevSection (mit inversem Edge-Type)
|
||||
let inverseEdgeType: string | null = null;
|
||||
if (vocabulary) {
|
||||
inverseEdgeType = vocabulary.getInverse(forwardEdgeType);
|
||||
}
|
||||
|
||||
// Generiere Rückwärts-Edge (currentSection -> prevSection)
|
||||
if (inverseEdgeType) {
|
||||
edgeCallouts.push(`> [!edge] ${inverseEdgeType}`);
|
||||
edgeCallouts.push(`> [[#^${prevSection.blockId}]]`);
|
||||
}
|
||||
|
||||
// Generiere Forward-Edge als Backlink (prevSection -> currentSection)
|
||||
// Diese Edge beschreibt die Beziehung von prevSection zu currentSection
|
||||
edgeCallouts.push(`> [!edge] ${forwardEdgeType}`);
|
||||
edgeCallouts.push(`> [[#^${prevSection.blockId}]]`);
|
||||
}
|
||||
|
||||
return edgeCallouts.join("\n");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -76,6 +76,14 @@ export interface CaptureTextStep {
|
|||
required?: boolean;
|
||||
section?: string; // Markdown section header (e.g., "## 🧩 Erlebnisse")
|
||||
prompt?: string; // Optional prompt text
|
||||
// WP-26: Section-Type und Block-ID Support
|
||||
section_type?: string; // Optional: Section-Type (z.B. "experience", "insight")
|
||||
block_id?: string; // Optional: Explizite Block-ID (z.B. "sit")
|
||||
generate_block_id?: boolean; // Optional: Automatische Block-ID-Generierung aus Step-Key
|
||||
references?: Array<{ // Optional: Referenzen zu vorherigen Sections
|
||||
block_id: string; // Block-ID der referenzierten Section
|
||||
edge_type?: string; // Optional: Vorgeschlagener Edge-Type
|
||||
}>;
|
||||
output?: {
|
||||
template?: string; // Template with tokens: {text}, {field}, {value}
|
||||
};
|
||||
|
|
@ -91,6 +99,14 @@ export interface CaptureTextLineStep {
|
|||
enabled?: boolean; // Show heading level selector (default: false)
|
||||
default?: number; // Default heading level 1-6 (default: 2)
|
||||
};
|
||||
// WP-26: Section-Type und Block-ID Support
|
||||
section_type?: string; // Optional: Section-Type (z.B. "experience", "insight")
|
||||
block_id?: string; // Optional: Explizite Block-ID (z.B. "sit")
|
||||
generate_block_id?: boolean; // Optional: Automatische Block-ID-Generierung aus Step-Key
|
||||
references?: Array<{ // Optional: Referenzen zu vorherigen Sections
|
||||
block_id: string; // Block-ID der referenzierten Section
|
||||
edge_type?: string; // Optional: Vorgeschlagener Edge-Type
|
||||
}>;
|
||||
output?: {
|
||||
template?: string; // Template with tokens: {text}, {field}, {value}, {heading_level}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -50,6 +50,9 @@ export function createWizardState(profile: InterviewProfile): WizardState {
|
|||
patches: [],
|
||||
activeLoopPath: [], // Start at top level
|
||||
pendingEdgeAssignments: [], // Start with empty pending assignments
|
||||
// WP-26: Initialize Section-Type und Block-ID Tracking
|
||||
generatedBlockIds: new Map(),
|
||||
sectionSequence: [],
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,11 @@ import {
|
|||
createMarkdownToolbar,
|
||||
} from "./markdownToolbar";
|
||||
import { detectEdgeSelectorContext, changeEdgeTypeForLinks } from "../mapping/edgeTypeSelector";
|
||||
import { renderProfileToMarkdown, type RenderAnswers } from "../interview/renderer";
|
||||
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";
|
||||
import { NoteIndex } from "../entityPicker/noteIndex";
|
||||
import { EntityPickerModal, type EntityPickerResult } from "./EntityPickerModal";
|
||||
import { insertWikilinkIntoTextarea } from "../entityPicker/wikilink";
|
||||
|
|
@ -176,8 +180,18 @@ export class InterviewWizardModal extends Modal {
|
|||
stepKey: step?.key || "null",
|
||||
stepLabel: step?.label || "null",
|
||||
totalSteps: flattenSteps(this.state.profile.steps).length,
|
||||
sectionSequenceLength: this.state.sectionSequence.length,
|
||||
generatedBlockIdsCount: this.state.generatedBlockIds.size,
|
||||
});
|
||||
|
||||
// WP-26: Track Section-Info während des Wizard-Durchlaufs
|
||||
if (step) {
|
||||
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`);
|
||||
}
|
||||
|
||||
if (!step) {
|
||||
// Check if we're at the end legitimately or if there's an error
|
||||
const steps = flattenSteps(this.state.profile.steps);
|
||||
|
|
@ -415,10 +429,30 @@ export class InterviewWizardModal extends Modal {
|
|||
valuePreview: value.substring(0, 50) + (value.length > 50 ? "..." : ""),
|
||||
});
|
||||
|
||||
// WP-26: Speichere Fokus-Info vor State-Update, um Fokus-Verlust zu vermeiden
|
||||
let hadFocus = false;
|
||||
let selectionStart = 0;
|
||||
let selectionEnd = 0;
|
||||
if (document.activeElement === textareaRef) {
|
||||
hadFocus = true;
|
||||
selectionStart = textareaRef.selectionStart;
|
||||
selectionEnd = textareaRef.selectionEnd;
|
||||
}
|
||||
|
||||
// Update stored value
|
||||
this.currentInputValues.set(step.key, value);
|
||||
this.state.collectedData.set(step.key, value);
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// Update preview if in preview mode
|
||||
if (isPreviewMode) {
|
||||
this.updatePreview(previewContainer, value);
|
||||
|
|
@ -469,44 +503,158 @@ export class InterviewWizardModal extends Modal {
|
|||
backToEditWrapper.style.display = "none";
|
||||
}
|
||||
},
|
||||
(app: App) => {
|
||||
// Open entity picker modal
|
||||
if (!this.noteIndex) {
|
||||
new Notice("Note index not available");
|
||||
return;
|
||||
}
|
||||
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;
|
||||
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());
|
||||
|
||||
let linkText = `[[${result.basename}]]`;
|
||||
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:",
|
||||
});
|
||||
|
||||
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}]]`;
|
||||
// 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}]]`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Insert link with rel: prefix if edge type was selected
|
||||
// Extract inner part (without [[ and ]])
|
||||
const innerLink = linkText.replace(/^\[\[/, "").replace(/\]\]$/, "");
|
||||
insertWikilinkIntoTextarea(textarea, innerLink);
|
||||
// Insert link
|
||||
const innerLink = linkText.replace(/^\[\[/, "").replace(/\]\]$/, "");
|
||||
insertWikilinkIntoTextarea(textarea, innerLink);
|
||||
};
|
||||
}
|
||||
).open();
|
||||
|
||||
// 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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
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();
|
||||
}
|
||||
},
|
||||
async (app: App, textarea: HTMLTextAreaElement) => {
|
||||
// Edge-Type-Selektor für Interview-Eingabefeld
|
||||
|
|
@ -973,15 +1121,30 @@ export class InterviewWizardModal extends Modal {
|
|||
if (typeof item === "object" && item !== null) {
|
||||
const itemEntries = Object.entries(item as Record<string, unknown>);
|
||||
if (itemEntries.length > 0) {
|
||||
const previewText = itemEntries
|
||||
.map(([key, value]) => {
|
||||
const nestedStep = step.items.find(s => s.key === key);
|
||||
const label = nestedStep?.label || key;
|
||||
const strValue = String(value);
|
||||
return `${label}: ${strValue.substring(0, 40)}${strValue.length > 40 ? "..." : ""}`;
|
||||
})
|
||||
.join(", ");
|
||||
preview.createSpan({ text: previewText });
|
||||
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(", ") });
|
||||
} else {
|
||||
preview.createSpan({ text: "Empty item" });
|
||||
}
|
||||
|
|
@ -1093,6 +1256,8 @@ export class InterviewWizardModal extends Modal {
|
|||
if (currentState) {
|
||||
const newState = setDraftField(currentState, fieldId, value);
|
||||
this.state.loopRuntimeStates.set(step.key, newState);
|
||||
// WP-26: NICHT renderStep() hier aufrufen, um Fokus-Verlust zu vermeiden
|
||||
// Das Re-Rendering wird nur bei expliziten Navigationen durchgeführt
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -1134,6 +1299,12 @@ export class InterviewWizardModal extends Modal {
|
|||
itemNextBtn.onclick = () => {
|
||||
const currentState = this.state.loopRuntimeStates.get(step.key);
|
||||
if (currentState) {
|
||||
// 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);
|
||||
}
|
||||
|
||||
const newState = itemNextStep(currentState, step.items.length);
|
||||
this.state.loopRuntimeStates.set(step.key, newState);
|
||||
this.renderStep();
|
||||
|
|
@ -1180,6 +1351,14 @@ export class InterviewWizardModal extends Modal {
|
|||
|
||||
if (isDraftDirty(currentState.draft)) {
|
||||
const wasNewItem = currentState.editIndex === null;
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
const newState = commitDraft(currentState);
|
||||
this.state.loopRuntimeStates.set(step.key, newState);
|
||||
// Update answers
|
||||
|
|
@ -1208,6 +1387,8 @@ export class InterviewWizardModal extends Modal {
|
|||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
this.renderStep();
|
||||
} else {
|
||||
new Notice("Please enter at least one field");
|
||||
|
|
@ -1267,6 +1448,28 @@ export class InterviewWizardModal extends Modal {
|
|||
});
|
||||
}
|
||||
|
||||
// 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";
|
||||
}
|
||||
}
|
||||
|
||||
// Container for editor/preview
|
||||
const editorContainer = fieldContainer.createEl("div", {
|
||||
cls: "markdown-editor-container",
|
||||
|
|
@ -1338,8 +1541,32 @@ export class InterviewWizardModal extends Modal {
|
|||
textSetting.addTextArea((text) => {
|
||||
textareaRef = text.inputEl;
|
||||
text.setValue(existingValue);
|
||||
|
||||
// WP-26: Speichere Fokus-Info vor onChange, um Fokus-Verlust zu vermeiden
|
||||
let hadFocus = false;
|
||||
let selectionStart = 0;
|
||||
let selectionEnd = 0;
|
||||
|
||||
text.onChange((value) => {
|
||||
// WP-26: Speichere Fokus-Info vor State-Update
|
||||
if (document.activeElement === textareaRef) {
|
||||
hadFocus = true;
|
||||
selectionStart = textareaRef.selectionStart;
|
||||
selectionEnd = textareaRef.selectionEnd;
|
||||
}
|
||||
|
||||
onFieldChange(nestedStep.key, value);
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// Update preview if in preview mode
|
||||
const currentPreviewMode = this.previewMode.get(previewKey) || false;
|
||||
if (currentPreviewMode) {
|
||||
|
|
@ -1515,6 +1742,29 @@ export class InterviewWizardModal extends Modal {
|
|||
settingNameEl.style.display = "none";
|
||||
}
|
||||
|
||||
// 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";
|
||||
}
|
||||
}
|
||||
|
||||
fieldSetting.addText((text) => {
|
||||
text.setValue(existingValue);
|
||||
text.onChange((value) => {
|
||||
|
|
@ -2416,38 +2666,86 @@ export class InterviewWizardModal extends Modal {
|
|||
graphSchema = await this.plugin.ensureGraphSchemaLoaded();
|
||||
}
|
||||
|
||||
// Get source note ID and type
|
||||
const sourceContent = this.fileContent;
|
||||
const { extractFrontmatterId } = await import("../parser/parseFrontmatter");
|
||||
const sourceNoteId = extractFrontmatterId(sourceContent) || undefined;
|
||||
const sourceFrontmatter = sourceContent.match(/^---\n([\s\S]*?)\n---/);
|
||||
// WP-26: Prüfe, ob es eine Block-ID-Referenz ist ([[#^block-id]])
|
||||
let sourceType: string | undefined;
|
||||
if (sourceFrontmatter && sourceFrontmatter[1]) {
|
||||
const typeMatch = sourceFrontmatter[1].match(/^type:\s*(.+)$/m);
|
||||
if (typeMatch && typeMatch[1]) {
|
||||
sourceType = typeMatch[1].trim();
|
||||
}
|
||||
}
|
||||
|
||||
// Get target note ID and type
|
||||
let targetNoteId: string | undefined;
|
||||
let targetType: string | undefined;
|
||||
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();
|
||||
}
|
||||
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) {
|
||||
sourceType = sourceSection.sectionType || sourceSection.noteType;
|
||||
console.log(`[WP-26] Source-Type aus Section: ${sourceType}`);
|
||||
}
|
||||
} 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
|
||||
// Get source note ID and type
|
||||
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();
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Target note might not exist yet, that's OK
|
||||
console.debug("[Mindnet] Could not read target note for inline micro:", e);
|
||||
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Target note might not exist yet, that's OK
|
||||
console.debug("[Mindnet] Could not read target note for inline micro:", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Show inline edge type modal
|
||||
|
|
@ -2794,13 +3092,47 @@ export class InterviewWizardModal extends Modal {
|
|||
this.state.loopContexts.set(loopKey, loopState.items);
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// Use renderer to generate markdown from collected data
|
||||
const answers: RenderAnswers = {
|
||||
collectedData: this.state.collectedData,
|
||||
loopContexts: this.state.loopContexts,
|
||||
sectionSequence: Array.from(this.state.sectionSequence), // WP-26: Section-Sequenz übergeben
|
||||
};
|
||||
|
||||
const renderedMarkdown = renderProfileToMarkdown(this.state.profile, answers);
|
||||
// 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,
|
||||
};
|
||||
|
||||
const renderedMarkdown = renderProfileToMarkdown(this.state.profile, answers, renderOptions);
|
||||
|
||||
if (renderedMarkdown.trim()) {
|
||||
// Append rendered markdown to file
|
||||
|
|
@ -2878,6 +3210,183 @@ export class InterviewWizardModal extends Modal {
|
|||
return JSON.stringify(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
const captureStep = nestedStep as import("../interview/types").CaptureTextStep | import("../interview/types").CaptureTextLineStep;
|
||||
|
||||
// Für capture_text: Nur wenn Section vorhanden ist
|
||||
if (nestedStep.type === "capture_text" && !captureStep.section) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Für capture_text_line: Nur wenn heading_level enabled ist
|
||||
if (nestedStep.type === "capture_text_line" && !captureStep.heading_level?.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// WP-26: Block-ID ermitteln
|
||||
let blockId: string | null = null;
|
||||
if (captureStep.block_id) {
|
||||
blockId = captureStep.block_id;
|
||||
} else if (captureStep.generate_block_id) {
|
||||
// Für Loop-Items: Verwende Loop-Key + Step-Key für eindeutige Block-ID
|
||||
blockId = slugify(`${loopKey}-${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 = "";
|
||||
if (nestedStep.type === "capture_text" && captureStep.section) {
|
||||
// Extrahiere Heading-Text aus Section (z.B. "## 📖 Kontext" -> "📖 Kontext")
|
||||
heading = captureStep.section.replace(/^#+\s+/, "");
|
||||
} else if (nestedStep.type === "capture_text_line") {
|
||||
// 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;
|
||||
}
|
||||
|
||||
const captureStep = step as import("../interview/types").CaptureTextStep | import("../interview/types").CaptureTextLineStep;
|
||||
|
||||
console.log(`[WP-26] trackSectionInfo aufgerufen für Step ${step.key}`, {
|
||||
type: step.type,
|
||||
hasSection: !!captureStep.section,
|
||||
section: captureStep.section,
|
||||
hasSectionType: !!captureStep.section_type,
|
||||
sectionType: captureStep.section_type,
|
||||
hasBlockId: !!captureStep.block_id,
|
||||
blockId: captureStep.block_id,
|
||||
generateBlockId: captureStep.generate_block_id,
|
||||
hasHeadingLevel: !!captureStep.heading_level?.enabled,
|
||||
});
|
||||
|
||||
// Nur wenn Section vorhanden ist
|
||||
if (!captureStep.section && step.type === "capture_text") {
|
||||
console.log(`[WP-26] Step ${step.key} hat keine section, überspringe`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Für capture_text_line: Nur wenn heading_level enabled ist
|
||||
if (step.type === "capture_text_line" && !captureStep.heading_level?.enabled) {
|
||||
console.log(`[WP-26] Step ${step.key} hat kein heading_level enabled, überspringe`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 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 = "";
|
||||
if (step.type === "capture_text" && captureStep.section) {
|
||||
// Extrahiere Heading-Text aus Section (z.B. "## 📖 Kontext" -> "📖 Kontext")
|
||||
heading = captureStep.section.replace(/^#+\s+/, "");
|
||||
} else if (step.type === "capture_text_line") {
|
||||
// Für capture_text_line: Heading wird später aus dem eingegebenen Text generiert
|
||||
// Verwende Step-Key als Platzhalter
|
||||
heading = captureStep.key;
|
||||
}
|
||||
|
||||
// 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}`);
|
||||
}
|
||||
}
|
||||
|
||||
onClose(): void {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user