@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.
- package/dist/agentic/CyberneticIntentClassifier.d.ts +60 -2
- package/dist/agentic/CyberneticIntentClassifier.d.ts.map +1 -1
- package/dist/agentic/SiteMapDiscovery.d.ts +109 -0
- package/dist/agentic/SiteMapDiscovery.d.ts.map +1 -0
- package/dist/agentic/index.d.ts +1 -0
- package/dist/agentic/index.d.ts.map +1 -1
- package/dist/agentic/types.d.ts +3 -0
- package/dist/agentic/types.d.ts.map +1 -1
- package/dist/cybernetic-chatbot-client-full.esm.js +699 -6
- package/dist/cybernetic-chatbot-client-full.esm.js.map +1 -1
- package/dist/cybernetic-chatbot-client-full.min.js +1 -1
- package/dist/cybernetic-chatbot-client-full.min.js.map +1 -1
- package/dist/cybernetic-chatbot-client-full.umd.js +699 -6
- package/dist/cybernetic-chatbot-client-full.umd.js.map +1 -1
- package/dist/cybernetic-chatbot-client.esm.js +979 -7
- package/dist/cybernetic-chatbot-client.esm.js.map +1 -1
- package/dist/cybernetic-chatbot-client.min.js +1 -1
- package/dist/cybernetic-chatbot-client.min.js.map +1 -1
- package/dist/cybernetic-chatbot-client.umd.js +984 -10
- package/dist/cybernetic-chatbot-client.umd.js.map +1 -1
- package/dist/hooks/index.d.ts +3 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/useSiteMapDiscovery.d.ts +60 -0
- package/dist/hooks/useSiteMapDiscovery.d.ts.map +1 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/types.d.ts +96 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +10 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
2990
|
-
|
|
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
|
-
|
|
2999
|
-
|
|
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
|