Enhance interview configuration and rendering for WP-26 integration
Some checks are pending
Node.js build / build (20.x) (push) Waiting to run
Node.js build / build (22.x) (push) Waiting to run

- Expanded the interview configuration YAML to include new profiles for experience and insight, with detailed steps for capturing user input.
- Updated the parsing logic to support new input types, including select options, enhancing user interaction during interviews.
- Improved the rendering logic to ensure correct handling of section edges and types, aligning with the updated configuration structure.
- Enhanced tests to validate the new configurations and rendering behavior, ensuring robustness in the interview process.
This commit is contained in:
Lars 2026-01-30 18:27:38 +01:00
parent 8186ca5ce0
commit 054cfcf82d
7 changed files with 783 additions and 266 deletions

View File

@ -23,140 +23,20 @@ ui_defaults:
profiles: profiles:
- key: experience_cluster # ---------------------------------------------------------------------------
group: experience # EXPERIENCE
label: "Experience Cluster" # ---------------------------------------------------------------------------
- key: experience_basic
group: history
label: "Experience Basis"
note_type: experience note_type: experience
defaults: defaults:
status: active status: active
folder: "03_experience" folder: "03_experience"
chunking_profile: structured_smart_edges
retriever_weight: 1.0
edging: edging:
mode: both mode: both
steps:
- id: title
kind: capture_frontmatter
field: title
label: "Hub Titel"
required: true
# Einzeiliges Feld
- id: subtitle
kind: capture_text_line
label: "Untertitel"
heading_level:
enabled: true
default: 1
prompt: "Kurzer Untertitel"
- id: intro
kind: capture_text
section: "## Einleitung"
label: "Einleitung"
prompt: "Beschreibe die Einleitung"
- id: items
kind: loop
label: "Erlebnisse"
item_label: "Erlebnis"
min_items: 1
steps:
- id: item_Headline
kind: capture_text_line
label: "Überschrift"
required: false
heading_level:
enabled: true
default: 2
prompt: "Nennen kurz die Gruppenüberschrift"
- id: level2
label: "Listeneinträge zusammenstellen"
kind: loop
steps:
- id: item_text
kind: capture_text
section: ""
label: "Liste"
required: true
prompt: "Stelle eine Liste von Erlebnisse zusammen"
- id: review
kind: review
label: "Review & Apply"
checks: [lint_current_note]
- key: experience_cluster_2
group: experience
label: "Experience Cluster2"
note_type: experience
defaults:
status: active
chunking_profile: timeline
steps:
- id: title
kind: capture_frontmatter
field: title
label: "Cluster Titel"
required: true
input:
kind: text_line
# Gruppen = Überschrift + viele Listeneinträge
- id: groups
kind: loop
label: "Erlebnis-Gruppen"
ui:
mode: subwizard # pro Gruppe eigener Flow
commit: on_next # kein Add-Item-Zwang
allow_edit: true
allow_delete: true
allow_reorder: true
show_item_overview: true # Übersicht, aber editierbar
output:
join: "\n" # Gruppen durch Leerzeile trennen
steps:
- id: group_heading
kind: heading
label: "Überschrift"
required: true
default_level: 2
allow_level_change: true
output:
template: "{hashes} {text}\n"
- id: entries
kind: loop
label: "Einträge"
ui:
mode: subwizard
commit: on_next
allow_edit: true
allow_delete: true
allow_reorder: true
show_item_overview: true
output:
join: "" # Einträge direkt unter Überschrift
steps:
- id: entry
kind: text_line
label: "Listeneintrag"
required: true
output:
template: "- {text}\n"
- id: review
kind: review
label: "Review & Apply"
checks:
- lint_current_note
- missing_targets
- missing_frontmatter_id
- key: experience_single
group: experience
label: "Experience Einzelereignis"
note_type: experience
defaults:
status: active
chunking_profile: timeline
steps: steps:
- id: title - id: title
kind: capture_frontmatter kind: capture_frontmatter
@ -164,26 +44,63 @@ profiles:
label: "Titel" label: "Titel"
required: true required: true
- id: context - id: significance
kind: capture_frontmatter
field: retriever_weight
label: "Bedeutung"
required: false
input:
kind: select
options:
- label: "Normal (1.0)"
value: 1.0
- label: "Signifikant / prägend (1.1)"
value: 1.1
- id: situation
kind: capture_text kind: capture_text
section: "## 📖 Kontext" section: "## Situation (Was ist passiert?)"
label: "Kontext" label: "Situation"
required: true required: true
prompt: "In welchem Rahmen ist es passiert?" prompt: "Was ist passiert? Wer war beteiligt? Was war der Kontext?"
section_type: experience
generate_block_id: true
- id: trigger - id: reaction
kind: capture_text kind: capture_text
section: "## ⚡ Auslöser" section: "## Meine Reaktion (Was habe ich getan?)"
label: "Auslöser" label: "Reaktion"
required: false required: true
prompt: "Was hat es ausgelöst?" prompt: "Was hast du konkret getan/gesagt/unterlassen?"
section_type: experience
generate_block_id: true
- id: transformation - id: impact
kind: capture_text kind: capture_text
section: "## 🧠 Innere Transformation" section: "## Ergebnis & Auswirkung"
label: "Innere Transformation" label: "Auswirkung"
required: false required: false
prompt: "Was hat sich innerlich verändert?" prompt: "Welche Folgen hatte es (kurzfristig/langfristig)?"
section_type: state
generate_block_id: true
- id: learning
kind: capture_text
section: "## Reflexion & Learning (Was lerne ich daraus?)"
label: "Learning"
required: false
prompt: "Welche Erkenntnis oder Regel ergibt sich daraus?"
section_type: insight
generate_block_id: true
- id: next
kind: capture_text
section: "## Nächster Schritt"
label: "Nächster Schritt"
required: false
prompt: "Was folgt daraus ganz konkret?"
section_type: decision
generate_block_id: true
- id: review - id: review
kind: review kind: review
@ -194,11 +111,16 @@ profiles:
- missing_frontmatter_id - missing_frontmatter_id
- key: experience_hub - key: experience_hub
group: experience group: history
label: "Experience Hub" label: "Experience Hub"
note_type: experience note_type: experience
defaults: defaults:
status: active status: active
folder: "03_experience"
chunking_profile: structured_smart_edges
retriever_weight: 1.2
edging:
mode: post_run
steps: steps:
- id: title - id: title
kind: capture_frontmatter kind: capture_frontmatter
@ -206,34 +128,564 @@ profiles:
label: "Hub Titel" label: "Hub Titel"
required: true required: true
- id: purpose
kind: capture_text
section: "## Zweck / Klammer"
label: "Zweck"
required: false
prompt: "Wozu ist dieser Hub da? Welche Art von Erlebnissen sammelt er?"
section_type: experience
generate_block_id: true
- id: items - id: items
kind: loop kind: loop
label: "Erlebnisse" label: "Einträge"
item_label: "Erlebnis" item_label: "Eintrag"
min_items: 1 min_items: 1
steps: steps:
- id: item_text - id: item
kind: capture_text kind: capture_text_line
section: "## 🧩 Erlebnisse" label: "Erlebnis (Kurzform)"
label: "Erlebnis"
required: true required: true
prompt: "Beschreibe ein Erlebnis (kurz)." prompt: "Kurzbeschreibung + ggf. Link auf Detail-Note"
- id: review
kind: review
label: "Review & Apply"
checks:
- lint_current_note
- missing_frontmatter_id
# ---------------------------------------------------------------------------
# INSIGHT
# ---------------------------------------------------------------------------
- key: insight_basic
group: knowledge
label: "Insight Basis"
note_type: insight
defaults:
status: active
folder: "04_insight"
chunking_profile: structured_smart_edges
retriever_weight: 1.0
edging:
mode: both
steps:
- id: title
kind: capture_frontmatter
field: title
label: "Titel"
required: true
- id: significance
kind: capture_frontmatter
field: retriever_weight
label: "Bedeutung"
required: false
input:
kind: select
options:
- label: "Normal (1.0)"
value: 1.0
- label: "Signifikant (1.1)"
value: 1.1
- id: observation
kind: capture_text
section: "## Beobachtung (Was sehe ich?)"
label: "Beobachtung"
required: true
prompt: "Welche konkrete Beobachtung ist der Ausgangspunkt?"
section_type: insight
generate_block_id: true
- id: interpretation
kind: capture_text
section: "## Interpretation (Was bedeutet das?)"
label: "Interpretation"
required: true
prompt: "Welche Erklärung/Schlussfolgerung ziehst du daraus?"
section_type: insight
generate_block_id: true
- id: need
kind: capture_text
section: "## Bedürfnis (Was steckt dahinter?)"
label: "Bedürfnis"
required: false
prompt: "Welches Bedürfnis/Anliegen wird sichtbar?"
section_type: need
generate_block_id: true
- id: recommendation
kind: capture_text
section: "## Handlungsempfehlung"
label: "Handlungsempfehlung"
required: false
prompt: "Welche konkrete Empfehlung ergibt sich?"
section_type: decision
generate_block_id: true
- id: review - id: review
kind: review kind: review
label: "Review & Apply" label: "Review & Apply"
checks: [lint_current_note] checks:
- lint_current_note
- missing_targets
- missing_frontmatter_id
- key: insight_hub
group: knowledge
label: "Insight Hub"
note_type: insight
defaults:
status: active
folder: "04_insight"
chunking_profile: structured_smart_edges
retriever_weight: 1.2
edging:
mode: post_run
steps:
- id: title
kind: capture_frontmatter
field: title
label: "Hub Titel"
required: true
- id: scope
kind: capture_text
section: "## Scope"
label: "Scope"
required: false
prompt: "Welche Art von Insights werden hier gesammelt?"
section_type: insight
generate_block_id: true
- key: principle - id: items
group: principle kind: loop
label: "Principle" label: "Einträge"
item_label: "Eintrag"
min_items: 1
steps:
- id: item
kind: capture_text_line
label: "Insight (Kurzform)"
required: true
prompt: "Kurzform + ggf. Link auf Detail-Note"
- id: review
kind: review
label: "Review & Apply"
checks:
- lint_current_note
- missing_frontmatter_id
# ---------------------------------------------------------------------------
# DECISION
# ---------------------------------------------------------------------------
- key: decision_basic
group: action
label: "Decision Basis"
note_type: decision
defaults:
status: active
folder: "05_decision"
chunking_profile: structured_smart_edges
retriever_weight: 1.0
edging:
mode: both
steps:
- id: title
kind: capture_frontmatter
field: title
label: "Titel"
required: true
- id: significance
kind: capture_frontmatter
field: retriever_weight
label: "Bedeutung"
required: false
input:
kind: select
options:
- label: "Normal (1.0)"
value: 1.0
- label: "Signifikant (1.1)"
value: 1.1
- id: context
kind: capture_text
section: "## Kontext & Problemstellung"
label: "Kontext"
required: true
prompt: "Was ist die Situation? Welches Problem/Tradeoff muss gelöst werden?"
section_type: decision
generate_block_id: true
- id: options
kind: loop
label: "Optionen"
item_label: "Option"
min_items: 1
steps:
- id: option
kind: capture_text
section: ""
label: "Option"
required: true
prompt: "Beschreibe eine Option inkl. Vor-/Nachteile (kurz)."
- id: rationale
kind: capture_text
section: "## Begründung (Werte/Prinzipien/Einsichten)"
label: "Begründung"
required: true
prompt: "Welche Werte/Prinzipien/Erfahrungen/Einsichten begründen die Wahl?"
section_type: insight
generate_block_id: true
- id: decision
kind: capture_text
section: "## Entscheidung"
label: "Entscheidung"
required: true
prompt: "Welche Entscheidung triffst du konkret?"
section_type: decision
generate_block_id: true
- id: risks
kind: capture_text
section: "## Risiken & Tradeoffs"
label: "Risiken"
required: false
prompt: "Welche Risiken nimmst du bewusst in Kauf? Welche Gegenmaßnahmen?"
section_type: risk
generate_block_id: true
- id: execution
kind: capture_text
section: "## Umsetzung / Nächste Schritte"
label: "Umsetzung"
required: false
prompt: "Was sind die nächsten konkreten Schritte?"
section_type: task
generate_block_id: true
- id: review
kind: review
label: "Review & Apply"
checks:
- lint_current_note
- missing_targets
- missing_frontmatter_id
# ---------------------------------------------------------------------------
# PROJECT / GOAL / TASK
# ---------------------------------------------------------------------------
- key: project_basic
group: action
label: "Project Basis"
note_type: project
defaults:
status: active
folder: "02_project"
chunking_profile: structured_smart_edges
retriever_weight: 1.0
edging:
mode: both
steps:
- id: title
kind: capture_frontmatter
field: title
label: "Titel"
required: true
- id: mission
kind: capture_text
section: "## Mission & Zielsetzung"
label: "Mission"
required: true
prompt: "Wozu gibt es dieses Projekt? Was ist der Zielzustand?"
section_type: goal
generate_block_id: true
- id: status
kind: capture_text
section: "## Aktueller Status"
label: "Status"
required: false
prompt: "Wo stehst du gerade?"
section_type: state
generate_block_id: true
- id: blockers
kind: capture_text
section: "## Blockaden / Hindernisse"
label: "Blockaden"
required: false
prompt: "Was blockiert Fortschritt? Warum?"
section_type: obstacle
generate_block_id: true
- id: next
kind: capture_text
section: "## Nächste konkrete Schritte"
label: "Nächste Schritte"
required: true
prompt: "Welche 13 nächsten Schritte bringen dich voran?"
section_type: task
generate_block_id: true
- id: risks
kind: capture_text
section: "## Risiken"
label: "Risiken"
required: false
prompt: "Welche Risiken gibt es, und was tust du dagegen?"
section_type: risk
generate_block_id: true
- id: review
kind: review
label: "Review & Apply"
checks:
- lint_current_note
- missing_targets
- missing_frontmatter_id
- key: goal_basic
group: action
label: "Goal Basis"
note_type: goal
defaults:
status: active
folder: "02_goal"
chunking_profile: structured_smart_edges
retriever_weight: 1.0
edging:
mode: both
steps:
- id: title
kind: capture_frontmatter
field: title
label: "Titel"
required: true
- id: target
kind: capture_text
section: "## Zielzustand"
label: "Zielzustand"
required: true
prompt: "Was ist der Zielzustand (klar, überprüfbar)?"
section_type: goal
generate_block_id: true
- id: why
kind: capture_text
section: "## Motivation / Warum"
label: "Warum"
required: false
prompt: "Warum ist es dir wichtig?"
section_type: motivation
generate_block_id: true
- id: constraints
kind: capture_text
section: "## Constraints"
label: "Constraints"
required: false
prompt: "Welche Randbedingungen (Zeit, Energie, Ressourcen) gelten?"
section_type: obstacle
generate_block_id: true
- id: next
kind: capture_text
section: "## Nächster Schritt"
label: "Nächster Schritt"
required: false
prompt: "Was ist der nächste konkrete Schritt?"
section_type: task
generate_block_id: true
- id: review
kind: review
label: "Review & Apply"
checks:
- lint_current_note
- missing_targets
- missing_frontmatter_id
- key: task_basic
group: action
label: "Task Basis"
note_type: task
defaults:
status: active
folder: "02_task"
chunking_profile: structured_smart_edges
retriever_weight: 1.0
edging:
mode: both
steps:
- id: title
kind: capture_frontmatter
field: title
label: "Titel"
required: true
- id: task
kind: capture_text
section: "## Aufgabe"
label: "Aufgabe"
required: true
prompt: "Was ist zu tun (klar, klein, überprüfbar)?"
section_type: task
generate_block_id: true
- id: context
kind: capture_text
section: "## Kontext"
label: "Kontext"
required: false
prompt: "Zu welchem Projekt/Entscheidung gehört es? Warum jetzt?"
section_type: project
generate_block_id: true
- id: dod
kind: capture_text
section: "## Definition of Done"
label: "DoD"
required: false
prompt: "Woran erkennst du: fertig?"
section_type: task
generate_block_id: true
- id: blockers
kind: capture_text
section: "## Blocker"
label: "Blocker"
required: false
prompt: "Was blockiert?"
section_type: obstacle
generate_block_id: true
- id: review
kind: review
label: "Review & Apply"
checks:
- lint_current_note
- missing_targets
- missing_frontmatter_id
# ---------------------------------------------------------------------------
# VALUE / PRINCIPLE (Normativer Kern)
# ---------------------------------------------------------------------------
- key: value_basic
group: identity
label: "Value Basis"
note_type: value
defaults:
status: active
folder: "01_value"
chunking_profile: structured_smart_edges
retriever_weight: 1.0
edging:
mode: both
steps:
- id: title
kind: capture_frontmatter
field: title
label: "Titel"
required: true
- id: definition
kind: capture_text
section: "## Definition"
label: "Definition"
required: true
prompt: "Was bedeutet dieser Wert für dich (in einem Satz + 23 Bulletpoints)?"
section_type: value
generate_block_id: true
- id: origin
kind: capture_text
section: "## Herkunft / Warum mir das wichtig ist"
label: "Warum"
required: false
prompt: "Woher kommt das (Erlebnis/Einsicht)?"
section_type: experience
generate_block_id: true
- id: principles
kind: capture_text
section: "## Operationalisierung (Prinzipien)"
label: "Prinzipien"
required: false
prompt: "Welche Prinzipien machen den Wert im Alltag sichtbar?"
section_type: principle
generate_block_id: true
- id: review
kind: review
label: "Review & Apply"
checks:
- lint_current_note
- missing_targets
- missing_frontmatter_id
- key: value_hub
group: identity
label: "Value Hub"
note_type: value
defaults:
status: active
folder: "01_value"
chunking_profile: structured_smart_edges
retriever_weight: 1.2
edging:
mode: post_run
steps:
- id: title
kind: capture_frontmatter
field: title
label: "Hub Titel"
required: true
- id: scope
kind: capture_text
section: "## Scope"
label: "Scope"
required: false
prompt: "Welche Werte sind hier gebündelt und warum?"
section_type: value
generate_block_id: true
- id: items
kind: loop
label: "Einträge"
item_label: "Wert"
min_items: 1
steps:
- id: item
kind: capture_text_line
label: "Wert (Kurzform)"
required: true
prompt: "Kurzform + Link auf Detail-Note"
- id: review
kind: review
label: "Review & Apply"
checks:
- lint_current_note
- missing_frontmatter_id
- key: principle_basic
group: identity
label: "Principle Basis"
note_type: principle note_type: principle
defaults: defaults:
status: stable status: active
chunking_profile: principle_dense folder: "01_principle"
retriever_weight: 2 chunking_profile: structured_smart_edges
retriever_weight: 1.0
edging:
mode: both
steps: steps:
- id: title - id: title
kind: capture_frontmatter kind: capture_frontmatter
@ -243,34 +695,36 @@ profiles:
- id: statement - id: statement
kind: capture_text kind: capture_text
section: "## 🧭 Prinzip" section: "## Prinzip"
label: "Prinzip" label: "Prinzip"
required: true required: true
prompt: "Formuliere das Prinzip als klaren Satz." prompt: "Formuliere das Prinzip als klaren Satz."
section_type: principle
generate_block_id: true
- id: retriever_weight - id: application
kind: capture_frontmatter kind: capture_text
field: retriever_weight section: "## Anwendung (Entscheidungsregeln)"
label: "Retriever Weight" label: "Anwendung"
required: false required: false
input: prompt: "Wie wendest du es konkret an? (Wenn-dann-Regeln, Beispiele)"
kind: number section_type: decision
min: -3 generate_block_id: true
max: 3
step: 1
- id: llm_refine - id: signals
kind: llm_dialog kind: capture_text
label: "LLM Verdichtung (manuell)" section: "## Signale / Wächterfragen"
mode: manual label: "Signale"
prompt_template: | required: false
Verdichte die bisherigen Inhalte zu 3-5 Bulletpoints. prompt: "Woran erkennst du, dass du dem Prinzip folgst oder davon abweichst?"
Erfinde keine Fakten. section_type: insight
output_target: generate_block_id: true
kind: section_append
section: "## 🧠 Verdichtung (LLM Vorschlag)"
- id: review - id: review
kind: review kind: review
label: "Review & Apply" label: "Review & Apply"
checks: [lint_current_note] checks:
- lint_current_note
- missing_targets
- missing_frontmatter_id

View File

@ -285,6 +285,32 @@ function parseStep(raw: Record<string, unknown>): InterviewStep | null {
step.prompt = raw.prompt.trim(); step.prompt = raw.prompt.trim();
} }
// Parse input (kind: select | number | text_line, options for select)
if (raw.input && typeof raw.input === "object") {
const input = raw.input as Record<string, unknown>;
const inputKind =
typeof input.kind === "string" && input.kind.trim()
? (input.kind.trim() as "text_line" | "number" | "select")
: "text_line";
step.input = { kind: inputKind };
if (inputKind === "select" && Array.isArray(input.options)) {
step.input.options = [];
for (const opt of input.options) {
if (opt && typeof opt === "object") {
const o = opt as Record<string, unknown>;
const label = typeof o.label === "string" ? o.label.trim() : "";
const value = typeof o.value === "number" ? o.value : typeof o.value === "string" ? o.value : "";
if (label !== "" || value !== "") {
step.input!.options!.push({ label: label || String(value), value });
}
}
}
}
if (typeof input.min === "number") step.input.min = input.min;
if (typeof input.max === "number") step.input.max = input.max;
if (typeof input.step === "number") step.input.step = input.step;
}
// Parse output.template // Parse output.template
if (raw.output && typeof raw.output === "object") { if (raw.output && typeof raw.output === "object") {
const output = raw.output as Record<string, unknown>; const output = raw.output as Record<string, unknown>;

View File

@ -285,9 +285,9 @@ describe("WP-26 Interview Renderer", () => {
const result = renderProfileToMarkdown(profile, answers, mockOptions); const result = renderProfileToMarkdown(profile, answers, mockOptions);
// Prüfe, dass Edges am Ende der Einsicht-Sektion stehen (nach dem Text) // Prüfe, dass Edges am Ende der Einsicht-Sektion stehen (nach dem Text; Abstract-Wrapper nutzt >>)
const insightSection = result.match(/## Einsicht[\s\S]*?Einsicht-Text\n\n(> \[!edge\][\s\S]*)/)?.[1]; const insightSection = result.match(/## Einsicht[\s\S]*?Einsicht-Text\n\n([\s\S]*?\[!edge\][\s\S]*)/)?.[1];
expect(insightSection).toBeDefined(); expect(insightSection).toBeDefined();
expect(insightSection).toMatch(/> \[!edge\]/); expect(insightSection).toMatch(/>+ \[!edge\]/);
}); });
}); });

View File

@ -36,28 +36,13 @@ export function renderProfileToMarkdown(
answers: RenderAnswers, answers: RenderAnswers,
options?: RenderOptions options?: RenderOptions
): string { ): string {
// WP-26: Verwende übergebene Section-Sequenz oder erstelle neue // WP-26: Section-Sequenz und Block-IDs ausschließlich während des Render-Durchlaufs aufbauen.
// Die Section-Sequenz wird während des interaktiven Wizard-Durchlaufs getrackt // Kein Vorfüllen aus answers.sectionSequence, damit nur Sektionen mit tatsächlichem Inhalt
const sectionSequence: SectionInfo[] = answers.sectionSequence ? [...answers.sectionSequence] : []; // (die gerendert werden) in der Sequenz landen keine Kanten zu nicht existierenden Sektionen.
const sectionSequence: SectionInfo[] = [];
const generatedBlockIds = new Map<string, SectionInfo>(); const generatedBlockIds = new Map<string, SectionInfo>();
console.log(`[WP-26] renderProfileToMarkdown:`, { console.log(`[WP-26] renderProfileToMarkdown: sectionSequence wird beim Rendern aufgebaut (kein Vorfüllen).`);
sectionSequenceLength: sectionSequence.length,
sectionSequence: sectionSequence.map(s => ({
stepKey: s.stepKey,
blockId: s.blockId,
sectionType: s.sectionType,
})),
});
// 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);
}
}
}
const context: RenderContext = { const context: RenderContext = {
profile, profile,
@ -998,9 +983,18 @@ function renderForwardEdges(
} }
} }
// WP-26: Forward-Edge generieren (prevSection -> currentSection) // WP-26: In der aktuellen Section steht ein Link ZU prev → reale Kante ist current -> prev.
// Diese Edge wird in der aktuellen Section eingefügt // sectionEdgeTypes liefert den Type für prev -> current; hier brauchen wir die Inverse (current -> prev).
edgeCallouts.push(`> [!edge] ${forwardEdgeType}`); const { vocabulary } = context.options;
let edgeTypeForCallout = forwardEdgeType;
if (vocabulary) {
const canonical = vocabulary.getCanonical(forwardEdgeType);
const inverse = canonical ? vocabulary.getInverse(canonical) : null;
if (inverse) {
edgeTypeForCallout = inverse;
}
}
edgeCallouts.push(`> [!edge] ${edgeTypeForCallout}`);
edgeCallouts.push(`> [[#^${resolvedPrevBlockId}]]`); edgeCallouts.push(`> [[#^${resolvedPrevBlockId}]]`);
} }
@ -1068,19 +1062,10 @@ function renderSingleBackwardEdge(
} }
} }
// WP-26: Backward-Edge generieren (currentSection -> prevSection) // WP-26: Backward-Edge wird in prevSection eingefügt: Link von prev zu current → reale Kante ist prev -> current.
// Das ist die inverse Edge zu der Forward-Edge (prevSection -> currentSection) // sectionEdgeTypes liefert genau den Type für prev -> current; also forwardEdgeType unverändert verwenden.
let backwardEdgeType: string = forwardEdgeType; // (Keine Inverse: Die Kante im Text ist prev -> current, nicht current -> prev.)
if (vocabulary) { return `> [!edge] ${forwardEdgeType}\n> [[#^${currentSection.blockId}]]`;
const inverse = vocabulary.getInverse(forwardEdgeType);
if (inverse) {
backwardEdgeType = inverse;
}
}
// WP-26: Backward-Edge (currentSection -> prevSection) wird in prevSection eingefügt
// Diese Edge zeigt von currentSection zurück zu prevSection
return `> [!edge] ${backwardEdgeType}\n> [[#^${currentSection.blockId}]]`;
} }
/** /**

View File

@ -64,6 +64,14 @@ export interface CaptureFrontmatterStep {
field: string; field: string;
required?: boolean; required?: boolean;
prompt?: string; // Optional prompt text prompt?: string; // Optional prompt text
/** Input UI: "text_line" (default) or "select" with options */
input?: {
kind: "text_line" | "number" | "select";
options?: Array<{ label: string; value: string | number }>;
min?: number;
max?: number;
step?: number;
};
output?: { output?: {
template?: string; // Template with tokens: {text}, {field}, {value} template?: string; // Template with tokens: {text}, {field}, {value}
}; };

View File

@ -974,42 +974,60 @@ export class InterviewWizardModal extends Modal {
settingNameEl2.style.display = "none"; settingNameEl2.style.display = "none";
} }
fieldSetting.addText((text) => { const applyFrontmatterPatch = (value: string | number) => {
text.setValue(defaultValue); this.currentInputValues.set(step.key, String(value));
// Store initial value
this.currentInputValues.set(step.key, defaultValue);
text.onChange((value) => {
console.log("Frontmatter field changed", {
stepKey: step.key,
field: step.field,
value: value,
});
// Update stored value
this.currentInputValues.set(step.key, value);
this.state.collectedData.set(step.key, value); this.state.collectedData.set(step.key, value);
// Update or add patch
const existingPatchIndex = this.state.patches.findIndex( const existingPatchIndex = this.state.patches.findIndex(
p => p.type === "frontmatter" && p.field === step.field (p) => p.type === "frontmatter" && p.field === step.field
); );
const patch = { const patch = {
type: "frontmatter" as const, type: "frontmatter" as const,
field: step.field!, field: step.field!,
value: value, value,
}; };
if (existingPatchIndex >= 0) { if (existingPatchIndex >= 0) {
this.state.patches[existingPatchIndex] = patch; this.state.patches[existingPatchIndex] = patch;
} else { } else {
this.state.patches.push(patch); this.state.patches.push(patch);
} }
};
// Select/dropdown when input.kind === "select" and options are defined
if (step.input?.kind === "select" && Array.isArray(step.input.options) && step.input.options.length > 0) {
const options = step.input.options;
const optionValues = options.map((o) => String(o.value));
const initialVal = defaultValue !== "" && optionValues.includes(String(defaultValue))
? String(defaultValue)
: String(options[0]?.value ?? "");
fieldSetting.addDropdown((dropdown) => {
for (const opt of options) {
dropdown.addOption(String(opt.value), opt.label);
}
dropdown.setValue(initialVal);
applyFrontmatterPatch(
options.find((o) => String(o.value) === initialVal)?.value ?? options[0]?.value ?? initialVal
);
dropdown.onChange((value) => {
const opt = options.find((o) => String(o.value) === value);
const valueToStore = opt?.value ?? value;
applyFrontmatterPatch(typeof valueToStore === "number" ? valueToStore : valueToStore);
});
dropdown.selectEl.style.width = "100%";
});
} else {
fieldSetting.addText((text) => {
text.setValue(defaultValue);
this.currentInputValues.set(step.key, defaultValue);
text.onChange((value) => {
this.currentInputValues.set(step.key, value);
applyFrontmatterPatch(value);
}); });
text.inputEl.style.width = "100%"; text.inputEl.style.width = "100%";
text.inputEl.style.boxSizing = "border-box"; text.inputEl.style.boxSizing = "border-box";
text.inputEl.focus(); text.inputEl.focus();
}); });
} }
}
renderLoopStep(step: InterviewStep, containerEl: HTMLElement): void { renderLoopStep(step: InterviewStep, containerEl: HTMLElement): void {
if (step.type !== "loop") return; if (step.type !== "loop") return;
@ -2549,9 +2567,22 @@ export class InterviewWizardModal extends Modal {
this.saveCurrentStepData(currentStep); this.saveCurrentStepData(currentStep);
} }
// WP-26: Zeige Übersichts-Modal für Sektions-Edges (nur wenn Sections vorhanden) // WP-26: Vocabulary ggf. laden (wird sonst erst in applyPatches geladen dann wäre Dialog schon übersprungen)
if (!this.vocabulary && this.settings?.edgeVocabularyPath) {
try {
const vocabText = await VocabularyLoader.loadText(
this.app,
this.settings.edgeVocabularyPath
);
this.vocabulary = parseEdgeVocabulary(vocabText);
} catch (e) {
console.warn("[WP-26] Vocabulary konnte nicht geladen werden (Kanten-Dialog):", e);
}
}
// WP-26: Übersichts-Modal immer anzeigen, wenn Vocabulary geladen ist (einmal alle Kanten prüfen/ändern).
// Verhindert die langsame Schritt-für-Schritt-Bearbeitung; Standardkanten können unverändert übernommen werden.
let sectionEdgeTypes: Map<string, Map<string, string>> | undefined = undefined; let sectionEdgeTypes: Map<string, Map<string, string>> | undefined = undefined;
if (this.state.sectionSequence.length > 1 && this.vocabulary) { if (this.vocabulary) {
try { try {
const graphSchema = this.plugin?.ensureGraphSchemaLoaded const graphSchema = this.plugin?.ensureGraphSchemaLoaded
? await this.plugin.ensureGraphSchemaLoaded() ? await this.plugin.ensureGraphSchemaLoaded()
@ -2567,9 +2598,10 @@ export class InterviewWizardModal extends Modal {
const result = await overviewModal.show(); const result = await overviewModal.show();
if (!result.cancelled && (result.sectionEdges.size > 0 || result.noteEdges.size > 0)) { // Bei OK immer übernehmen (auch wenn nichts geändert wurde), damit Renderer korrekte Types/Inversen nutzt
if (!result.cancelled) {
sectionEdgeTypes = result.sectionEdges; sectionEdgeTypes = result.sectionEdges;
console.log("[WP-26] Edge-Types vom Nutzer geändert:", { console.log("[WP-26] Edge-Types aus Übersicht übernommen:", {
sectionEdgesCount: result.sectionEdges.size, sectionEdgesCount: result.sectionEdges.size,
noteEdgesCount: result.noteEdges.size, noteEdgesCount: result.noteEdges.size,
sectionEdges: Array.from(result.sectionEdges.entries()).map(([from, toMap]) => ({ sectionEdges: Array.from(result.sectionEdges.entries()).map(([from, toMap]) => ({
@ -2582,11 +2614,9 @@ export class InterviewWizardModal extends Modal {
})), })),
}); });
// WP-26: Speichere Note-Edges für späteres Post-Run-Edging // WP-26: Note-Edges für Post-Run-Edging übernehmen
// Diese werden in pendingEdgeAssignments gespeichert
for (const [fromBlockId, toMap] of result.noteEdges.entries()) { for (const [fromBlockId, toMap] of result.noteEdges.entries()) {
for (const [toNote, edgeType] of toMap.entries()) { for (const [toNote, edgeType] of toMap.entries()) {
// Finde Section-Key für fromBlockId
const sectionInfo = fromBlockId === "ROOT" const sectionInfo = fromBlockId === "ROOT"
? null ? null
: this.state.sectionSequence.find(s => s.blockId === fromBlockId); : this.state.sectionSequence.find(s => s.blockId === fromBlockId);

View File

@ -408,17 +408,31 @@ export class SectionEdgesOverviewModal extends Modal {
return section.noteType; return section.noteType;
} }
/**
* Prüft, ob eine Section im Dialog Inhalt hat (wird später gerendert).
* Nur Kanten zwischen Sections mit Inhalt werden angezeigt, damit keine
* Kanten zum Abgleich erscheinen, die später nicht generiert werden.
*/
private hasSectionContent(section: SectionInfo): boolean {
const value = this.collectedData.get(section.stepKey);
if (value === undefined || value === null) return false;
const str = typeof value === "string" ? value : String(value);
return str.trim() !== "";
}
private buildSectionEdgeList(): void { private buildSectionEdgeList(): void {
this.sectionEdges = []; this.sectionEdges = [];
for (let i = 1; i < this.sectionSequence.length; i++) { for (let i = 1; i < this.sectionSequence.length; i++) {
const currentSection = this.sectionSequence[i]; const currentSection = this.sectionSequence[i];
if (!currentSection || !currentSection.blockId) continue; if (!currentSection || !currentSection.blockId) continue;
if (!this.hasSectionContent(currentSection)) continue;
// Alle vorherigen Sections // Alle vorherigen Sections
for (let j = 0; j < i; j++) { for (let j = 0; j < i; j++) {
const prevSection = this.sectionSequence[j]; const prevSection = this.sectionSequence[j];
if (!prevSection || !prevSection.blockId) continue; if (!prevSection || !prevSection.blockId) continue;
if (!this.hasSectionContent(prevSection)) continue;
// WP-26: Verwende effektive Section-Types (mit Heading-Level-basierter Fallback-Logik) // WP-26: Verwende effektive Section-Types (mit Heading-Level-basierter Fallback-Logik)
const prevType = this.getEffectiveSectionType(prevSection, j); const prevType = this.getEffectiveSectionType(prevSection, j);