@astermind/cybernetic-chatbot-client 2.2.14 → 2.2.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,4 @@
1
+ import { useState, useRef, useCallback, useEffect } from 'react';
1
2
  import 'fs';
2
3
  import 'path';
3
4
 
@@ -2660,6 +2661,555 @@ const configLoaders = {
2660
2661
  loadFromScriptAttributes
2661
2662
  };
2662
2663
 
2664
+ // src/agentic/SiteMapDiscovery.ts
2665
+ // Multi-source sitemap discovery and merging for agentic navigation
2666
+ /**
2667
+ * Multi-source sitemap discovery and management
2668
+ *
2669
+ * Combines routes from three sources:
2670
+ * 1. Static props (explicit configuration)
2671
+ * 2. Framework auto-discovery (React Router, Vue Router, etc.)
2672
+ * 3. Backend API (tenant-specific sitemaps)
2673
+ */
2674
+ class SiteMapDiscovery {
2675
+ constructor(config, apiUrl, apiKey) {
2676
+ this.staticEntries = [];
2677
+ this.discoveredEntries = [];
2678
+ this.backendEntries = [];
2679
+ this.backendCache = null;
2680
+ this.isInitialized = false;
2681
+ this.initPromise = null;
2682
+ this.config = config;
2683
+ this.apiUrl = apiUrl;
2684
+ this.apiKey = apiKey;
2685
+ // Load static entries immediately (synchronous)
2686
+ if (config.static) {
2687
+ this.staticEntries = this.enhanceEntries(config.static, 'props');
2688
+ }
2689
+ }
2690
+ /**
2691
+ * Initialize all sitemap sources
2692
+ * Call this during widget initialization or first query based on loadStrategy
2693
+ */
2694
+ async initialize() {
2695
+ if (this.isInitialized)
2696
+ return;
2697
+ if (this.initPromise)
2698
+ return this.initPromise;
2699
+ this.initPromise = this.performInitialization();
2700
+ await this.initPromise;
2701
+ this.isInitialized = true;
2702
+ }
2703
+ async performInitialization() {
2704
+ const tasks = [];
2705
+ // Source 2: Framework auto-discovery
2706
+ if (this.config.discovery?.enabled !== false) {
2707
+ tasks.push(this.discoverFromFramework());
2708
+ }
2709
+ // Source 3: Backend API
2710
+ if (this.config.backend?.enabled !== false && this.config.backend?.endpoint) {
2711
+ tasks.push(this.fetchFromBackend());
2712
+ }
2713
+ const results = await Promise.allSettled(tasks);
2714
+ for (const result of results) {
2715
+ if (result.status === 'fulfilled') {
2716
+ const { entries, source } = result.value;
2717
+ if (source === 'discovery') {
2718
+ this.discoveredEntries = entries;
2719
+ }
2720
+ else if (source === 'backend') {
2721
+ this.backendEntries = entries;
2722
+ }
2723
+ }
2724
+ else {
2725
+ console.warn('[SiteMapDiscovery] Source failed:', result.reason);
2726
+ }
2727
+ }
2728
+ const total = this.staticEntries.length + this.discoveredEntries.length + this.backendEntries.length;
2729
+ console.log(`[SiteMapDiscovery] Initialized with ${total} total entries (${this.staticEntries.length} static, ${this.discoveredEntries.length} discovered, ${this.backendEntries.length} backend)`);
2730
+ }
2731
+ /**
2732
+ * Get merged sitemap entries with deduplication
2733
+ */
2734
+ getMergedEntries() {
2735
+ return this.mergeEntries(this.staticEntries, this.discoveredEntries, this.backendEntries);
2736
+ }
2737
+ /**
2738
+ * Convert to legacy AgenticSiteMapEntry format for classifier compatibility
2739
+ */
2740
+ getLegacyEntries() {
2741
+ return this.getMergedEntries().map(e => ({
2742
+ path: e.path,
2743
+ name: e.name,
2744
+ description: e.description,
2745
+ params: e.params,
2746
+ dynamicParams: e.dynamicParams,
2747
+ aliases: e.aliases
2748
+ }));
2749
+ }
2750
+ // ==================== SOURCE 2: FRAMEWORK DISCOVERY ====================
2751
+ async discoverFromFramework() {
2752
+ const config = this.config.discovery || {};
2753
+ const entries = [];
2754
+ try {
2755
+ // Detect framework if not specified
2756
+ const framework = config.framework || this.detectFramework();
2757
+ let routes = [];
2758
+ switch (framework) {
2759
+ case 'react-router':
2760
+ routes = this.discoverReactRouter();
2761
+ break;
2762
+ case 'vue-router':
2763
+ routes = this.discoverVueRouter();
2764
+ break;
2765
+ case 'next':
2766
+ routes = this.discoverNextRoutes();
2767
+ break;
2768
+ case 'angular':
2769
+ routes = this.discoverAngularRoutes();
2770
+ break;
2771
+ default:
2772
+ if (config.domFallback !== false) {
2773
+ routes = this.discoverFromDOM(config.navSelectors);
2774
+ }
2775
+ }
2776
+ // Transform routes to sitemap entries
2777
+ for (const route of routes) {
2778
+ // Apply exclude patterns
2779
+ if (this.shouldExcludeRoute(route.path, config.excludePaths)) {
2780
+ continue;
2781
+ }
2782
+ // Apply custom transform if provided
2783
+ let entry;
2784
+ if (config.transformRoute) {
2785
+ entry = config.transformRoute(route);
2786
+ }
2787
+ else {
2788
+ entry = this.defaultRouteTransform(route);
2789
+ }
2790
+ if (entry) {
2791
+ entries.push(this.enhanceEntry(entry, 'discovery'));
2792
+ }
2793
+ }
2794
+ console.log(`[SiteMapDiscovery] Discovered ${entries.length} routes from ${framework}`);
2795
+ }
2796
+ catch (error) {
2797
+ console.warn('[SiteMapDiscovery] Framework discovery failed:', error);
2798
+ return {
2799
+ entries: [],
2800
+ source: 'discovery',
2801
+ timestamp: Date.now(),
2802
+ error: String(error)
2803
+ };
2804
+ }
2805
+ return {
2806
+ entries,
2807
+ source: 'discovery',
2808
+ timestamp: Date.now()
2809
+ };
2810
+ }
2811
+ /**
2812
+ * Detect JavaScript framework from global objects
2813
+ */
2814
+ detectFramework() {
2815
+ if (typeof window === 'undefined')
2816
+ return 'generic';
2817
+ const win = window;
2818
+ // React Router v6.4+ data router
2819
+ if (win.__REACT_ROUTER_DATA_ROUTER__) {
2820
+ return 'react-router';
2821
+ }
2822
+ // React Router v5 context
2823
+ if (win.__reactRouterContext) {
2824
+ return 'react-router';
2825
+ }
2826
+ // Vue Router
2827
+ if (win.__VUE_ROUTER__ || win.__VUE_APP__?.$router) {
2828
+ return 'vue-router';
2829
+ }
2830
+ // Next.js
2831
+ if (win.__NEXT_DATA__) {
2832
+ return 'next';
2833
+ }
2834
+ // Angular
2835
+ if (win.ng || document.querySelector('[ng-version]')) {
2836
+ return 'angular';
2837
+ }
2838
+ return 'generic';
2839
+ }
2840
+ /**
2841
+ * Discover routes from React Router
2842
+ */
2843
+ discoverReactRouter() {
2844
+ const routes = [];
2845
+ if (typeof window === 'undefined')
2846
+ return routes;
2847
+ const win = window;
2848
+ // Try React Router v6.4+ data router
2849
+ const dataRouter = win.__REACT_ROUTER_DATA_ROUTER__;
2850
+ if (dataRouter?.routes) {
2851
+ this.extractRoutesFromConfig(dataRouter.routes, routes, '');
2852
+ return routes;
2853
+ }
2854
+ // Fallback: Try to find routes from __REACT_DEVTOOLS_GLOBAL_HOOK__
2855
+ // This is less reliable but can work in some cases
2856
+ return routes;
2857
+ }
2858
+ /**
2859
+ * Extract routes recursively from React Router config
2860
+ */
2861
+ extractRoutesFromConfig(routeConfigs, routes, parentPath) {
2862
+ for (const config of routeConfigs) {
2863
+ const path = this.normalizePath(parentPath, config.path || '');
2864
+ // Extract route info from handle metadata
2865
+ const route = {
2866
+ path,
2867
+ name: config.handle?.name || config.handle?.title || config.id,
2868
+ meta: config.handle || {},
2869
+ children: []
2870
+ };
2871
+ // Only include routes with paths (skip layout routes without path)
2872
+ if (config.path || config.index) {
2873
+ routes.push(route);
2874
+ }
2875
+ // Recursively process children
2876
+ if (config.children) {
2877
+ this.extractRoutesFromConfig(config.children, routes, path);
2878
+ }
2879
+ }
2880
+ }
2881
+ /**
2882
+ * Discover routes from Vue Router
2883
+ */
2884
+ discoverVueRouter() {
2885
+ const routes = [];
2886
+ if (typeof window === 'undefined')
2887
+ return routes;
2888
+ const win = window;
2889
+ // Access Vue Router instance
2890
+ const app = win.__VUE_APP__;
2891
+ const router = app?.$router || win.__VUE_ROUTER__;
2892
+ if (router?.getRoutes) {
2893
+ const vueRoutes = router.getRoutes();
2894
+ for (const vueRoute of vueRoutes) {
2895
+ routes.push({
2896
+ path: vueRoute.path,
2897
+ name: vueRoute.name?.toString(),
2898
+ meta: vueRoute.meta || {},
2899
+ component: vueRoute.components?.default?.name
2900
+ });
2901
+ }
2902
+ }
2903
+ return routes;
2904
+ }
2905
+ /**
2906
+ * Discover routes from Next.js
2907
+ */
2908
+ discoverNextRoutes() {
2909
+ const routes = [];
2910
+ if (typeof window === 'undefined')
2911
+ return routes;
2912
+ const win = window;
2913
+ const nextData = win.__NEXT_DATA__;
2914
+ if (!nextData)
2915
+ return routes;
2916
+ // Next.js doesn't expose routes directly in client
2917
+ // We can extract from build manifest if available
2918
+ const pages = nextData.buildManifest?.pages || {};
2919
+ for (const pagePath of Object.keys(pages)) {
2920
+ if (pagePath.startsWith('/_'))
2921
+ continue; // Skip internal pages
2922
+ routes.push({
2923
+ path: pagePath,
2924
+ name: this.pathToName(pagePath)
2925
+ });
2926
+ }
2927
+ return routes;
2928
+ }
2929
+ /**
2930
+ * Discover routes from Angular Router
2931
+ */
2932
+ discoverAngularRoutes() {
2933
+ // Angular route discovery requires access to Router service
2934
+ // which is typically not exposed globally
2935
+ // Users should provide routes via props or backend
2936
+ return [];
2937
+ }
2938
+ /**
2939
+ * Discover routes by scanning DOM navigation elements
2940
+ */
2941
+ discoverFromDOM(selectors) {
2942
+ const routes = [];
2943
+ if (typeof document === 'undefined')
2944
+ return routes;
2945
+ const defaultSelectors = [
2946
+ 'nav a[href]',
2947
+ '[role="navigation"] a[href]',
2948
+ '.nav a[href]',
2949
+ '.navigation a[href]',
2950
+ '.sidebar a[href]',
2951
+ '.menu a[href]',
2952
+ 'header a[href]'
2953
+ ];
2954
+ const allSelectors = selectors || defaultSelectors;
2955
+ const seen = new Set();
2956
+ for (const selector of allSelectors) {
2957
+ try {
2958
+ const links = document.querySelectorAll(selector);
2959
+ for (const link of links) {
2960
+ const href = link.getAttribute('href');
2961
+ if (!href)
2962
+ continue;
2963
+ // Only process internal links
2964
+ if (href.startsWith('http') && !href.startsWith(window.location.origin)) {
2965
+ continue;
2966
+ }
2967
+ // Normalize path
2968
+ const path = href.startsWith('/') ? href : `/${href}`;
2969
+ // Skip duplicates, anchors, and special links
2970
+ if (seen.has(path) || path.startsWith('/#') || path === '/' || path.includes('javascript:')) {
2971
+ continue;
2972
+ }
2973
+ seen.add(path);
2974
+ // Clean path (remove query string and hash)
2975
+ const cleanPath = path.split('?')[0].split('#')[0];
2976
+ routes.push({
2977
+ path: cleanPath,
2978
+ name: link.textContent?.trim() || this.pathToName(cleanPath)
2979
+ });
2980
+ }
2981
+ }
2982
+ catch (e) {
2983
+ // Invalid selector - skip
2984
+ }
2985
+ }
2986
+ return routes;
2987
+ }
2988
+ // ==================== SOURCE 3: BACKEND API ====================
2989
+ async fetchFromBackend() {
2990
+ const config = this.config.backend || {};
2991
+ const endpoint = config.endpoint || '/api/external/sitemap';
2992
+ // Check cache
2993
+ if (this.backendCache && Date.now() < this.backendCache.expiresAt) {
2994
+ return {
2995
+ entries: this.backendCache.entries,
2996
+ source: 'backend',
2997
+ timestamp: this.backendCache.fetchedAt
2998
+ };
2999
+ }
3000
+ try {
3001
+ const url = new URL(endpoint, this.apiUrl);
3002
+ // Add query params
3003
+ if (config.tenantScoped !== false) {
3004
+ url.searchParams.set('tenantScoped', 'true');
3005
+ }
3006
+ if (config.includeGeneral !== false) {
3007
+ url.searchParams.set('includeGeneral', 'true');
3008
+ }
3009
+ const response = await fetch(url.toString(), {
3010
+ method: 'GET',
3011
+ headers: {
3012
+ 'X-API-Key': this.apiKey,
3013
+ 'Content-Type': 'application/json',
3014
+ ...config.headers
3015
+ }
3016
+ });
3017
+ if (!response.ok) {
3018
+ throw new Error(`Backend sitemap fetch failed: ${response.status}`);
3019
+ }
3020
+ const data = await response.json();
3021
+ const entries = this.transformBackendResponse(data);
3022
+ // Update cache
3023
+ const ttl = config.cacheTtl || 300000; // 5 minutes default
3024
+ this.backendCache = {
3025
+ entries,
3026
+ fetchedAt: Date.now(),
3027
+ expiresAt: Date.now() + ttl
3028
+ };
3029
+ console.log(`[SiteMapDiscovery] Loaded ${entries.length} routes from backend`);
3030
+ return {
3031
+ entries,
3032
+ source: 'backend',
3033
+ timestamp: Date.now()
3034
+ };
3035
+ }
3036
+ catch (error) {
3037
+ console.warn('[SiteMapDiscovery] Backend fetch failed:', error);
3038
+ return {
3039
+ entries: this.backendCache?.entries || [],
3040
+ source: 'backend',
3041
+ timestamp: Date.now(),
3042
+ error: String(error)
3043
+ };
3044
+ }
3045
+ }
3046
+ /**
3047
+ * Transform backend response to enhanced entries
3048
+ */
3049
+ transformBackendResponse(data) {
3050
+ // Handle array or object with entries/sitemap key
3051
+ const rawEntries = Array.isArray(data)
3052
+ ? data
3053
+ : data.entries || data.sitemap || data.routes || [];
3054
+ return rawEntries.map((entry) => this.enhanceEntry({
3055
+ path: entry.url || entry.path,
3056
+ name: entry.title || entry.name,
3057
+ description: entry.description,
3058
+ aliases: entry.keywords || entry.aliases,
3059
+ params: entry.params,
3060
+ dynamicParams: entry.dynamicParams
3061
+ }, 'backend'));
3062
+ }
3063
+ // ==================== MERGE LOGIC ====================
3064
+ /**
3065
+ * Merge entries from all sources with deduplication
3066
+ */
3067
+ mergeEntries(...sources) {
3068
+ const mergeConfig = this.config.merge || {};
3069
+ const priorityOrder = mergeConfig.sourcePriority || ['props', 'backend', 'discovery'];
3070
+ const dedupeStrategy = mergeConfig.deduplication || 'path';
3071
+ // Create priority map (higher number = higher priority)
3072
+ const priorityMap = new Map();
3073
+ priorityOrder.forEach((source, index) => {
3074
+ priorityMap.set(source, priorityOrder.length - index);
3075
+ });
3076
+ // Flatten all entries with priority scores
3077
+ const allEntries = [];
3078
+ for (const sourceEntries of sources) {
3079
+ for (const entry of sourceEntries) {
3080
+ const priority = priorityMap.get(entry._source || 'props') || 0;
3081
+ allEntries.push({ ...entry, _priority: priority });
3082
+ }
3083
+ }
3084
+ // Sort by priority (higher first)
3085
+ allEntries.sort((a, b) => (b._priority || 0) - (a._priority || 0));
3086
+ // Deduplicate
3087
+ const seen = new Map();
3088
+ for (const entry of allEntries) {
3089
+ let key;
3090
+ switch (dedupeStrategy) {
3091
+ case 'name':
3092
+ key = entry.name.toLowerCase();
3093
+ break;
3094
+ case 'both':
3095
+ key = `${entry.path}|${entry.name.toLowerCase()}`;
3096
+ break;
3097
+ case 'path':
3098
+ default:
3099
+ key = this.normalizePath('', entry.path);
3100
+ }
3101
+ if (!seen.has(key)) {
3102
+ seen.set(key, entry);
3103
+ }
3104
+ else if (mergeConfig.keepNonConflicting !== false) {
3105
+ // Merge non-conflicting fields from lower priority entry
3106
+ const existing = seen.get(key);
3107
+ if (!existing.description && entry.description) {
3108
+ existing.description = entry.description;
3109
+ }
3110
+ if (!existing.aliases?.length && entry.aliases?.length) {
3111
+ existing.aliases = entry.aliases;
3112
+ }
3113
+ }
3114
+ }
3115
+ return Array.from(seen.values());
3116
+ }
3117
+ // ==================== HELPERS ====================
3118
+ enhanceEntries(entries, source) {
3119
+ return entries.map(e => this.enhanceEntry(e, source));
3120
+ }
3121
+ enhanceEntry(entry, source) {
3122
+ return {
3123
+ ...entry,
3124
+ _source: source,
3125
+ _discoveredAt: Date.now()
3126
+ };
3127
+ }
3128
+ /**
3129
+ * Default transformation from DiscoveredRoute to AgenticSiteMapEntry
3130
+ */
3131
+ defaultRouteTransform(route) {
3132
+ // Skip routes without meaningful paths
3133
+ if (!route.path || route.path === '*' || route.path === '**') {
3134
+ return null;
3135
+ }
3136
+ return {
3137
+ path: route.path,
3138
+ name: route.name || route.meta?.title || this.pathToName(route.path),
3139
+ description: route.meta?.description,
3140
+ aliases: route.meta?.keywords || route.meta?.aliases,
3141
+ dynamicParams: route.path.includes(':') || route.path.includes('[')
3142
+ };
3143
+ }
3144
+ /**
3145
+ * Normalize path by combining parent and child paths
3146
+ */
3147
+ normalizePath(parent, path) {
3148
+ if (!path || path === '/')
3149
+ return parent || '/';
3150
+ if (path.startsWith('/'))
3151
+ return path;
3152
+ if (!parent || parent === '/')
3153
+ return `/${path}`;
3154
+ return `${parent}/${path}`.replace(/\/+/g, '/');
3155
+ }
3156
+ /**
3157
+ * Convert path to human-readable name
3158
+ */
3159
+ pathToName(path) {
3160
+ const segments = path.split('/').filter(Boolean);
3161
+ const last = segments[segments.length - 1] || 'Home';
3162
+ return last
3163
+ .replace(/[-_]/g, ' ')
3164
+ .replace(/\[.*?\]/g, '') // Remove Next.js dynamic segments
3165
+ .replace(/:/g, '') // Remove React Router params
3166
+ .split(' ')
3167
+ .map(w => w.charAt(0).toUpperCase() + w.slice(1))
3168
+ .join(' ')
3169
+ .trim() || 'Page';
3170
+ }
3171
+ /**
3172
+ * Check if route should be excluded based on glob patterns
3173
+ */
3174
+ shouldExcludeRoute(path, patterns) {
3175
+ if (!patterns?.length)
3176
+ return false;
3177
+ for (const pattern of patterns) {
3178
+ // Simple glob matching (supports * wildcard)
3179
+ const regex = new RegExp('^' + pattern.replace(/\*/g, '.*').replace(/\?/g, '.') + '$');
3180
+ if (regex.test(path))
3181
+ return true;
3182
+ }
3183
+ return false;
3184
+ }
3185
+ /**
3186
+ * Force refresh from all sources
3187
+ */
3188
+ async refresh() {
3189
+ this.backendCache = null;
3190
+ this.isInitialized = false;
3191
+ this.initPromise = null;
3192
+ await this.initialize();
3193
+ }
3194
+ /**
3195
+ * Check if initialized
3196
+ */
3197
+ isReady() {
3198
+ return this.isInitialized;
3199
+ }
3200
+ /**
3201
+ * Get current entry counts by source
3202
+ */
3203
+ getStats() {
3204
+ return {
3205
+ static: this.staticEntries.length,
3206
+ discovered: this.discoveredEntries.length,
3207
+ backend: this.backendEntries.length,
3208
+ merged: this.getMergedEntries().length
3209
+ };
3210
+ }
3211
+ }
3212
+
2663
3213
  // src/agentic/CyberneticIntentClassifier.ts
2664
3214
  // Hybrid intent classification for agentic capabilities
2665
3215
  /**
@@ -2702,15 +3252,28 @@ const ACTION_PATTERNS = {
2702
3252
  * a local DOM action or be sent to the backend RAG.
2703
3253
  */
2704
3254
  class CyberneticIntentClassifier {
2705
- constructor(config) {
3255
+ /**
3256
+ * Create a new intent classifier
3257
+ *
3258
+ * @param config - Agent configuration
3259
+ * @param apiUrl - Backend API URL (for multi-source sitemap)
3260
+ * @param apiKey - API key for authentication
3261
+ */
3262
+ constructor(config, apiUrl = '', apiKey = '') {
2706
3263
  this.categories = [];
2707
3264
  this.topicKeywords = new Map(); // topic -> keywords
3265
+ /** Multi-source sitemap discovery instance */
3266
+ this.siteMapDiscovery = null;
2708
3267
  this.config = config;
2709
3268
  this.siteMapIndex = new Map();
2710
3269
  this.formIndex = new Map();
2711
3270
  this.modalIndex = new Map();
2712
3271
  // Build indexes for fast lookup
2713
3272
  this.buildIndexes();
3273
+ // Initialize multi-source sitemap discovery if configured
3274
+ if (config.siteMapConfig) {
3275
+ this.siteMapDiscovery = new SiteMapDiscovery(config.siteMapConfig, apiUrl, apiKey);
3276
+ }
2714
3277
  }
2715
3278
  /**
2716
3279
  * Build search indexes from configuration
@@ -2981,22 +3544,35 @@ class CyberneticIntentClassifier {
2981
3544
  }
2982
3545
  return '';
2983
3546
  }
3547
+ /**
3548
+ * Strip common articles and filler words for better matching
3549
+ */
3550
+ stripArticles(text) {
3551
+ return text
3552
+ .replace(/^(the|a|an|my|our|your|this|that)\s+/gi, '')
3553
+ .replace(/\s+(page|section|screen|view)$/gi, '')
3554
+ .trim();
3555
+ }
2984
3556
  /**
2985
3557
  * Find site map match for target string
2986
3558
  */
2987
3559
  findSiteMapMatch(target) {
2988
3560
  const normalizedTarget = target.toLowerCase().replace(/[^a-z0-9\s]/g, '');
2989
- // Exact match
2990
- const exact = this.siteMapIndex.get(normalizedTarget);
3561
+ const strippedTarget = this.stripArticles(normalizedTarget);
3562
+ // Exact match (try both with and without articles)
3563
+ const exact = this.siteMapIndex.get(normalizedTarget) || this.siteMapIndex.get(strippedTarget);
2991
3564
  if (exact) {
2992
3565
  return { entry: exact, similarity: 1.0 };
2993
3566
  }
2994
- // Fuzzy match
3567
+ // Fuzzy match using stripped version for better accuracy
2995
3568
  let bestMatch = null;
2996
3569
  let bestScore = 0;
2997
3570
  for (const [key, entry] of this.siteMapIndex) {
2998
- const score = this.calculateSimilarity(normalizedTarget, key);
2999
- if (score > bestScore && score > 0.6) {
3571
+ // Compare both original and stripped versions, take the better score
3572
+ const score1 = this.calculateSimilarity(normalizedTarget, key);
3573
+ const score2 = this.calculateSimilarity(strippedTarget, key);
3574
+ const score = Math.max(score1, score2);
3575
+ if (score > bestScore && score > 0.5) {
3000
3576
  bestScore = score;
3001
3577
  bestMatch = entry;
3002
3578
  }
@@ -3393,6 +3969,124 @@ class CyberneticIntentClassifier {
3393
3969
  isTrained() {
3394
3970
  return this.categories.length > 0 || this.topicKeywords.size > 0;
3395
3971
  }
3972
+ // ==================== MULTI-SOURCE SITEMAP DISCOVERY ====================
3973
+ /**
3974
+ * Initialize multi-source sitemap discovery
3975
+ * Call this after construction to load all sitemap sources
3976
+ *
3977
+ * @returns Promise that resolves when discovery is complete
3978
+ */
3979
+ async initializeDiscovery() {
3980
+ if (!this.siteMapDiscovery) {
3981
+ console.log('[CyberneticIntentClassifier] No siteMapConfig provided, skipping discovery');
3982
+ return;
3983
+ }
3984
+ try {
3985
+ await this.siteMapDiscovery.initialize();
3986
+ this.rebuildIndexesFromDiscovery();
3987
+ console.log(`[CyberneticIntentClassifier] Discovery complete, indexed ${this.siteMapIndex.size} entries`);
3988
+ }
3989
+ catch (error) {
3990
+ console.error('[CyberneticIntentClassifier] Discovery initialization failed:', error);
3991
+ }
3992
+ }
3993
+ /**
3994
+ * Rebuild sitemap indexes from discovery results
3995
+ * Merges discovered entries with existing static config
3996
+ */
3997
+ rebuildIndexesFromDiscovery() {
3998
+ if (!this.siteMapDiscovery)
3999
+ return;
4000
+ // Get merged entries from discovery (includes static props)
4001
+ const mergedEntries = this.siteMapDiscovery.getLegacyEntries();
4002
+ // Clear existing sitemap index (keep form and modal indexes)
4003
+ this.siteMapIndex.clear();
4004
+ // Index all merged entries
4005
+ for (const entry of mergedEntries) {
4006
+ const siteMapEntry = {
4007
+ path: entry.path,
4008
+ name: entry.name,
4009
+ description: entry.description,
4010
+ params: entry.params,
4011
+ aliases: entry.aliases
4012
+ };
4013
+ // Index by name and aliases
4014
+ const keys = [
4015
+ entry.name.toLowerCase(),
4016
+ entry.path.toLowerCase(),
4017
+ ...(entry.aliases || []).map(a => a.toLowerCase())
4018
+ ];
4019
+ // Also index description words for fuzzy matching
4020
+ if (entry.description) {
4021
+ const descWords = entry.description
4022
+ .toLowerCase()
4023
+ .split(/\s+/)
4024
+ .filter(w => w.length > 4 && !this.isStopWord(w));
4025
+ keys.push(...descWords);
4026
+ }
4027
+ for (const key of keys) {
4028
+ this.siteMapIndex.set(key, siteMapEntry);
4029
+ }
4030
+ }
4031
+ }
4032
+ /**
4033
+ * Check if sitemap discovery is ready
4034
+ *
4035
+ * @returns true if discovery is complete and entries are loaded
4036
+ */
4037
+ isSiteMapReady() {
4038
+ if (!this.siteMapDiscovery) {
4039
+ // No discovery configured - check if static sitemap exists
4040
+ return this.siteMapIndex.size > 0 || (this.config.siteMap?.length ?? 0) > 0;
4041
+ }
4042
+ return this.siteMapDiscovery.isReady();
4043
+ }
4044
+ /**
4045
+ * Ensure sitemap is ready before classification
4046
+ * Initializes discovery if not already done
4047
+ *
4048
+ * @returns Promise that resolves when sitemap is ready
4049
+ */
4050
+ async ensureSiteMapReady() {
4051
+ if (this.isSiteMapReady()) {
4052
+ return;
4053
+ }
4054
+ await this.initializeDiscovery();
4055
+ }
4056
+ /**
4057
+ * Refresh sitemap from all sources
4058
+ * Forces reload of backend and re-discovery
4059
+ *
4060
+ * @returns Promise that resolves when refresh is complete
4061
+ */
4062
+ async refreshSiteMap() {
4063
+ if (!this.siteMapDiscovery) {
4064
+ console.log('[CyberneticIntentClassifier] No siteMapConfig provided, cannot refresh');
4065
+ return;
4066
+ }
4067
+ await this.siteMapDiscovery.refresh();
4068
+ this.rebuildIndexesFromDiscovery();
4069
+ }
4070
+ /**
4071
+ * Get the SiteMapDiscovery instance for advanced use cases
4072
+ *
4073
+ * @returns The SiteMapDiscovery instance or null if not configured
4074
+ */
4075
+ getSiteMapDiscovery() {
4076
+ return this.siteMapDiscovery;
4077
+ }
4078
+ /**
4079
+ * Get current sitemap entries (merged from all sources)
4080
+ *
4081
+ * @returns Array of sitemap entries
4082
+ */
4083
+ getSiteMapEntries() {
4084
+ if (this.siteMapDiscovery) {
4085
+ return this.siteMapDiscovery.getLegacyEntries();
4086
+ }
4087
+ // Fall back to static config
4088
+ return this.config.siteMap || [];
4089
+ }
3396
4090
  }
3397
4091
 
3398
4092
  // src/agentic/CyberneticAgent.ts
@@ -4029,6 +4723,284 @@ function registerAgenticCapabilities(client) {
4029
4723
  client.registerAgentic(capabilities);
4030
4724
  }
4031
4725
 
4726
+ // src/hooks/useSiteMapDiscovery.ts
4727
+ // React hook for sitemap discovery integration
4728
+ // Note: React is a peer dependency - this hook is optional for non-React consumers
4729
+ /**
4730
+ * Hook for discovering routes from React Router and other frameworks
4731
+ *
4732
+ * @example
4733
+ * ```tsx
4734
+ * function App() {
4735
+ * const { entries, isDiscovering } = useSiteMapDiscovery({
4736
+ * enabled: true,
4737
+ * excludePaths: ['/admin/*', '/internal/*']
4738
+ * });
4739
+ *
4740
+ * return (
4741
+ * <ChatbotWidget
4742
+ * agent={{
4743
+ * enabled: true,
4744
+ * siteMap: entries
4745
+ * }}
4746
+ * />
4747
+ * );
4748
+ * }
4749
+ * ```
4750
+ */
4751
+ function useSiteMapDiscovery(options = {}) {
4752
+ const { enabled = true, transformRoute: _transformRoute, excludePaths: _excludePaths = [], navSelectors: _navSelectors, disableDomFallback = false } = options;
4753
+ const [state, setState] = useState({
4754
+ entries: [],
4755
+ isDiscovering: false,
4756
+ error: null,
4757
+ framework: null
4758
+ });
4759
+ const discoveredRef = useRef(false);
4760
+ const optionsRef = useRef(options);
4761
+ optionsRef.current = options;
4762
+ /**
4763
+ * Detect JavaScript framework
4764
+ */
4765
+ const detectFramework = useCallback(() => {
4766
+ if (typeof window === 'undefined')
4767
+ return 'generic';
4768
+ const win = window;
4769
+ if (win.__REACT_ROUTER_DATA_ROUTER__)
4770
+ return 'react-router';
4771
+ if (win.__reactRouterContext)
4772
+ return 'react-router';
4773
+ if (win.__VUE_ROUTER__ || win.__VUE_APP__?.$router)
4774
+ return 'vue-router';
4775
+ if (win.__NEXT_DATA__)
4776
+ return 'next';
4777
+ if (win.ng || document.querySelector('[ng-version]'))
4778
+ return 'angular';
4779
+ return 'generic';
4780
+ }, []);
4781
+ /**
4782
+ * Check if path should be excluded
4783
+ */
4784
+ const shouldExclude = useCallback((path) => {
4785
+ const patterns = optionsRef.current.excludePaths || [];
4786
+ for (const pattern of patterns) {
4787
+ const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$');
4788
+ if (regex.test(path))
4789
+ return true;
4790
+ }
4791
+ return false;
4792
+ }, []);
4793
+ /**
4794
+ * Convert path to human-readable name
4795
+ */
4796
+ const pathToName = useCallback((path) => {
4797
+ const segments = path.split('/').filter(Boolean);
4798
+ const last = segments[segments.length - 1] || 'Home';
4799
+ return last
4800
+ .replace(/[-_:]/g, ' ')
4801
+ .replace(/\[.*?\]/g, '')
4802
+ .split(' ')
4803
+ .map(w => w.charAt(0).toUpperCase() + w.slice(1))
4804
+ .join(' ')
4805
+ .trim() || 'Page';
4806
+ }, []);
4807
+ /**
4808
+ * Default route transform
4809
+ */
4810
+ const defaultTransform = useCallback((route) => {
4811
+ if (!route.path || route.path === '*' || route.path === '**')
4812
+ return null;
4813
+ return {
4814
+ path: route.path,
4815
+ name: route.name || route.meta?.title || pathToName(route.path),
4816
+ description: route.meta?.description,
4817
+ aliases: route.meta?.keywords || route.meta?.aliases,
4818
+ dynamicParams: route.path.includes(':') || route.path.includes('[')
4819
+ };
4820
+ }, [pathToName]);
4821
+ /**
4822
+ * Extract routes from React Router config
4823
+ */
4824
+ const extractReactRouterRoutes = useCallback((routeConfigs, routes, parentPath) => {
4825
+ for (const config of routeConfigs) {
4826
+ let path = config.path || '';
4827
+ if (path && !path.startsWith('/')) {
4828
+ path = parentPath ? `${parentPath}/${path}`.replace(/\/+/g, '/') : `/${path}`;
4829
+ }
4830
+ else if (!path && parentPath) {
4831
+ path = parentPath;
4832
+ }
4833
+ const route = {
4834
+ path,
4835
+ name: config.handle?.name || config.handle?.title || config.id,
4836
+ meta: config.handle || {}
4837
+ };
4838
+ if (config.path && !shouldExclude(path)) {
4839
+ routes.push(route);
4840
+ }
4841
+ if (config.children) {
4842
+ extractReactRouterRoutes(config.children, routes, path);
4843
+ }
4844
+ }
4845
+ }, [shouldExclude]);
4846
+ /**
4847
+ * Discover routes from DOM navigation
4848
+ */
4849
+ const discoverFromDOM = useCallback(() => {
4850
+ if (typeof document === 'undefined')
4851
+ return [];
4852
+ const defaultSelectors = [
4853
+ 'nav a[href]',
4854
+ '[role="navigation"] a[href]',
4855
+ '.nav a[href]',
4856
+ '.sidebar a[href]',
4857
+ '.menu a[href]',
4858
+ 'header a[href]'
4859
+ ];
4860
+ const selectors = optionsRef.current.navSelectors || defaultSelectors;
4861
+ const routes = [];
4862
+ const seen = new Set();
4863
+ for (const selector of selectors) {
4864
+ try {
4865
+ const links = document.querySelectorAll(selector);
4866
+ for (const link of links) {
4867
+ const href = link.getAttribute('href');
4868
+ if (!href || href.startsWith('http') || href.startsWith('javascript:'))
4869
+ continue;
4870
+ const path = href.startsWith('/') ? href : `/${href}`;
4871
+ const cleanPath = path.split('?')[0].split('#')[0];
4872
+ if (seen.has(cleanPath) || cleanPath === '/' || shouldExclude(cleanPath))
4873
+ continue;
4874
+ seen.add(cleanPath);
4875
+ routes.push({
4876
+ path: cleanPath,
4877
+ name: link.textContent?.trim() || pathToName(cleanPath)
4878
+ });
4879
+ }
4880
+ }
4881
+ catch (e) {
4882
+ // Invalid selector
4883
+ }
4884
+ }
4885
+ return routes;
4886
+ }, [shouldExclude, pathToName]);
4887
+ /**
4888
+ * Main discovery function
4889
+ */
4890
+ const discoverRoutes = useCallback(() => {
4891
+ if (!enabled || typeof window === 'undefined') {
4892
+ return;
4893
+ }
4894
+ setState((prev) => ({ ...prev, isDiscovering: true, error: null }));
4895
+ try {
4896
+ const framework = detectFramework();
4897
+ const routes = [];
4898
+ const win = window;
4899
+ // Try framework-specific discovery
4900
+ if (framework === 'react-router' && win.__REACT_ROUTER_DATA_ROUTER__?.routes) {
4901
+ extractReactRouterRoutes(win.__REACT_ROUTER_DATA_ROUTER__.routes, routes, '');
4902
+ }
4903
+ else if (framework === 'vue-router') {
4904
+ const router = win.__VUE_APP__?.$router || win.__VUE_ROUTER__;
4905
+ if (router?.getRoutes) {
4906
+ for (const vueRoute of router.getRoutes()) {
4907
+ if (!shouldExclude(vueRoute.path)) {
4908
+ routes.push({
4909
+ path: vueRoute.path,
4910
+ name: vueRoute.name?.toString(),
4911
+ meta: vueRoute.meta || {}
4912
+ });
4913
+ }
4914
+ }
4915
+ }
4916
+ }
4917
+ else if (framework === 'next' && win.__NEXT_DATA__?.buildManifest?.pages) {
4918
+ for (const pagePath of Object.keys(win.__NEXT_DATA__.buildManifest.pages)) {
4919
+ if (!pagePath.startsWith('/_') && !shouldExclude(pagePath)) {
4920
+ routes.push({
4921
+ path: pagePath,
4922
+ name: pathToName(pagePath)
4923
+ });
4924
+ }
4925
+ }
4926
+ }
4927
+ // DOM fallback if no routes found and not disabled
4928
+ if (routes.length === 0 && !disableDomFallback) {
4929
+ routes.push(...discoverFromDOM());
4930
+ }
4931
+ // Transform routes to sitemap entries
4932
+ const transform = optionsRef.current.transformRoute || defaultTransform;
4933
+ const entries = [];
4934
+ for (const route of routes) {
4935
+ const entry = transform(route);
4936
+ if (entry) {
4937
+ entries.push(entry);
4938
+ }
4939
+ }
4940
+ setState({
4941
+ entries,
4942
+ isDiscovering: false,
4943
+ error: null,
4944
+ framework
4945
+ });
4946
+ discoveredRef.current = true;
4947
+ console.log(`[useSiteMapDiscovery] Discovered ${entries.length} routes from ${framework}`);
4948
+ }
4949
+ catch (error) {
4950
+ console.warn('[useSiteMapDiscovery] Discovery failed:', error);
4951
+ setState((prev) => ({
4952
+ ...prev,
4953
+ isDiscovering: false,
4954
+ error: String(error)
4955
+ }));
4956
+ }
4957
+ }, [
4958
+ enabled,
4959
+ detectFramework,
4960
+ extractReactRouterRoutes,
4961
+ discoverFromDOM,
4962
+ defaultTransform,
4963
+ shouldExclude,
4964
+ pathToName,
4965
+ disableDomFallback
4966
+ ]);
4967
+ // Initial discovery with delay to ensure router is mounted
4968
+ useEffect(() => {
4969
+ if (!enabled || discoveredRef.current)
4970
+ return;
4971
+ const timer = setTimeout(discoverRoutes, 100);
4972
+ return () => clearTimeout(timer);
4973
+ }, [enabled, discoverRoutes]);
4974
+ // Re-discover on location changes (for SPAs)
4975
+ useEffect(() => {
4976
+ if (!enabled || typeof window === 'undefined')
4977
+ return;
4978
+ const handlePopState = () => {
4979
+ // Re-run discovery after navigation (routes might have changed)
4980
+ setTimeout(discoverRoutes, 50);
4981
+ };
4982
+ window.addEventListener('popstate', handlePopState);
4983
+ return () => window.removeEventListener('popstate', handlePopState);
4984
+ }, [enabled, discoverRoutes]);
4985
+ return {
4986
+ ...state,
4987
+ refresh: discoverRoutes
4988
+ };
4989
+ }
4990
+ /**
4991
+ * Create a FrameworkDiscoveryConfig from hook options
4992
+ * Useful for passing to CyberneticClient
4993
+ */
4994
+ function createDiscoveryConfig(options) {
4995
+ return {
4996
+ enabled: options.enabled ?? true,
4997
+ domFallback: !options.disableDomFallback,
4998
+ navSelectors: options.navSelectors,
4999
+ excludePaths: options.excludePaths,
5000
+ transformRoute: options.transformRoute
5001
+ };
5002
+ }
5003
+
4032
5004
  // © 2026 AsterMind AI Co. – All Rights Reserved.
4033
5005
  // Patent Pending US 63/897,713
4034
5006
  // ELMConfig.ts - Configuration interfaces, defaults, helpers for ELM-based models
@@ -5424,5 +6396,5 @@ function createClient(config) {
5424
6396
  return new CyberneticClient(config);
5425
6397
  }
5426
6398
 
5427
- export { ApiClient, CyberneticAgent, CyberneticCache, CyberneticClient, CyberneticIntentClassifier, CyberneticLocalRAG, CyberneticOfflineStorage, LicenseManager, OmegaOfflineRAG, REQUIRED_FEATURES, configLoaders, createClient, createLicenseManager, detectEnvironment, getEnforcementMode, getTokenExpiration, isValidJWTFormat, loadConfig, registerAgenticCapabilities, validateConfig, verifyLicenseToken };
6399
+ export { ApiClient, CyberneticAgent, CyberneticCache, CyberneticClient, CyberneticIntentClassifier, CyberneticLocalRAG, CyberneticOfflineStorage, LicenseManager, OmegaOfflineRAG, REQUIRED_FEATURES, SiteMapDiscovery, configLoaders, createClient, createDiscoveryConfig, createLicenseManager, detectEnvironment, getEnforcementMode, getTokenExpiration, isValidJWTFormat, loadConfig, registerAgenticCapabilities, useSiteMapDiscovery, validateConfig, verifyLicenseToken };
5428
6400
  //# sourceMappingURL=cybernetic-chatbot-client.esm.js.map