import { App, Notice, PluginSettingTab, Setting, TFile } from "obsidian"; import type MindnetCausalAssistantPlugin from "../main"; import { VocabularyLoader } from "../vocab/VocabularyLoader"; export class MindnetSettingTab extends PluginSettingTab { plugin: MindnetCausalAssistantPlugin; constructor(app: App, plugin: MindnetCausalAssistantPlugin) { super(app, plugin); this.plugin = plugin; } display(): void { const { containerEl } = this; containerEl.empty(); containerEl.createEl("h2", { text: "Mindnet Settings" }); // ============================================ // 1. Dictionary & Schema Configuration // ============================================ containerEl.createEl("h3", { text: "📚 Dictionary & Schema" }); containerEl.createEl("p", { text: "Konfigurationsdateien für Edge-Vokabular, Graph-Schema und Interview-Profile.", cls: "setting-item-description", }); // Edge vocabulary path new Setting(containerEl) .setName("Edge vocabulary path") .setDesc( "Pfad zur Edge-Vokabular-Datei (Markdown). Definiert verfügbare Edge-Typen und deren Beschreibungen. Wird für die semantische Mapping-Erstellung verwendet." ) .addText((text) => text .setPlaceholder("_system/dictionary/edge_vocabulary.md") .setValue(this.plugin.settings.edgeVocabularyPath) .onChange(async (value) => { this.plugin.settings.edgeVocabularyPath = value; await this.plugin.saveSettings(); }) ) .addButton((button) => button .setButtonText("Validate") .setCta() .onClick(async () => { try { const text = await VocabularyLoader.loadText( this.app, this.plugin.settings.edgeVocabularyPath ); new Notice( `Edge vocabulary file found (${text.length} characters)` ); } catch (e) { const msg = e instanceof Error ? e.message : String(e); new Notice( `Failed to load edge vocabulary: ${msg}` ); } }) ); // Graph schema path new Setting(containerEl) .setName("Graph schema path") .setDesc( "Pfad zur Graph-Schema-Datei (Markdown). Definiert typische und verbotene Edge-Typen für verschiedene Quelltypen. Wird für Empfehlungen beim Mapping verwendet." ) .addText((text) => text .setPlaceholder("_system/dictionary/graph_schema.md") .setValue(this.plugin.settings.graphSchemaPath) .onChange(async (value) => { this.plugin.settings.graphSchemaPath = value; await this.plugin.saveSettings(); }) ) .addButton((button) => button .setButtonText("Validate") .setCta() .onClick(async () => { try { const { GraphSchemaLoader } = await import("../schema/GraphSchemaLoader"); const isValid = await GraphSchemaLoader.validate( this.app, this.plugin.settings.graphSchemaPath ); if (isValid) { const schema = await GraphSchemaLoader.load( this.app, this.plugin.settings.graphSchemaPath ); const ruleCount = schema.schema.size; new Notice( `Graph schema file found (${ruleCount} source types)` ); } else { new Notice("Graph schema file not found or invalid"); } } catch (e) { const msg = e instanceof Error ? e.message : String(e); new Notice(`Failed to validate graph schema: ${msg}`); } }) ); // Interview config path new Setting(containerEl) .setName("Interview config path") .setDesc( "Pfad zur Interview-Konfigurationsdatei (YAML). Definiert verfügbare Interview-Profile mit ihren Schritten und Einstellungen." ) .addText((text) => text .setPlaceholder("_system/dictionary/interview_config.yaml") .setValue(this.plugin.settings.interviewConfigPath) .onChange(async (value) => { this.plugin.settings.interviewConfigPath = value; await this.plugin.saveSettings(); }) ) .addButton((button) => button .setButtonText("Validate") .setCta() .onClick(async () => { try { const { InterviewConfigLoader } = await import("../interview/InterviewConfigLoader"); const result = await InterviewConfigLoader.loadConfig( this.app, this.plugin.settings.interviewConfigPath ); if (result.errors.length > 0) { new Notice( `Interview config loaded with ${result.errors.length} error(s). Check console.` ); console.warn("Interview config errors:", result.errors); } else { new Notice( `Interview config file found (${result.config.profiles.length} profile(s))` ); } } catch (e) { const msg = e instanceof Error ? e.message : String(e); new Notice(`Failed to load interview config: ${msg}`); } }) ); // Chain roles path new Setting(containerEl) .setName("Chain roles path") .setDesc( "Pfad zur Chain-Roles-Konfigurationsdatei (YAML). Definiert Rollen für Chain-Intelligence mit ihren Edge-Typen." ) .addText((text) => text .setPlaceholder("_system/dictionary/chain_roles.yaml") .setValue(this.plugin.settings.chainRolesPath) .onChange(async (value) => { this.plugin.settings.chainRolesPath = value; await this.plugin.saveSettings(); // Trigger reload if path changes if (this.plugin.settings.chainRolesPath) { // Reload will happen on next file modify or can be triggered manually } }) ) .addButton((button) => button .setButtonText("Validate") .setCta() .onClick(async () => { try { const { ChainRolesLoader } = await import("../dictionary/ChainRolesLoader"); const result = await ChainRolesLoader.load( this.app, this.plugin.settings.chainRolesPath ); if (result.errors.length > 0) { new Notice( `Chain roles loaded with ${result.errors.length} error(s). Check console.` ); console.warn("Chain roles errors:", result.errors); } else { const roleCount = Object.keys(result.data?.roles || {}).length; new Notice(`Chain roles file found (${roleCount} role(s))`); } } catch (e) { const msg = e instanceof Error ? e.message : String(e); new Notice(`Failed to load chain roles: ${msg}`); } }) ); // Chain templates path new Setting(containerEl) .setName("Chain templates path") .setDesc( "Pfad zur Chain-Templates-Konfigurationsdatei (YAML). Definiert Templates für Chain-Intelligence mit Slots und Constraints." ) .addText((text) => text .setPlaceholder("_system/dictionary/chain_templates.yaml") .setValue(this.plugin.settings.chainTemplatesPath) .onChange(async (value) => { this.plugin.settings.chainTemplatesPath = value; await this.plugin.saveSettings(); // Trigger reload if path changes if (this.plugin.settings.chainTemplatesPath) { // Reload will happen on next file modify or can be triggered manually } }) ) .addButton((button) => button .setButtonText("Validate") .setCta() .onClick(async () => { try { const { ChainTemplatesLoader } = await import("../dictionary/ChainTemplatesLoader"); const result = await ChainTemplatesLoader.load( this.app, this.plugin.settings.chainTemplatesPath ); if (result.errors.length > 0) { new Notice( `Chain templates loaded with ${result.errors.length} error(s). Check console.` ); console.warn("Chain templates errors:", result.errors); } else { const templateCount = result.data?.templates?.length || 0; new Notice(`Chain templates file found (${templateCount} template(s))`); } } catch (e) { const msg = e instanceof Error ? e.message : String(e); new Notice(`Failed to load chain templates: ${msg}`); } }) ); // Analysis policies path new Setting(containerEl) .setName("Analysis policies path") .setDesc( "Pfad zur Analysis-Policies-Konfigurationsdatei (YAML). Definiert Severity-Policies für verschiedene Profile (discovery, decisioning, audit) und Finding-Codes." ) .addText((text) => text .setPlaceholder("_system/dictionary/analysis_policies.yaml") .setValue(this.plugin.settings.analysisPoliciesPath) .onChange(async (value) => { this.plugin.settings.analysisPoliciesPath = value; await this.plugin.saveSettings(); }) ) .addButton((button) => button .setButtonText("Validate") .setCta() .onClick(async () => { try { const { parse } = await import("yaml"); const file = this.app.vault.getAbstractFileByPath(this.plugin.settings.analysisPoliciesPath); if (!file || !("extension" in file) || file.extension !== "yaml") { new Notice("Analysis policies file not found"); return; } const text = await this.app.vault.read(file as TFile); const parsed = parse(text); if (parsed && parsed.profiles) { const profileCount = Object.keys(parsed.profiles || {}).length; new Notice(`Analysis policies file found (${profileCount} profile(s))`); } else { new Notice("Analysis policies file found but invalid format"); } } catch (e) { const msg = e instanceof Error ? e.message : String(e); new Notice(`Failed to load analysis policies: ${msg}`); } }) ); // Template matching profile new Setting(containerEl) .setName("Template matching profile") .setDesc( "Profil für Template Matching: 'discovery' (weniger strikt, mehr Findings) oder 'decisioning' (strikter, weniger Findings). Wird aus chain_templates.yaml defaults.profiles geladen." ) .addDropdown((dropdown) => dropdown .addOption("discovery", "Discovery") .addOption("decisioning", "Decisioning") .setValue(this.plugin.settings.templateMatchingProfile) .onChange(async (value) => { this.plugin.settings.templateMatchingProfile = value as "discovery" | "decisioning"; await this.plugin.saveSettings(); }) ); // Chain Inspector: Include candidates new Setting(containerEl) .setName("Chain Inspector: Include candidates") .setDesc( "Wenn aktiviert, werden Candidate-Edges (nicht explizit zugeordnete Links) in die Chain-Analyse einbezogen. Standard: deaktiviert (nur explizite Edges werden analysiert)." ) .addToggle((toggle) => toggle .setValue(this.plugin.settings.chainInspectorIncludeCandidates) .onChange(async (value) => { this.plugin.settings.chainInspectorIncludeCandidates = value; await this.plugin.saveSettings(); }) ); // Chain Inspector: Max template matches new Setting(containerEl) .setName("Chain Inspector: Max template matches") .setDesc( "Maximale Anzahl der Template-Matches, die im Report angezeigt werden. Standard: 3. Höhere Werte zeigen mehr Matches, können aber den Report unübersichtlich machen." ) .addText((text) => text .setPlaceholder("3") .setValue(String(this.plugin.settings.chainInspectorMaxTemplateMatches)) .onChange(async (value) => { const numValue = parseInt(value, 10); if (!isNaN(numValue) && numValue > 0) { this.plugin.settings.chainInspectorMaxTemplateMatches = numValue; await this.plugin.saveSettings(); } }) ); // Max matches per template (default) new Setting(containerEl) .setName("Max matches per template (default)") .setDesc( "Standard: Wie viele verschiedene Zuordnungen pro Template maximal ausgegeben werden (z. B. intra-note + cross-note). 0 = kein Limit. Wird durch chain_templates.yaml (defaults.matching.max_matches_per_template) überschrieben." ) .addText((text) => text .setPlaceholder("2") .setValue(String(this.plugin.settings.maxMatchesPerTemplateDefault)) .onChange(async (value) => { const numValue = parseInt(value, 10); if (!isNaN(numValue) && numValue >= 0 && numValue <= 10000) { this.plugin.settings.maxMatchesPerTemplateDefault = numValue; await this.plugin.saveSettings(); } }) ); // Max assignments collected (loop protection) new Setting(containerEl) .setName("Max assignments collected (loop protection)") .setDesc( "Schleifenschutz: Max. Anzahl gesammelter Zuordnungen pro Template beim Backtracking. Nur zur Absicherung gegen Endlosschleifen; höhere Werte = mehr Kanten können erkannt werden. Überschreibbar durch chain_templates.yaml (defaults.matching.max_assignments_collected)." ) .addText((text) => text .setPlaceholder("1000") .setValue(String(this.plugin.settings.maxAssignmentsCollectedDefault)) .onChange(async (value) => { const numValue = parseInt(value, 10); if (!isNaN(numValue) && numValue > 0 && numValue <= 100000) { this.plugin.settings.maxAssignmentsCollectedDefault = numValue; await this.plugin.saveSettings(); } }) ); // ============================================ // 2. Graph Traversal & Linting // ============================================ containerEl.createEl("h3", { text: "🔍 Graph Traversal & Linting" }); containerEl.createEl("p", { text: "Einstellungen für Graph-Durchquerung und Lint-Validierung.", cls: "setting-item-description", }); // Max hops new Setting(containerEl) .setName("Max hops") .setDesc( "Maximale Anzahl von Hops (Schritten) bei der Graph-Durchquerung. Bestimmt, wie tief der Graph durchsucht wird." ) .addText((text) => text .setPlaceholder("3") .setValue(String(this.plugin.settings.maxHops)) .onChange(async (value) => { const numValue = parseInt(value, 10); if (!isNaN(numValue) && numValue > 0) { this.plugin.settings.maxHops = numValue; await this.plugin.saveSettings(); } }) ); // Chain direction new Setting(containerEl) .setName("Chain direction") .setDesc( "Richtung für Chain-Traversal: 'Forward' (vorwärts), 'Backward' (rückwärts) oder 'Both' (beide Richtungen)." ) .addDropdown((dropdown) => dropdown .addOption("forward", "Forward") .addOption("backward", "Backward") .addOption("both", "Both") .setValue(this.plugin.settings.chainDirection) .onChange(async (value) => { if (value === "forward" || value === "backward" || value === "both") { this.plugin.settings.chainDirection = value; await this.plugin.saveSettings(); } }) ); // Strict mode new Setting(containerEl) .setName("Strict mode") .setDesc( "Aktiviert den strikten Validierungsmodus. Erzwingt strengere Regeln bei der Lint-Validierung." ) .addToggle((toggle) => toggle .setValue(this.plugin.settings.strictMode) .onChange(async (value) => { this.plugin.settings.strictMode = value; await this.plugin.saveSettings(); }) ); // Show canonical hints new Setting(containerEl) .setName("Show canonical hints") .setDesc( "Zeigt INFO-Hinweise mit kanonischer Edge-Typ-Auflösung in den Lint-Ergebnissen an. Hilfreich für die Debugging von Edge-Typ-Zuordnungen." ) .addToggle((toggle) => toggle .setValue(this.plugin.settings.showCanonicalHints) .onChange(async (value) => { this.plugin.settings.showCanonicalHints = value; await this.plugin.saveSettings(); }) ); // ============================================ // 3. Interview & Note Creation // ============================================ containerEl.createEl("h3", { text: "📝 Interview & Note Creation" }); containerEl.createEl("p", { text: "Einstellungen für Interview-Wizard und Notiz-Erstellung.", cls: "setting-item-description", }); // Auto-start interview on create new Setting(containerEl) .setName("Auto-start interview on create") .setDesc( "Startet automatisch den Interview-Wizard, wenn eine neue Notiz über ein Profil erstellt wird." ) .addToggle((toggle) => toggle .setValue(this.plugin.settings.autoStartInterviewOnCreate) .onChange(async (value) => { this.plugin.settings.autoStartInterviewOnCreate = value; await this.plugin.saveSettings(); }) ); // Default notes folder new Setting(containerEl) .setName("Default notes folder") .setDesc( "Standard-Ordner für neue Notizen (vault-relativer Pfad, leer für Root). Profil-spezifische Defaults haben Vorrang." ) .addText((text) => text .setPlaceholder("") .setValue(this.plugin.settings.defaultNotesFolder) .onChange(async (value) => { this.plugin.settings.defaultNotesFolder = value || ""; await this.plugin.saveSettings(); }) ); // ============================================ // 4. Unresolved Link Handling // ============================================ containerEl.createEl("h3", { text: "🔗 Unresolved Link Handling" }); containerEl.createEl("p", { text: "Konfiguration für das Verhalten bei Klicks auf nicht aufgelöste Links.", cls: "setting-item-description", }); // Intercept unresolved links new Setting(containerEl) .setName("Intercept unresolved link clicks") .setDesc( "Aktiviert die Abfangen von Klicks auf nicht aufgelöste interne Links. Öffnet die Profilauswahl, wenn auf einen nicht existierenden Link geklickt wird." ) .addToggle((toggle) => toggle .setValue(this.plugin.settings.interceptUnresolvedLinkClicks) .onChange(async (value) => { this.plugin.settings.interceptUnresolvedLinkClicks = value; await this.plugin.saveSettings(); }) ); // Auto-start interview on unresolved click new Setting(containerEl) .setName("Auto-start interview on unresolved link click") .setDesc( "Startet automatisch den Interview-Wizard, wenn eine Notiz aus einem nicht aufgelösten Link erstellt wird. Erfordert, dass 'Intercept unresolved link clicks' aktiviert ist." ) .addToggle((toggle) => toggle .setValue(this.plugin.settings.autoStartOnUnresolvedClick) .onChange(async (value) => { this.plugin.settings.autoStartOnUnresolvedClick = value; await this.plugin.saveSettings(); }) ); // Bypass modifier (Reading View) new Setting(containerEl) .setName("Bypass modifier (Reading View)") .setDesc( "Modifier-Taste zum Umgehen des Link-Intercepts in der Leseansicht. Wenn diese Taste gedrückt wird, wird der Standard-Link-Klick ausgeführt (z.B. zum Erstellen einer leeren Notiz)." ) .addDropdown((dropdown) => dropdown .addOption("Alt", "Alt") .addOption("Ctrl", "Ctrl/Cmd") .addOption("Shift", "Shift") .addOption("None", "None") .setValue(this.plugin.settings.bypassModifier) .onChange(async (value) => { if (value === "Alt" || value === "Ctrl" || value === "Shift" || value === "None") { this.plugin.settings.bypassModifier = value; await this.plugin.saveSettings(); } }) ); // Editor follow modifier (Live Preview/Source) new Setting(containerEl) .setName("Editor follow modifier (Live Preview/Source)") .setDesc( "Modifier-Taste, die im Editor (Live Preview/Source) gedrückt werden muss, um den Link-Intercept zu aktivieren. Standard: Ctrl/Cmd. Verhindert versehentliche Intercepts beim normalen Bearbeiten." ) .addDropdown((dropdown) => dropdown .addOption("Alt", "Alt") .addOption("Ctrl", "Ctrl/Cmd") .addOption("Shift", "Shift") .addOption("None", "None") .setValue(this.plugin.settings.editorFollowModifier) .onChange(async (value) => { if (value === "Alt" || value === "Ctrl" || value === "Shift" || value === "None") { this.plugin.settings.editorFollowModifier = value; await this.plugin.saveSettings(); } }) ); // ============================================ // 5. Templater Compatibility // ============================================ containerEl.createEl("h3", { text: "⚙️ Templater Compatibility" }); containerEl.createEl("p", { text: "Einstellungen für die Kompatibilität mit dem Templater-Plugin.", cls: "setting-item-description", }); // Wait for first modify after create new Setting(containerEl) .setName("Wait for first modify after create") .setDesc( "Wartet, bis Templater die Datei modifiziert hat, bevor der Wizard gestartet wird. Empfohlen, wenn Templater verwendet wird, um Konflikte zu vermeiden." ) .addToggle((toggle) => toggle .setValue(this.plugin.settings.waitForFirstModifyAfterCreate) .onChange(async (value) => { this.plugin.settings.waitForFirstModifyAfterCreate = value; await this.plugin.saveSettings(); }) ); // Modify timeout new Setting(containerEl) .setName("Modify timeout (ms)") .setDesc( "Timeout in Millisekunden für das Warten auf ein File-Modify-Event. Wenn Templater nicht innerhalb dieser Zeit reagiert, wird der Wizard trotzdem gestartet." ) .addText((text) => text .setPlaceholder("1200") .setValue(String(this.plugin.settings.waitForModifyTimeoutMs)) .onChange(async (value) => { const numValue = parseInt(value, 10); if (!isNaN(numValue) && numValue > 0) { this.plugin.settings.waitForModifyTimeoutMs = numValue; await this.plugin.saveSettings(); } }) ); // ============================================ // 6. Note Adoption // ============================================ containerEl.createEl("h3", { text: "🔄 Note Adoption" }); containerEl.createEl("p", { text: "Einstellungen für die automatische Übernahme neu erstellter Notizen im Editor.", cls: "setting-item-description", }); // Adopt new notes in editor new Setting(containerEl) .setName("Adopt new notes in editor") .setDesc( "Übernimmt automatisch neu erstellte Notizen im Editor und konvertiert sie in das Mindnet-Format (mit Frontmatter-ID). Nützlich, wenn Obsidian eine Notiz direkt erstellt, bevor unser Intercept greift." ) .addToggle((toggle) => toggle .setValue(this.plugin.settings.adoptNewNotesInEditor) .onChange(async (value) => { this.plugin.settings.adoptNewNotesInEditor = value; await this.plugin.saveSettings(); }) ); // Adopt max chars new Setting(containerEl) .setName("Adopt max chars") .setDesc( "Maximale Inhaltslänge (in Zeichen), um eine Notiz als Übernahme-Kandidat zu betrachten. Leere oder sehr kurze Notizen werden bevorzugt übernommen." ) .addText((text) => text .setPlaceholder("200") .setValue(String(this.plugin.settings.adoptMaxChars)) .onChange(async (value) => { const numValue = parseInt(value, 10); if (!isNaN(numValue) && numValue > 0) { this.plugin.settings.adoptMaxChars = numValue; await this.plugin.saveSettings(); } }) ); // Adopt confirm mode new Setting(containerEl) .setName("Adopt confirm mode") .setDesc( "Wann die Übernahme-Bestätigung angezeigt wird: 'Always ask' (immer fragen), 'Only for low confidence' (nur bei niedriger Konfidenz, überspringt bei hoher Konfidenz), oder 'Never ask' (nie fragen, automatisch übernehmen)." ) .addDropdown((dropdown) => dropdown .addOption("always", "Always ask") .addOption("onlyLowConfidence", "Only for low confidence") .addOption("never", "Never ask") .setValue(this.plugin.settings.adoptConfirmMode) .onChange(async (value) => { if (value === "always" || value === "onlyLowConfidence" || value === "never") { this.plugin.settings.adoptConfirmMode = value; await this.plugin.saveSettings(); } }) ); // High confidence window new Setting(containerEl) .setName("High confidence window (ms)") .setDesc( "Zeitfenster in Millisekunden für die hohe Konfidenz-Übernahme. Notizen, die innerhalb dieses Zeitfensters nach einem Klick erstellt wurden, werden mit hoher Konfidenz als Übernahme-Kandidaten betrachtet." ) .addText((text) => text .setPlaceholder("3000") .setValue(String(this.plugin.settings.highConfidenceWindowMs)) .onChange(async (value) => { const numValue = parseInt(value, 10); if (!isNaN(numValue) && numValue > 0) { this.plugin.settings.highConfidenceWindowMs = numValue; await this.plugin.saveSettings(); } }) ); // ============================================ // 7. Semantic Mapping Builder // ============================================ containerEl.createEl("h3", { text: "🕸️ Semantic Mapping Builder" }); containerEl.createEl("p", { text: "Einstellungen für den Semantic Mapping Builder, der automatisch Mapping-Blöcke in Notizen erstellt.", cls: "setting-item-description", }); // Wrapper callout type new Setting(containerEl) .setName("Mapping wrapper callout type") .setDesc( "Callout-Typ für den Mapping-Wrapper (z.B. 'abstract', 'info', 'note'). Bestimmt das visuelle Erscheinungsbild des Mapping-Blocks." ) .addText((text) => text .setPlaceholder("abstract") .setValue(this.plugin.settings.mappingWrapperCalloutType) .onChange(async (value) => { this.plugin.settings.mappingWrapperCalloutType = value || "abstract"; await this.plugin.saveSettings(); }) ); // Wrapper title new Setting(containerEl) .setName("Mapping wrapper title") .setDesc( "Titel-Text für den Mapping-Wrapper-Callout. Wird als Überschrift des Mapping-Blocks angezeigt." ) .addText((text) => text .setPlaceholder("🕸️ Semantic Mapping") .setValue(this.plugin.settings.mappingWrapperTitle) .onChange(async (value) => { this.plugin.settings.mappingWrapperTitle = value || "🕸️ Semantic Mapping"; await this.plugin.saveSettings(); }) ); // Wrapper folded new Setting(containerEl) .setName("Mapping wrapper folded") .setDesc( "Startet mit dem Mapping-Wrapper-Callout eingeklappt (collapsed). Reduziert visuellen Lärm in der Notiz." ) .addToggle((toggle) => toggle .setValue(this.plugin.settings.mappingWrapperFolded) .onChange(async (value) => { this.plugin.settings.mappingWrapperFolded = value; await this.plugin.saveSettings(); }) ); // Unassigned handling new Setting(containerEl) .setName("Unassigned handling") .setDesc( "Wie Links ohne bestehende Mappings behandelt werden: 'Prompt (interactive)' (interaktive Auswahl), 'None (skip unmapped)' (überspringen), 'Use default edge type' (Standard-Typ verwenden), oder 'Use advisor' (zukünftig: E1-Engine)." ) .addDropdown((dropdown) => dropdown .addOption("prompt", "Prompt (interactive)") .addOption("none", "None (skip unmapped)") .addOption("defaultType", "Use default edge type") .addOption("advisor", "Use advisor (future: E1 engine)") .setValue(this.plugin.settings.unassignedHandling) .onChange(async (value) => { if (value === "prompt" || value === "none" || value === "defaultType" || value === "advisor") { this.plugin.settings.unassignedHandling = value; await this.plugin.saveSettings(); } }) ); // Default edge type new Setting(containerEl) .setName("Default edge type") .setDesc( "Standard-Edge-Typ für nicht zugeordnete Links. Wird nur verwendet, wenn 'Unassigned handling' auf 'Use default edge type' gesetzt ist." ) .addText((text) => text .setPlaceholder("") .setValue(this.plugin.settings.defaultEdgeType) .onChange(async (value) => { this.plugin.settings.defaultEdgeType = value; await this.plugin.saveSettings(); }) ); // Allow overwrite existing mappings new Setting(containerEl) .setName("Allow overwrite existing mappings") .setDesc( "Wenn aktiviert, wird vor dem Überschreiben bestehender Edge-Typ-Zuordnungen eine Bestätigung angefordert. Wenn deaktiviert, werden bestehende Mappings immer beibehalten." ) .addToggle((toggle) => toggle .setValue(this.plugin.settings.allowOverwriteExistingMappings) .onChange(async (value) => { this.plugin.settings.allowOverwriteExistingMappings = value; await this.plugin.saveSettings(); }) ); // Inline micro edge suggester new Setting(containerEl) .setName("Inline micro edge suggester enabled") .setDesc( "Aktiviert den Inline-Micro-Edge-Suggester. Zeigt nach dem Einfügen eines Links über den Entity Picker sofort eine Edge-Typ-Auswahl an (nur wenn Profil edging.mode=inline_micro)." ) .addToggle((toggle) => toggle .setValue(this.plugin.settings.inlineMicroEnabled) .onChange(async (value) => { this.plugin.settings.inlineMicroEnabled = value; await this.plugin.saveSettings(); }) ); // Inline max alternatives new Setting(containerEl) .setName("Inline max alternatives") .setDesc( "Maximale Anzahl von alternativen Edge-Typen, die im Inline-Micro-Modal angezeigt werden (Standard: 6)." ) .addText((text) => text .setPlaceholder("6") .setValue(String(this.plugin.settings.inlineMaxAlternatives)) .onChange(async (value) => { const numValue = parseInt(value, 10); if (!isNaN(numValue) && numValue > 0) { this.plugin.settings.inlineMaxAlternatives = numValue; await this.plugin.saveSettings(); } }) ); // ============================================ // 8. Backend Logging Configuration // ============================================ containerEl.createEl("h3", { text: "📝 Backend Logging" }); containerEl.createEl("p", { text: "Konfiguration für das Backend-Logging. Steuert Log-Level, Dateigröße und Rotation. Diese Einstellungen werden beim Backend-Start verwendet.", cls: "setting-item-description", }); // Log level new Setting(containerEl) .setName("Log level") .setDesc( "Log-Level für das Backend: 'INFO' (Standard, weniger Zeilen), 'WARNING' (nur Warnungen/Fehler), 'ERROR' (nur Fehler), oder 'DEBUG' (sehr ausführlich, viele Zeilen)." ) .addDropdown((dropdown) => dropdown .addOption("INFO", "INFO") .addOption("WARNING", "WARNING") .addOption("ERROR", "ERROR") .addOption("DEBUG", "DEBUG") .setValue(this.plugin.settings.logLevel) .onChange(async (value) => { if (value === "DEBUG" || value === "INFO" || value === "WARNING" || value === "ERROR") { this.plugin.settings.logLevel = value; await this.plugin.saveSettings(); } }) ); // Log max bytes new Setting(containerEl) .setName("Log max bytes") .setDesc( "Maximale Größe einer Log-Datei in Bytes vor Rotation. Standard: 1048576 (1 MB). Kleinere Werte führen zu häufigerer Rotation und kürzeren Einzeldateien. Empfohlen: 524288 (512 KB) für sehr große Logs." ) .addText((text) => text .setPlaceholder("1048576") .setValue(String(this.plugin.settings.logMaxBytes)) .onChange(async (value) => { const numValue = parseInt(value, 10); if (!isNaN(numValue) && numValue > 0) { this.plugin.settings.logMaxBytes = numValue; await this.plugin.saveSettings(); } }) ); // Log backup count new Setting(containerEl) .setName("Log backup count") .setDesc( "Anzahl rotierter Backup-Dateien für logs/mindnet.log. Standard: 2. Bei Erreichen von 'Log max bytes' wird die aktuelle Datei rotiert (mindnet.log.1, mindnet.log.2, etc.)." ) .addText((text) => text .setPlaceholder("2") .setValue(String(this.plugin.settings.logBackupCount)) .onChange(async (value) => { const numValue = parseInt(value, 10); if (!isNaN(numValue) && numValue >= 0) { this.plugin.settings.logBackupCount = numValue; await this.plugin.saveSettings(); } }) ); // ============================================ // 9. Debug & Development // ============================================ containerEl.createEl("h3", { text: "🐛 Debug & Development" }); containerEl.createEl("p", { text: "Einstellungen für Debugging und Entwicklung.", cls: "setting-item-description", }); // Debug logging new Setting(containerEl) .setName("Debug logging") .setDesc( "Aktiviert ausführliches Debug-Logging für das Unresolved-Link-Handling. Logs erscheinen in der Browser-Konsole (F12). Nützlich für die Fehlersuche." ) .addToggle((toggle) => toggle .setValue(this.plugin.settings.debugLogging) .onChange(async (value) => { this.plugin.settings.debugLogging = value; await this.plugin.saveSettings(); }) ); // ============================================ // 9.1. Module-specific Logging // ============================================ containerEl.createEl("h3", { text: "📊 Modulspezifisches Logging" }); containerEl.createEl("p", { text: "Konfigurieren Sie individuelle Log-Level für verschiedene Module. Nützlich zum gezielten Debugging von Chain-Matching und Edge-Erkennung. Logs erscheinen in der Browser-Konsole (F12).", cls: "setting-item-description", }); // Ensure moduleLogLevels exists if (!this.plugin.settings.moduleLogLevels) { this.plugin.settings.moduleLogLevels = {}; } // Define important modules for chain matching const importantModules = [ { name: "templateMatching", description: "Template-Matching: Erkennt Chain-Templates und ordnet Slots zu" }, { name: "chainInspector", description: "Chain Inspector: Analysiert Chains und findet fehlende Kanten" }, { name: "todoGenerator", description: "Todo Generator: Generiert Todos für fehlende Slots und Links" }, { name: "graphIndex", description: "Graph Index: Baut den Graph-Index auf und verwaltet Edges" }, { name: "workbenchBuilder", description: "Workbench Builder: Baut Workbench-Matches auf" }, { name: "chainWorkbenchCommand", description: "Chain Workbench Command: Hauptkommando für Chain-Workbench" }, ]; // Create settings for each module for (const module of importantModules) { const currentLevel = this.plugin.settings.moduleLogLevels[module.name] || "INFO"; new Setting(containerEl) .setName(`${module.name}`) .setDesc(module.description) .addDropdown((dropdown) => { dropdown .addOption("NONE", "NONE (keine Logs)") .addOption("ERROR", "ERROR (nur Fehler)") .addOption("WARN", "WARN (Warnungen und Fehler)") .addOption("INFO", "INFO (Standard)") .addOption("DEBUG", "DEBUG (sehr ausführlich)") .setValue(currentLevel) .onChange(async (value) => { if (value === "NONE" || value === "ERROR" || value === "WARN" || value === "INFO" || value === "DEBUG") { if (value === "INFO") { // Remove from config if set to default delete this.plugin.settings.moduleLogLevels[module.name]; } else { this.plugin.settings.moduleLogLevels[module.name] = value; } await this.plugin.saveSettings(); // Update logger registry immediately const { initializeLogging } = await import("../utils/logger"); initializeLogging(this.plugin.settings.moduleLogLevels); } }); }); } // Add button to reset all to defaults new Setting(containerEl) .setName("Alle auf Standard zurücksetzen") .setDesc("Setzt alle Modul-Log-Level auf INFO (Standard) zurück.") .addButton((button) => button .setButtonText("Zurücksetzen") .onClick(async () => { this.plugin.settings.moduleLogLevels = {}; await this.plugin.saveSettings(); // Update logger registry const { initializeLogging } = await import("../utils/logger"); initializeLogging({}); // Refresh settings UI this.display(); }) ); // ============================================ // 10. Export // ============================================ containerEl.createEl("h3", { text: "📤 Export" }); containerEl.createEl("p", { text: "Einstellungen für den Graph-Export.", cls: "setting-item-description", }); // Export path new Setting(containerEl) .setName("Export path") .setDesc( "Pfad für die exportierte Graph-JSON-Datei. Die Datei enthält alle Knoten (Nodes) und Kanten (Edges) aus dem Vault." ) .addText((text) => text .setPlaceholder("_system/exports/graph_export.json") .setValue(this.plugin.settings.exportPath) .onChange(async (value) => { this.plugin.settings.exportPath = value || "_system/exports/graph_export.json"; await this.plugin.saveSettings(); }) ) .addButton((button) => button .setButtonText("Export now") .setCta() .onClick(async () => { try { // Load vocabulary using the same method as the command const { VocabularyLoader } = await import("../vocab/VocabularyLoader"); const { parseEdgeVocabulary } = await import("../vocab/parseEdgeVocabulary"); const { Vocabulary } = await import("../vocab/Vocabulary"); const text = await VocabularyLoader.loadText( this.app, this.plugin.settings.edgeVocabularyPath ); const parsed = parseEdgeVocabulary(text); const vocabulary = new Vocabulary(parsed); const { exportGraph } = await import("../export/exportGraph"); await exportGraph(this.app, vocabulary, this.plugin.settings.exportPath); new Notice(`Graph exported to ${this.plugin.settings.exportPath}`); console.log(`Graph exported: ${this.plugin.settings.exportPath}`); } catch (e) { const msg = e instanceof Error ? e.message : String(e); new Notice(`Failed to export graph: ${msg}`); console.error(e); } }) ); // ============================================ // 11. Fix Actions Settings // ============================================ containerEl.createEl("h3", { text: "🔧 Fix Actions" }); containerEl.createEl("p", { text: "Einstellungen für automatische Fix-Aktionen bei Chain Inspector Findings.", cls: "setting-item-description", }); // Create missing note mode new Setting(containerEl) .setName("Create missing note mode") .setDesc( "Verhalten beim Erstellen fehlender Noten: 'Skeleton only' (nur Frontmatter), 'Create and open profile picker' (mit Profil-Auswahl), 'Create and start wizard' (mit Wizard)." ) .addDropdown((dropdown) => dropdown .addOption("skeleton_only", "Skeleton only") .addOption("create_and_open_profile_picker", "Create and open profile picker") .addOption("create_and_start_wizard", "Create and start wizard") .setValue(this.plugin.settings.fixActions.createMissingNote.mode) .onChange(async (value) => { if ( value === "skeleton_only" || value === "create_and_open_profile_picker" || value === "create_and_start_wizard" ) { this.plugin.settings.fixActions.createMissingNote.mode = value; await this.plugin.saveSettings(); } }) ); // Default type strategy new Setting(containerEl) .setName("Default type strategy") .setDesc( "Strategie für Note-Typ-Zuweisung: 'Profile picker' (immer Picker zeigen), 'Inference then picker' (heuristische Vorauswahl), 'Default concept no prompt' (Standard 'concept' ohne Prompt)." ) .addDropdown((dropdown) => dropdown .addOption("profile_picker", "Profile picker") .addOption("inference_then_picker", "Inference then picker") .addOption("default_concept_no_prompt", "Default concept no prompt") .setValue(this.plugin.settings.fixActions.createMissingNote.defaultTypeStrategy) .onChange(async (value) => { if ( value === "profile_picker" || value === "inference_then_picker" || value === "default_concept_no_prompt" ) { this.plugin.settings.fixActions.createMissingNote.defaultTypeStrategy = value; await this.plugin.saveSettings(); } }) ); // Include zones new Setting(containerEl) .setName("Include zones") .setDesc( "Welche Zonen in neu erstellten Noten einfügen: 'None' (keine), 'Note links only' (nur Note-Verbindungen), 'Candidates only' (nur Kandidaten), 'Both' (beide)." ) .addDropdown((dropdown) => dropdown .addOption("none", "None") .addOption("note_links_only", "Note links only") .addOption("candidates_only", "Candidates only") .addOption("both", "Both") .setValue(this.plugin.settings.fixActions.createMissingNote.includeZones) .onChange(async (value) => { if ( value === "none" || value === "note_links_only" || value === "candidates_only" || value === "both" ) { this.plugin.settings.fixActions.createMissingNote.includeZones = value; await this.plugin.saveSettings(); } }) ); // Create missing heading level new Setting(containerEl) .setName("Create missing heading level") .setDesc( "Heading-Level für neu erstellte Headings (1-6). Standard: 2 (H2)." ) .addText((text) => text .setPlaceholder("2") .setValue(String(this.plugin.settings.fixActions.createMissingHeading.level)) .onChange(async (value) => { const numValue = parseInt(value, 10); if (!isNaN(numValue) && numValue >= 1 && numValue <= 6) { this.plugin.settings.fixActions.createMissingHeading.level = numValue; await this.plugin.saveSettings(); } }) ); // Promote candidate keep original new Setting(containerEl) .setName("Promote candidate: Keep original") .setDesc( "Wenn aktiviert, bleibt das ursprüngliche Candidate-Edge im Kandidaten-Bereich erhalten, wenn es zu einem expliziten Edge befördert wird." ) .addToggle((toggle) => toggle .setValue(this.plugin.settings.fixActions.promoteCandidate.keepOriginal) .onChange(async (value) => { this.plugin.settings.fixActions.promoteCandidate.keepOriginal = value; await this.plugin.saveSettings(); }) ); } }