import { App, Notice, PluginSettingTab, Setting } 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}`); } }) ); // ============================================ // 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(); }) ); // ============================================ // 8. 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(); }) ); } }