From 054cfcf82d9a4bc50b184fc716d228e738ca30d2 Mon Sep 17 00:00:00 2001 From: Lars Date: Fri, 30 Jan 2026 18:27:38 +0100 Subject: [PATCH] Enhance interview configuration and rendering for WP-26 integration - 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. --- Dictionary/interview_config.yaml | 808 ++++++++++++++++++++------ src/interview/parseInterviewConfig.ts | 28 +- src/interview/renderer.test.ts | 6 +- src/interview/renderer.ts | 57 +- src/interview/types.ts | 8 + src/ui/InterviewWizardModal.ts | 128 ++-- src/ui/SectionEdgesOverviewModal.ts | 14 + 7 files changed, 783 insertions(+), 266 deletions(-) diff --git a/Dictionary/interview_config.yaml b/Dictionary/interview_config.yaml index 993a955..a65cf1b 100644 --- a/Dictionary/interview_config.yaml +++ b/Dictionary/interview_config.yaml @@ -23,140 +23,20 @@ ui_defaults: profiles: - - key: experience_cluster - group: experience - label: "Experience – Cluster" + # --------------------------------------------------------------------------- + # EXPERIENCE + # --------------------------------------------------------------------------- + - key: experience_basic + group: history + label: "Experience – Basis" note_type: experience defaults: status: active folder: "03_experience" + chunking_profile: structured_smart_edges + retriever_weight: 1.0 edging: 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: - id: title kind: capture_frontmatter @@ -164,26 +44,63 @@ profiles: label: "Titel" 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 - section: "## 📖 Kontext" - label: "Kontext" + section: "## Situation (Was ist passiert?)" + label: "Situation" 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 - section: "## ⚡ Auslöser" - label: "Auslöser" - required: false - prompt: "Was hat es ausgelöst?" + section: "## Meine Reaktion (Was habe ich getan?)" + label: "Reaktion" + required: true + prompt: "Was hast du konkret getan/gesagt/unterlassen?" + section_type: experience + generate_block_id: true - - id: transformation + - id: impact kind: capture_text - section: "## 🧠 Innere Transformation" - label: "Innere Transformation" + section: "## Ergebnis & Auswirkung" + label: "Auswirkung" 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 kind: review @@ -194,11 +111,16 @@ profiles: - missing_frontmatter_id - key: experience_hub - group: experience + group: history label: "Experience – Hub" note_type: experience defaults: status: active + folder: "03_experience" + chunking_profile: structured_smart_edges + retriever_weight: 1.2 + edging: + mode: post_run steps: - id: title kind: capture_frontmatter @@ -206,34 +128,564 @@ profiles: label: "Hub Titel" 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 kind: loop - label: "Erlebnisse" - item_label: "Erlebnis" + label: "Einträge" + item_label: "Eintrag" min_items: 1 steps: - - id: item_text - kind: capture_text - section: "## 🧩 Erlebnisse" - label: "Erlebnis" + - id: item + kind: capture_text_line + label: "Erlebnis (Kurzform)" 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 kind: review 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 - group: principle - label: "Principle" + - id: items + kind: loop + 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 1–3 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 + 2–3 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 defaults: - status: stable - chunking_profile: principle_dense - retriever_weight: 2 + status: active + folder: "01_principle" + chunking_profile: structured_smart_edges + retriever_weight: 1.0 + edging: + mode: both steps: - id: title kind: capture_frontmatter @@ -243,34 +695,36 @@ profiles: - id: statement kind: capture_text - section: "## 🧭 Prinzip" + section: "## Prinzip" label: "Prinzip" required: true prompt: "Formuliere das Prinzip als klaren Satz." + section_type: principle + generate_block_id: true - - id: retriever_weight - kind: capture_frontmatter - field: retriever_weight - label: "Retriever Weight" + - id: application + kind: capture_text + section: "## Anwendung (Entscheidungsregeln)" + label: "Anwendung" required: false - input: - kind: number - min: -3 - max: 3 - step: 1 + prompt: "Wie wendest du es konkret an? (Wenn-dann-Regeln, Beispiele)" + section_type: decision + generate_block_id: true - - id: llm_refine - kind: llm_dialog - label: "LLM – Verdichtung (manuell)" - mode: manual - prompt_template: | - Verdichte die bisherigen Inhalte zu 3-5 Bulletpoints. - Erfinde keine Fakten. - output_target: - kind: section_append - section: "## 🧠 Verdichtung (LLM Vorschlag)" + - id: signals + kind: capture_text + section: "## Signale / Wächterfragen" + label: "Signale" + required: false + prompt: "Woran erkennst du, dass du dem Prinzip folgst oder davon abweichst?" + section_type: insight + generate_block_id: true - id: review kind: review label: "Review & Apply" - checks: [lint_current_note] + checks: + - lint_current_note + - missing_targets + - missing_frontmatter_id + diff --git a/src/interview/parseInterviewConfig.ts b/src/interview/parseInterviewConfig.ts index 34b7f2c..d037f33 100644 --- a/src/interview/parseInterviewConfig.ts +++ b/src/interview/parseInterviewConfig.ts @@ -284,7 +284,33 @@ function parseStep(raw: Record): InterviewStep | null { if (typeof raw.prompt === "string" && 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; + 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; + 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 if (raw.output && typeof raw.output === "object") { const output = raw.output as Record; diff --git a/src/interview/renderer.test.ts b/src/interview/renderer.test.ts index 2809c5c..590862a 100644 --- a/src/interview/renderer.test.ts +++ b/src/interview/renderer.test.ts @@ -285,9 +285,9 @@ describe("WP-26 Interview Renderer", () => { const result = renderProfileToMarkdown(profile, answers, mockOptions); - // Prüfe, dass Edges am Ende der Einsicht-Sektion stehen (nach dem Text) - const insightSection = result.match(/## Einsicht[\s\S]*?Einsicht-Text\n\n(> \[!edge\][\s\S]*)/)?.[1]; + // 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([\s\S]*?\[!edge\][\s\S]*)/)?.[1]; expect(insightSection).toBeDefined(); - expect(insightSection).toMatch(/> \[!edge\]/); + expect(insightSection).toMatch(/>+ \[!edge\]/); }); }); diff --git a/src/interview/renderer.ts b/src/interview/renderer.ts index ddcfe49..5586d48 100644 --- a/src/interview/renderer.ts +++ b/src/interview/renderer.ts @@ -36,28 +36,13 @@ export function renderProfileToMarkdown( answers: RenderAnswers, options?: RenderOptions ): 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] : []; + // WP-26: Section-Sequenz und Block-IDs ausschließlich während des Render-Durchlaufs aufbauen. + // Kein Vorfüllen aus answers.sectionSequence, damit nur Sektionen mit tatsächlichem Inhalt + // (die gerendert werden) in der Sequenz landen – keine Kanten zu nicht existierenden Sektionen. + const sectionSequence: SectionInfo[] = []; const generatedBlockIds = new Map(); - console.log(`[WP-26] renderProfileToMarkdown:`, { - 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); - } - } - } + console.log(`[WP-26] renderProfileToMarkdown: sectionSequence wird beim Rendern aufgebaut (kein Vorfüllen).`); const context: RenderContext = { profile, @@ -998,9 +983,18 @@ function renderForwardEdges( } } - // WP-26: Forward-Edge generieren (prevSection -> currentSection) - // Diese Edge wird in der aktuellen Section eingefügt - edgeCallouts.push(`> [!edge] ${forwardEdgeType}`); + // WP-26: In der aktuellen Section steht ein Link ZU prev → reale Kante ist current -> prev. + // sectionEdgeTypes liefert den Type für prev -> current; hier brauchen wir die Inverse (current -> prev). + 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}]]`); } @@ -1068,19 +1062,10 @@ function renderSingleBackwardEdge( } } - // WP-26: Backward-Edge generieren (currentSection -> prevSection) - // Das ist die inverse Edge zu der Forward-Edge (prevSection -> currentSection) - let backwardEdgeType: string = forwardEdgeType; - if (vocabulary) { - 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}]]`; + // WP-26: Backward-Edge wird in prevSection eingefügt: Link von prev zu current → reale Kante ist prev -> current. + // sectionEdgeTypes liefert genau den Type für prev -> current; also forwardEdgeType unverändert verwenden. + // (Keine Inverse: Die Kante im Text ist prev -> current, nicht current -> prev.) + return `> [!edge] ${forwardEdgeType}\n> [[#^${currentSection.blockId}]]`; } /** diff --git a/src/interview/types.ts b/src/interview/types.ts index dfaecf4..c113d4c 100644 --- a/src/interview/types.ts +++ b/src/interview/types.ts @@ -64,6 +64,14 @@ export interface CaptureFrontmatterStep { field: string; required?: boolean; 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?: { template?: string; // Template with tokens: {text}, {field}, {value} }; diff --git a/src/ui/InterviewWizardModal.ts b/src/ui/InterviewWizardModal.ts index 78fc2dd..23ebeb3 100644 --- a/src/ui/InterviewWizardModal.ts +++ b/src/ui/InterviewWizardModal.ts @@ -964,51 +964,69 @@ export class InterviewWizardModal extends Modal { }); inputContainer.style.width = "100%"; - const fieldSetting = new Setting(inputContainer); - fieldSetting.settingEl.style.width = "100%"; - fieldSetting.controlEl.style.width = "100%"; + const fieldSetting = new Setting(inputContainer); + fieldSetting.settingEl.style.width = "100%"; + fieldSetting.controlEl.style.width = "100%"; - // Hide the default label from Setting component - const settingNameEl2 = fieldSetting.settingEl.querySelector(".setting-item-name") as HTMLElement | null; - if (settingNameEl2) { - settingNameEl2.style.display = "none"; - } - - fieldSetting.addText((text) => { - text.setValue(defaultValue); - // Store initial value - this.currentInputValues.set(step.key, defaultValue); - - text.onChange((value) => { - console.log("Frontmatter field changed", { - stepKey: step.key, - field: step.field, - value: value, - }); - - // Update stored value - this.currentInputValues.set(step.key, value); - - this.state.collectedData.set(step.key, value); - // Update or add patch - const existingPatchIndex = this.state.patches.findIndex( - p => p.type === "frontmatter" && p.field === step.field - ); - const patch = { - type: "frontmatter" as const, - field: step.field!, - value: value, - }; - if (existingPatchIndex >= 0) { - this.state.patches[existingPatchIndex] = patch; - } else { - this.state.patches.push(patch); + // Hide the default label from Setting component + const settingNameEl2 = fieldSetting.settingEl.querySelector(".setting-item-name") as HTMLElement | null; + if (settingNameEl2) { + settingNameEl2.style.display = "none"; + } + + const applyFrontmatterPatch = (value: string | number) => { + this.currentInputValues.set(step.key, String(value)); + this.state.collectedData.set(step.key, value); + const existingPatchIndex = this.state.patches.findIndex( + (p) => p.type === "frontmatter" && p.field === step.field + ); + const patch = { + type: "frontmatter" as const, + field: step.field!, + value, + }; + if (existingPatchIndex >= 0) { + this.state.patches[existingPatchIndex] = patch; + } else { + 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%"; }); - text.inputEl.style.width = "100%"; - text.inputEl.style.boxSizing = "border-box"; - text.inputEl.focus(); - }); + } 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.boxSizing = "border-box"; + text.inputEl.focus(); + }); + } } renderLoopStep(step: InterviewStep, containerEl: HTMLElement): void { @@ -2549,9 +2567,22 @@ export class InterviewWizardModal extends Modal { 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> | undefined = undefined; - if (this.state.sectionSequence.length > 1 && this.vocabulary) { + if (this.vocabulary) { try { const graphSchema = this.plugin?.ensureGraphSchemaLoaded ? await this.plugin.ensureGraphSchemaLoaded() @@ -2567,9 +2598,10 @@ export class InterviewWizardModal extends Modal { 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; - console.log("[WP-26] Edge-Types vom Nutzer geändert:", { + console.log("[WP-26] Edge-Types aus Übersicht übernommen:", { sectionEdgesCount: result.sectionEdges.size, noteEdgesCount: result.noteEdges.size, 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 - // Diese werden in pendingEdgeAssignments gespeichert + // WP-26: Note-Edges für Post-Run-Edging übernehmen for (const [fromBlockId, toMap] of result.noteEdges.entries()) { for (const [toNote, edgeType] of toMap.entries()) { - // Finde Section-Key für fromBlockId const sectionInfo = fromBlockId === "ROOT" ? null : this.state.sectionSequence.find(s => s.blockId === fromBlockId); diff --git a/src/ui/SectionEdgesOverviewModal.ts b/src/ui/SectionEdgesOverviewModal.ts index dba5c76..d4be0c8 100644 --- a/src/ui/SectionEdgesOverviewModal.ts +++ b/src/ui/SectionEdgesOverviewModal.ts @@ -408,17 +408,31 @@ export class SectionEdgesOverviewModal extends Modal { 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 { this.sectionEdges = []; for (let i = 1; i < this.sectionSequence.length; i++) { const currentSection = this.sectionSequence[i]; if (!currentSection || !currentSection.blockId) continue; + if (!this.hasSectionContent(currentSection)) continue; // Alle vorherigen Sections for (let j = 0; j < i; j++) { const prevSection = this.sectionSequence[j]; if (!prevSection || !prevSection.blockId) continue; + if (!this.hasSectionContent(prevSection)) continue; // WP-26: Verwende effektive Section-Types (mit Heading-Level-basierter Fallback-Logik) const prevType = this.getEffectiveSectionType(prevSection, j);