From 2fcf333e5676b0c56d42b0aec02768337ff4e367 Mon Sep 17 00:00:00 2001 From: Lars Date: Sat, 17 Jan 2026 13:59:26 +0100 Subject: [PATCH] Enhance edge type handling and categorization - Added optional description and category fields to edge type entries, improving metadata for edge types. - Updated the `getAllEdgeTypes` and `groupEdgeTypesByCategory` functions to utilize new fields for better organization and display. - Enhanced UI components to show descriptions as tooltips and categorize edge types in the EdgeTypeChooserModal and InlineEdgeTypeModal. - Improved parsing logic in `parseEdgeVocabulary` to extract descriptions and categories from the vocabulary table, ensuring richer edge type data. - Adjusted the LinkPromptModal to clarify edge type actions and maintain alias information during selection. --- src/mapping/schemaHelper.ts | 50 +++++- src/mapping/semanticMappingBuilder.ts | 12 +- src/ui/EdgeTypeChooserModal.ts | 65 ++++++-- src/ui/InlineEdgeTypeModal.ts | 218 +++++++++++++++++++++----- src/ui/LinkPromptModal.ts | 4 +- src/vocab/parseEdgeVocabulary.ts | 70 ++++++++- src/vocab/types.ts | 2 + 7 files changed, 353 insertions(+), 68 deletions(-) diff --git a/src/mapping/schemaHelper.ts b/src/mapping/schemaHelper.ts index 9448aa6..d399444 100644 --- a/src/mapping/schemaHelper.ts +++ b/src/mapping/schemaHelper.ts @@ -39,11 +39,15 @@ export function getAllEdgeTypes(vocabulary: EdgeVocabulary): Array<{ canonical: string; aliases: string[]; displayName: string; // First alias or canonical + description?: string; + category?: string; }> { const result: Array<{ canonical: string; aliases: string[]; displayName: string; + description?: string; + category?: string; }> = []; for (const [canonical, entry] of vocabulary.byCanonical.entries()) { @@ -53,6 +57,8 @@ export function getAllEdgeTypes(vocabulary: EdgeVocabulary): Array<{ canonical, aliases: entry.aliases, displayName, + description: entry.description, + category: entry.category, }); } @@ -64,14 +70,42 @@ export function getAllEdgeTypes(vocabulary: EdgeVocabulary): Array<{ /** * Group edge types by category (if available). - * MVP: Returns single "All" category. + * Uses category from EdgeTypeEntry if available. + * If categories are found, groups by category. Otherwise returns single "All" group. */ export function groupEdgeTypesByCategory( - edgeTypes: Array<{ canonical: string; aliases: string[]; displayName: string }> -): Map> { - // TODO: Implement category grouping from schema - // For MVP, return single "All" category - const grouped = new Map>(); - grouped.set("All", edgeTypes); - return grouped; + edgeTypes: Array<{ canonical: string; aliases: string[]; displayName: string; description?: string; category?: string }> +): Map> { + const grouped = new Map>(); + + // Check if any edge types have categories + const hasCategories = edgeTypes.some(e => e.category && e.category.trim() !== ""); + + if (hasCategories) { + // Group by category + for (const edgeType of edgeTypes) { + const category = edgeType.category && edgeType.category.trim() !== "" + ? edgeType.category.trim() + : "Allgemein"; // Default category if not specified + if (!grouped.has(category)) { + grouped.set(category, []); + } + grouped.get(category)!.push(edgeType); + } + + // Sort categories alphabetically + const sortedCategories = Array.from(grouped.keys()).sort(); + const sortedGrouped = new Map>(); + for (const category of sortedCategories) { + const types = grouped.get(category); + if (types) { + sortedGrouped.set(category, types); + } + } + return sortedGrouped; + } else { + // No categories found, use "All" + grouped.set("All", edgeTypes); + return grouped; + } } diff --git a/src/mapping/semanticMappingBuilder.ts b/src/mapping/semanticMappingBuilder.ts index 44e7060..ff9bf26 100644 --- a/src/mapping/semanticMappingBuilder.ts +++ b/src/mapping/semanticMappingBuilder.ts @@ -227,13 +227,18 @@ export async function buildSemanticMappings( // Apply decision if (decision.action === "keep") { + // Use the current type as-is (preserves alias if it was an alias) + // item.currentType is already the actual type from existingMappings (could be alias or canonical) mappingsToUse.set(item.link, decision.edgeType); if (mappingState.existingMappings.has(item.link)) { result.existingMappingsKept++; } } else if (decision.action === "change") { - // Use canonical type (alias is for display only) - mappingsToUse.set(item.link, decision.edgeType); + // Use alias if provided, otherwise use canonical type + // This ensures selected aliases are written to the file, not just canonical types + const finalEdgeType = decision.alias || decision.edgeType; + mappingsToUse.set(item.link, finalEdgeType); + console.log(`[Mindnet] Setting edge type for ${item.link}: ${finalEdgeType} (canonical: ${decision.edgeType}, alias: ${decision.alias || "none"})`); if (mappingState.existingMappings.has(item.link)) { // Changed existing } else { @@ -248,9 +253,10 @@ export async function buildSemanticMappings( } } else { // Silent modes: none/defaultType/advisor - // Always include existing mappings + // Always include existing mappings (preserve aliases as they are) for (const [link, edgeType] of mappingState.existingMappings.entries()) { if (section.links.includes(link)) { + // Use existing type as-is (could be alias or canonical) mappingsToUse.set(link, edgeType); result.existingMappingsKept++; } diff --git a/src/ui/EdgeTypeChooserModal.ts b/src/ui/EdgeTypeChooserModal.ts index 56bed42..6535b27 100644 --- a/src/ui/EdgeTypeChooserModal.ts +++ b/src/ui/EdgeTypeChooserModal.ts @@ -52,6 +52,13 @@ export class EdgeTypeChooserModal extends Modal { const allEdgeTypes = getAllEdgeTypes(this.vocabulary); const grouped = groupEdgeTypesByCategory(allEdgeTypes); + // Debug: Log grouping info + console.log("[Mindnet] EdgeTypeChooserModal:", { + totalEdgeTypes: allEdgeTypes.length, + categories: Array.from(grouped.keys()), + categorySizes: Array.from(grouped.entries()).map(([cat, types]) => ({ category: cat, count: types.length })), + }); + // Recommended section (if any) if (suggestions.typical.length > 0) { contentEl.createEl("h3", { text: "⭐ Recommended (schema)" }); @@ -61,11 +68,18 @@ export class EdgeTypeChooserModal extends Modal { const entry = this.vocabulary.byCanonical.get(canonical); if (!entry) continue; - const displayName = entry.aliases.length > 0 ? entry.aliases[0] : canonical; + // Display: canonical type (primary), aliases shown after selection const btn = recommendedContainer.createEl("button", { - text: `⭐ ${displayName}`, + text: `⭐ ${canonical}`, cls: "mod-cta", }); + + // Add description as tooltip/hover text + if (entry.description) { + btn.title = entry.description; + btn.setAttribute("data-description", entry.description); + } + btn.onclick = () => { this.selectEdgeType(canonical, entry.aliases); }; @@ -73,16 +87,24 @@ export class EdgeTypeChooserModal extends Modal { } // All categories - contentEl.createEl("h3", { text: "📂 All categories" }); + if (grouped.size > 1 || (grouped.size === 1 && !grouped.has("All"))) { + contentEl.createEl("h3", { text: "📂 All categories" }); + } for (const [category, types] of grouped.entries()) { - if (category !== "All") { - contentEl.createEl("h4", { text: category }); + // Show category heading if not "All" or if there are multiple categories + if (category !== "All" || grouped.size > 1) { + const categoryHeading = contentEl.createEl("h4", { text: category }); + // Add description if available (from first entry in category) + const firstType = types[0]; + if (firstType && firstType.description) { + categoryHeading.title = firstType.description; + } } const container = contentEl.createEl("div", { cls: "edge-type-list" }); - for (const { canonical, aliases, displayName } of types) { + for (const { canonical, aliases, displayName, description } of types) { const isProhibited = suggestions.prohibited.includes(canonical); const isTypical = suggestions.typical.includes(canonical); @@ -90,14 +112,21 @@ export class EdgeTypeChooserModal extends Modal { if (isTypical) prefix = "⭐"; if (isProhibited) prefix = "🚫"; + // Display: canonical type (primary), aliases shown after selection const btn = container.createEl("button", { - text: `${prefix} ${displayName}`, + text: `${prefix} ${canonical}`, }); if (isProhibited) { btn.addClass("prohibited"); } + // Add description as tooltip/hover text + if (description) { + btn.title = description; + btn.setAttribute("data-description", description); + } + btn.onclick = () => { this.selectEdgeType(canonical, aliases); }; @@ -122,12 +151,8 @@ export class EdgeTypeChooserModal extends Modal { // No aliases, use canonical directly this.result = { edgeType: canonical }; this.close(); - } else if (aliases.length === 1) { - // Single alias, use it - this.result = { edgeType: canonical, alias: aliases[0] }; - this.close(); } else { - // Multiple aliases, show alias chooser + // Always show alias chooser if aliases exist (even for single alias) this.selectedCanonical = canonical; this.showAliasChooser(aliases); } @@ -138,14 +163,26 @@ export class EdgeTypeChooserModal extends Modal { contentEl.empty(); contentEl.createEl("h2", { text: "Choose alias" }); - contentEl.createEl("p", { text: `Multiple aliases available for ${this.selectedCanonical}` }); + contentEl.createEl("p", { text: `Aliases available for ${this.selectedCanonical}` }); const container = contentEl.createEl("div", { cls: "alias-list" }); + // Option: Use canonical (no alias) + const canonicalBtn = container.createEl("button", { + text: `${this.selectedCanonical} (canonical)`, + cls: "mod-cta", + }); + canonicalBtn.onclick = () => { + if (this.selectedCanonical) { + this.result = { edgeType: this.selectedCanonical }; + this.close(); + } + }; + + // All aliases for (const alias of aliases) { const btn = container.createEl("button", { text: alias, - cls: "mod-cta", }); btn.onclick = () => { if (this.selectedCanonical) { diff --git a/src/ui/InlineEdgeTypeModal.ts b/src/ui/InlineEdgeTypeModal.ts index 52512df..813bf3f 100644 --- a/src/ui/InlineEdgeTypeModal.ts +++ b/src/ui/InlineEdgeTypeModal.ts @@ -67,6 +67,20 @@ export class InlineEdgeTypeModal extends Modal { cls: "inline-edge-type-modal__link-info", }); linkInfo.textContent = `Link: [[${this.linkBasename}]]`; + + // Show selected type if one was chosen (e.g., from chooser) + if (this.selectedEdgeType) { + const selectedInfo = contentEl.createEl("div", { + cls: "inline-edge-type-modal__selected-info", + }); + const displayType = this.selectedAlias || this.selectedEdgeType; + selectedInfo.textContent = `Ausgewählt: ${displayType}`; + selectedInfo.style.marginTop = "0.5em"; + selectedInfo.style.padding = "0.5em"; + selectedInfo.style.backgroundColor = "var(--background-modifier-hover)"; + selectedInfo.style.borderRadius = "4px"; + selectedInfo.style.fontWeight = "bold"; + } // Get recommendations const recommendations = this.getRecommendations(); @@ -80,10 +94,19 @@ export class InlineEdgeTypeModal extends Modal { return; } - // Auto-select first typical if available + // Auto-select first typical if available (use canonical, alias selection comes later) + // Only auto-select if no type has been explicitly selected yet if (typical.length > 0 && typical[0] && !this.selectedEdgeType) { - this.selectedEdgeType = typical[0]; - this.result = { chosenRawType: typical[0], cancelled: false }; + const firstTypical = typical[0]; + this.selectedEdgeType = firstTypical; + // Use canonical as default, alias will be selected in alias chooser if needed + this.result = { chosenRawType: firstTypical, cancelled: false }; + } + + // If a type was already selected (e.g., from chooser), ensure it's reflected in result + if (this.selectedEdgeType && !this.result) { + const rawType = this.selectedAlias || this.selectedEdgeType; + this.result = { chosenRawType: rawType, cancelled: false }; } // Recommended chips (typical) - all are directly selectable, first one is preselected @@ -100,21 +123,45 @@ export class InlineEdgeTypeModal extends Modal { }); for (let i = 0; i < typical.length; i++) { - const edgeType = typical[i]; - if (!edgeType) continue; + const canonical = typical[i]; + if (!canonical) continue; + // Get entry for description + let description: string | undefined = undefined; + let aliases: string[] = []; + if (this.vocabulary) { + const entry = this.vocabulary.byCanonical.get(canonical); + if (entry) { + description = entry.description; + aliases = entry.aliases; + } + } + + // Display: canonical type (primary) const chip = chipsContainer.createEl("button", { cls: "inline-edge-type-modal__chip", - text: edgeType, + text: canonical, }); - // First typical is preselected - if (i === 0) { + // Add description as tooltip/hover text + if (description) { + chip.title = description; + chip.setAttribute("data-description", description); + } + + // Mark as selected if this is the currently selected type + const isSelected = this.selectedEdgeType === canonical; + if (isSelected || (i === 0 && !this.selectedEdgeType)) { chip.addClass("mod-cta"); } chip.onclick = () => { - this.selectEdgeType(edgeType); + // If aliases exist, show alias chooser, otherwise use canonical + if (aliases.length > 0) { + this.showAliasChooser(canonical, aliases); + } else { + this.selectEdgeType(canonical); + } }; } } @@ -133,12 +180,38 @@ export class InlineEdgeTypeModal extends Modal { cls: "inline-edge-type-modal__chips", }); - for (const edgeType of alternatives) { + for (const canonical of alternatives) { + // Get entry for description + let description: string | undefined = undefined; + let aliases: string[] = []; + if (this.vocabulary) { + const entry = this.vocabulary.byCanonical.get(canonical); + if (entry) { + description = entry.description; + aliases = entry.aliases; + } + } + + // Display: canonical type (primary) const chip = chipsContainer.createEl("button", { cls: "inline-edge-type-modal__chip", - text: edgeType, + text: canonical, }); - chip.onclick = () => this.selectEdgeType(edgeType); + + // Add description as tooltip/hover text + if (description) { + chip.title = description; + chip.setAttribute("data-description", description); + } + + chip.onclick = () => { + // If aliases exist, show alias chooser, otherwise use canonical + if (aliases.length > 0) { + this.showAliasChooser(canonical, aliases); + } else { + this.selectEdgeType(canonical); + } + }; } } @@ -155,11 +228,28 @@ export class InlineEdgeTypeModal extends Modal { cls: "inline-edge-type-modal__chips", }); - for (const edgeType of prohibited) { + for (const canonical of prohibited) { + // Get entry for description + let description: string | undefined = undefined; + if (this.vocabulary) { + const entry = this.vocabulary.byCanonical.get(canonical); + if (entry) { + description = entry.description; + } + } + + // Display: canonical type (primary) const chip = chipsContainer.createEl("button", { cls: "inline-edge-type-modal__chip", - text: edgeType, + text: canonical, }); + + // Add description as tooltip/hover text (even for disabled chips) + if (description) { + chip.title = description; + chip.setAttribute("data-description", description); + } + chip.disabled = true; chip.addClass("mod-muted"); } @@ -183,10 +273,10 @@ export class InlineEdgeTypeModal extends Modal { ); const choice: EdgeTypeChoice | null = await chooser.show(); if (choice) { - // Store the choice and close modal + // Store the choice and re-render to show selection this.selectEdgeType(choice.edgeType, choice.alias); - // Close this modal immediately after selection - this.close(); + // Re-render the modal to show the selected type + this.onOpen(); } }; } @@ -201,8 +291,18 @@ export class InlineEdgeTypeModal extends Modal { cls: "mod-cta", }); okBtn.onclick = () => { - // If no type selected yet, use preselected + // If no type selected yet, use preselected canonical if (!this.result && this.selectedEdgeType) { + // Check if aliases exist - if so, show alias chooser first + if (this.vocabulary) { + const entry = this.vocabulary.byCanonical.get(this.selectedEdgeType); + if (entry && entry.aliases.length > 0) { + // Show alias chooser + this.showAliasChooser(this.selectedEdgeType, entry.aliases); + return; // Don't close yet, wait for alias selection + } + } + // No aliases, use canonical directly this.result = { chosenRawType: this.selectedEdgeType, cancelled: false }; } else if (!this.result) { // No selection possible, treat as skip @@ -249,27 +349,67 @@ export class InlineEdgeTypeModal extends Modal { } private selectEdgeType(edgeType: string, alias?: string | null): void { - // Remove previous selection - const chips = this.contentEl.querySelectorAll(".inline-edge-type-modal__chip"); - const chipsArray = Array.from(chips); - for (const chip of chipsArray) { - if (chip instanceof HTMLElement) { - chip.removeClass("mod-cta"); - } - } - - // Mark selected - const selectedChip = chipsArray.find( - (chip) => chip.textContent === edgeType || chip.textContent?.includes(edgeType) - ); - if (selectedChip && selectedChip instanceof HTMLElement) { - selectedChip.addClass("mod-cta"); - } - - // Store result + // Store result - use alias if provided, otherwise use edgeType (canonical) this.selectedEdgeType = edgeType; this.selectedAlias = alias || null; - this.result = { chosenRawType: edgeType, cancelled: false }; + // Use alias if available, otherwise use canonical + const rawType = alias || edgeType; + this.result = { chosenRawType: rawType, cancelled: false }; + + // Note: Visual highlighting will be done in onOpen() when re-rendering + } + + private showAliasChooser(canonical: string, aliases: string[]): void { + const { contentEl } = this; + + // Store original content structure for restoration + const originalSections = Array.from(contentEl.children); + + // Clear and show alias chooser + contentEl.empty(); + contentEl.createEl("h2", { text: "Choose alias" }); + contentEl.createEl("p", { text: `Aliases available for ${canonical}` }); + + const container = contentEl.createEl("div", { cls: "alias-list" }); + container.style.display = "flex"; + container.style.flexDirection = "column"; + container.style.gap = "0.5em"; + container.style.marginTop = "1em"; + + // Option: Use canonical (no alias) + const canonicalBtn = container.createEl("button", { + text: `${canonical} (canonical)`, + cls: "mod-cta", + }); + canonicalBtn.style.width = "100%"; + canonicalBtn.style.padding = "0.75em"; + canonicalBtn.onclick = () => { + this.selectEdgeType(canonical); + // Restore original content by re-rendering + this.onOpen(); + }; + + // All aliases + for (const alias of aliases) { + const btn = container.createEl("button", { + text: alias, + }); + btn.style.width = "100%"; + btn.style.padding = "0.75em"; + btn.onclick = () => { + this.selectEdgeType(canonical, alias); + // Restore original content by re-rendering + this.onOpen(); + }; + } + + const backBtn = container.createEl("button", { text: "← Back" }); + backBtn.style.width = "100%"; + backBtn.style.marginTop = "1em"; + backBtn.onclick = () => { + // Restore original content by re-rendering + this.onOpen(); + }; } private async openFullChooser(): Promise { @@ -290,8 +430,10 @@ export class InlineEdgeTypeModal extends Modal { const choice: EdgeTypeChoice | null = await chooser.show(); if (choice) { + // Store the choice and re-render to show selection this.selectEdgeType(choice.edgeType, choice.alias); - this.close(); + // Re-render the modal to show the selected type + this.onOpen(); } else { // User cancelled chooser, treat as skip this.result = { chosenRawType: null, cancelled: false }; diff --git a/src/ui/LinkPromptModal.ts b/src/ui/LinkPromptModal.ts index 6325d2f..87cb9f5 100644 --- a/src/ui/LinkPromptModal.ts +++ b/src/ui/LinkPromptModal.ts @@ -10,8 +10,8 @@ import { EdgeTypeChooserModal, type EdgeTypeChoice } from "./EdgeTypeChooserModa import { computeEdgeSuggestions } from "../mapping/schemaHelper"; export type LinkPromptDecision = - | { action: "keep"; edgeType: string } - | { action: "change"; edgeType: string; alias?: string } + | { action: "keep"; edgeType: string } // edgeType is the actual type (alias or canonical) to keep + | { action: "change"; edgeType: string; alias?: string } // edgeType is canonical, alias is the selected alias (if any) | { action: "skip" }; export class LinkPromptModal extends Modal { diff --git a/src/vocab/parseEdgeVocabulary.ts b/src/vocab/parseEdgeVocabulary.ts index c13232a..1c5ac84 100644 --- a/src/vocab/parseEdgeVocabulary.ts +++ b/src/vocab/parseEdgeVocabulary.ts @@ -19,12 +19,20 @@ const BACKTICK_RE = /`([^`]+)`/g; */ export function parseEdgeVocabulary(md: string): EdgeVocabulary { const lines = md.split(/\r?\n/); - const byCanonical = new Map(); + const byCanonical = new Map(); const aliasToCanonical = new Map(); let skippedRows = 0; + let currentCategory: string | null = null; // Track current H3 category for (const line of lines) { + // Detect H3 headings (###) as category separators + const h3Match = line.match(/^###\s+(.+)$/); + if (h3Match && h3Match[1]) { + currentCategory = h3Match[1].trim(); + continue; + } + // Skip header separator rows (e.g., "| :--- | :--- |") if (/^\s*\|[\s:|-]+\|\s*$/.test(line)) { continue; @@ -35,6 +43,12 @@ export function parseEdgeVocabulary(md: string): EdgeVocabulary { continue; } + // Skip header rows (contains "Canonical", "System-Typ", "Beschreibung", "Kategorie", etc.) + // Check for common header keywords + if (/canonical|system-typ|beschreibung|kategorie|category|description|inverser|aliasse/i.test(line)) { + continue; + } + // Extract all backticked tokens const tokens: string[] = []; let match: RegExpExecArray | null; @@ -54,6 +68,49 @@ export function parseEdgeVocabulary(md: string): EdgeVocabulary { continue; } + // Parse table cells (split by |, skip first and last empty cells) + const cells = line.split("|").map(c => c.trim()).filter(c => c); + + // Extract description and category from cells + // Expected order: Canonical | Inverse | Aliases | Description | Category (optional) + let description: string | undefined = undefined; + let category: string | undefined = undefined; + + // Try to extract from cells after aliases (index 3+) + // Description is usually the first text cell after aliases + // Category might be in brackets, short, or in a separate column + for (let i = 3; i < cells.length; i++) { + const cell = cells[i]; + if (!cell || !cell.trim()) continue; + + const trimmed = cell.trim(); + + // Check if this looks like a category: + // - Short text (< 40 chars) + // - Might be in brackets [Category] + // - Might be all caps + // - Might match category pattern + const looksLikeCategory = + trimmed.length < 40 && ( + /^\[.+\]$/.test(trimmed) || // [Category] + trimmed === trimmed.toUpperCase() || // ALL CAPS + /^[A-ZÄÖÜ][a-zäöüß]+(\s+[A-ZÄÖÜ][a-zäöüß]+)*$/.test(trimmed) // Title Case + ); + + if (looksLikeCategory && !category) { + // Remove brackets if present + category = trimmed.replace(/^\[|\]$/g, ""); + } else if (!description && trimmed.length > 0) { + // First substantial cell is likely description + // Remove markdown formatting but keep content + description = trimmed + .replace(/\*\*/g, "") // Remove bold + .replace(/\*/g, "") // Remove italic + .replace(/`/g, "") // Remove code + .trim(); + } + } + // Check if aliases cell contains "(Kein Alias)" const hasNoAliases = /\(Kein Alias\)/i.test(line); @@ -76,11 +133,16 @@ export function parseEdgeVocabulary(md: string): EdgeVocabulary { } } - // Store canonical entry + // Store canonical entry with description and category + // Use currentCategory from H3 heading if available, otherwise use extracted category + const finalCategory = currentCategory || category; + byCanonical.set(canonical, { canonical, inverse, aliases, + description, + category: finalCategory, }); // Build alias-to-canonical mapping (case-insensitive keys) @@ -95,7 +157,9 @@ export function parseEdgeVocabulary(md: string): EdgeVocabulary { } if (skippedRows > 0) { - console.warn(`parseEdgeVocabulary: Skipped ${skippedRows} rows with insufficient tokens`); + // Only warn if there are actually problematic rows (not just header/separator rows) + // Header and separator rows are expected and should not trigger warnings + console.debug(`parseEdgeVocabulary: Skipped ${skippedRows} data rows with insufficient tokens (this is normal if the file contains empty or malformed table rows)`); } return { diff --git a/src/vocab/types.ts b/src/vocab/types.ts index 80fff3d..3e4380d 100644 --- a/src/vocab/types.ts +++ b/src/vocab/types.ts @@ -4,6 +4,8 @@ export interface EdgeTypeEntry { canonical: CanonicalEdgeType; inverse?: CanonicalEdgeType; aliases: string[]; + description?: string; // Description from vocabulary table + category?: string; // Category/group from vocabulary table } export interface EdgeVocabulary {