@smartnet360/svelte-components 0.0.102 → 0.0.103
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/apps/antenna-pattern/index.d.ts +1 -0
- package/dist/apps/antenna-pattern/index.js +1 -0
- package/dist/apps/antenna-pattern/utils/load-static-antennas.d.ts +17 -0
- package/dist/apps/antenna-pattern/utils/load-static-antennas.js +83 -0
- package/dist/apps/site-check/SiteCheck.svelte +4 -6
- package/dist/core/Charts/ChartCard.svelte +122 -12
- package/dist/core/Charts/ChartCard.svelte.d.ts +2 -0
- package/dist/core/Charts/ChartComponent.svelte +8 -6
- package/dist/core/CoverageMap/ai/AITools.d.ts +117 -0
- package/dist/core/CoverageMap/ai/AITools.js +380 -0
- package/dist/core/CoverageMap/core/CoverageCalculator.d.ts +138 -0
- package/dist/core/CoverageMap/core/CoverageCalculator.js +375 -0
- package/dist/core/CoverageMap/core/GridCalculator.d.ts +115 -0
- package/dist/core/CoverageMap/core/GridCalculator.js +484 -0
- package/dist/core/CoverageMap/core/PathLossModels.d.ts +253 -0
- package/dist/core/CoverageMap/core/PathLossModels.js +380 -0
- package/dist/core/CoverageMap/core/SignalProcessor.d.ts +288 -0
- package/dist/core/CoverageMap/core/SignalProcessor.js +424 -0
- package/dist/core/CoverageMap/data/AntennaStore.d.ts +165 -0
- package/dist/core/CoverageMap/data/AntennaStore.js +327 -0
- package/dist/core/CoverageMap/data/SiteStore.d.ts +155 -0
- package/dist/core/CoverageMap/data/SiteStore.js +355 -0
- package/dist/core/CoverageMap/index.d.ts +74 -0
- package/dist/core/CoverageMap/index.js +103 -0
- package/dist/core/CoverageMap/types.d.ts +252 -0
- package/dist/core/CoverageMap/types.js +7 -0
- package/dist/core/CoverageMap/utils/geoUtils.d.ts +223 -0
- package/dist/core/CoverageMap/utils/geoUtils.js +374 -0
- package/dist/core/CoverageMap/utils/rfUtils.d.ts +329 -0
- package/dist/core/CoverageMap/utils/rfUtils.js +434 -0
- package/dist/core/CoverageMap/visualization/ColorSchemes.d.ts +149 -0
- package/dist/core/CoverageMap/visualization/ColorSchemes.js +377 -0
- package/dist/core/TreeView/index.d.ts +4 -4
- package/dist/core/TreeView/index.js +5 -5
- package/dist/core/TreeView/tree-utils.d.ts +12 -0
- package/dist/core/TreeView/tree-utils.js +115 -6
- package/dist/core/TreeView/tree.store.svelte.d.ts +94 -0
- package/dist/core/TreeView/tree.store.svelte.js +274 -0
- package/dist/map-v2/features/cells/controls/CellFilterControl.svelte +16 -27
- package/dist/map-v2/features/repeaters/controls/RepeaterFilterControl.svelte +33 -42
- package/dist/map-v2/features/sites/controls/SiteFilterControl.svelte +12 -19
- package/dist/map-v3/core/components/Map.svelte +4 -0
- package/dist/map-v3/core/stores/map.store.svelte.js +2 -0
- package/dist/map-v3/features/cells/components/CellFilterControl.svelte +24 -30
- package/dist/map-v3/features/coverage/index.d.ts +12 -0
- package/dist/map-v3/features/coverage/index.js +16 -0
- package/dist/map-v3/features/coverage/layers/CoverageLayer.svelte +198 -0
- package/dist/map-v3/features/coverage/layers/CoverageLayer.svelte.d.ts +10 -0
- package/dist/map-v3/features/coverage/logic/coloring.d.ts +28 -0
- package/dist/map-v3/features/coverage/logic/coloring.js +77 -0
- package/dist/map-v3/features/coverage/logic/geometry.d.ts +33 -0
- package/dist/map-v3/features/coverage/logic/geometry.js +112 -0
- package/dist/map-v3/features/coverage/stores/coverage.data.svelte.d.ts +46 -0
- package/dist/map-v3/features/coverage/stores/coverage.data.svelte.js +95 -0
- package/dist/map-v3/features/coverage/stores/coverage.display.svelte.d.ts +33 -0
- package/dist/map-v3/features/coverage/stores/coverage.display.svelte.js +90 -0
- package/dist/map-v3/features/coverage/types.d.ts +52 -0
- package/dist/map-v3/features/coverage/types.js +7 -0
- package/dist/map-v3/features/repeaters/components/RepeaterFilterControl.svelte +14 -20
- package/dist/map-v3/features/sites/components/SiteFilterControl.svelte +23 -33
- package/dist/map-v3/index.d.ts +4 -0
- package/dist/map-v3/index.js +5 -0
- package/package.json +2 -2
- package/dist/core/TreeView/tree.store.d.ts +0 -10
- package/dist/core/TreeView/tree.store.js +0 -320
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Load antennas from static/antennas folder via fetch
|
|
3
|
+
* This is a helper for loading pre-bundled antenna files without user interaction
|
|
4
|
+
*/
|
|
5
|
+
import type { Antenna } from '../db';
|
|
6
|
+
/**
|
|
7
|
+
* Load and parse all static antenna files
|
|
8
|
+
*/
|
|
9
|
+
export declare function loadStaticAntennas(): Promise<Antenna[]>;
|
|
10
|
+
/**
|
|
11
|
+
* Load static antennas and save to database
|
|
12
|
+
*/
|
|
13
|
+
export declare function importStaticAntennas(): Promise<{
|
|
14
|
+
success: boolean;
|
|
15
|
+
count: number;
|
|
16
|
+
error?: string;
|
|
17
|
+
}>;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Load antennas from static/antennas folder via fetch
|
|
3
|
+
* This is a helper for loading pre-bundled antenna files without user interaction
|
|
4
|
+
*/
|
|
5
|
+
import { parseMSIFile } from './msi-parser';
|
|
6
|
+
import { saveAntennas } from './db-utils';
|
|
7
|
+
const STATIC_ANTENNA_FILES = [
|
|
8
|
+
'ADU4515R17v06_0699_X_CO_M45_03T_Lr1.msi',
|
|
9
|
+
'ADU4515R17v06_0699_X_CO_M45_04T_Lr1.msi',
|
|
10
|
+
'ADU4515R17v06_0699_X_CO_M45_05T_Lr1.msi',
|
|
11
|
+
'ADU4515R17v06_0699_X_CO_M45_06T_Lr1.msi',
|
|
12
|
+
'ADU4515R17v06_0699_X_CO_M45_07T_Lr1.msi',
|
|
13
|
+
'ADU4515R17v06_0699_X_CO_M45_08T_Lr1.msi',
|
|
14
|
+
'ADU4515R17v06_0699_X_CO_M45_09T_Lr1.msi',
|
|
15
|
+
'ADU4515R17v06_0699_X_CO_M45_10T_Lr1.msi',
|
|
16
|
+
'ADU4515R17v06_0699_X_CO_M45_11T_Lr1.msi',
|
|
17
|
+
'ADU4515R17v06_0699_X_CO_M45_12T_Lr1.msi'
|
|
18
|
+
];
|
|
19
|
+
/**
|
|
20
|
+
* Load and parse all static antenna files
|
|
21
|
+
*/
|
|
22
|
+
export async function loadStaticAntennas() {
|
|
23
|
+
const antennas = [];
|
|
24
|
+
console.log(`[loadStaticAntennas] Starting to load ${STATIC_ANTENNA_FILES.length} antenna files...`);
|
|
25
|
+
for (const filename of STATIC_ANTENNA_FILES) {
|
|
26
|
+
try {
|
|
27
|
+
// Fetch the file from static folder
|
|
28
|
+
const url = `/antennas/${filename}`;
|
|
29
|
+
console.log(`[loadStaticAntennas] Fetching: ${url}`);
|
|
30
|
+
const response = await fetch(url);
|
|
31
|
+
if (!response.ok) {
|
|
32
|
+
console.warn(`[loadStaticAntennas] Failed to fetch ${filename}: ${response.status} ${response.statusText}`);
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
// Convert to File object for parser
|
|
36
|
+
const blob = await response.blob();
|
|
37
|
+
const file = new File([blob], filename, { type: 'text/plain' });
|
|
38
|
+
console.log(`[loadStaticAntennas] Parsing ${filename}...`);
|
|
39
|
+
// Parse the MSI file
|
|
40
|
+
const antenna = await parseMSIFile(file);
|
|
41
|
+
antenna.sourcePath = `static/antennas/${filename}`;
|
|
42
|
+
antennas.push(antenna);
|
|
43
|
+
console.log(`[loadStaticAntennas] ✓ Loaded: ${antenna.name} (${antenna.frequency} MHz, tilt: ${antenna.tilt}°)`);
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
console.error(`[loadStaticAntennas] Error parsing ${filename}:`, error);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
console.log(`[loadStaticAntennas] Completed. Loaded ${antennas.length}/${STATIC_ANTENNA_FILES.length} antennas`);
|
|
50
|
+
return antennas;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Load static antennas and save to database
|
|
54
|
+
*/
|
|
55
|
+
export async function importStaticAntennas() {
|
|
56
|
+
try {
|
|
57
|
+
console.log('[importStaticAntennas] Starting import process...');
|
|
58
|
+
const antennas = await loadStaticAntennas();
|
|
59
|
+
if (antennas.length === 0) {
|
|
60
|
+
console.error('[importStaticAntennas] No antennas were successfully loaded');
|
|
61
|
+
return {
|
|
62
|
+
success: false,
|
|
63
|
+
count: 0,
|
|
64
|
+
error: 'No antennas were successfully loaded'
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
console.log(`[importStaticAntennas] Saving ${antennas.length} antennas to database...`);
|
|
68
|
+
await saveAntennas(antennas);
|
|
69
|
+
console.log(`[importStaticAntennas] ✓ Successfully saved ${antennas.length} antennas to database`);
|
|
70
|
+
return {
|
|
71
|
+
success: true,
|
|
72
|
+
count: antennas.length
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
console.error('[importStaticAntennas] Failed to import static antennas:', error);
|
|
77
|
+
return {
|
|
78
|
+
success: false,
|
|
79
|
+
count: 0,
|
|
80
|
+
error: error instanceof Error ? error.message : 'Unknown error'
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -93,12 +93,10 @@
|
|
|
93
93
|
// Derive chart data from tree selection
|
|
94
94
|
let filteredData = $derived.by(() => {
|
|
95
95
|
if (!treeStore) return [];
|
|
96
|
-
const
|
|
97
|
-
if (!storeValue) return [];
|
|
98
|
-
const filtered = filterChartData(rawData, storeValue.state.checkedPaths);
|
|
96
|
+
const filtered = filterChartData(rawData, treeStore.state.checkedPaths);
|
|
99
97
|
log('🔍 Filtered Data:', {
|
|
100
98
|
totalRaw: rawData.length,
|
|
101
|
-
checkedPaths: Array.from(
|
|
99
|
+
checkedPaths: Array.from(treeStore.state.checkedPaths),
|
|
102
100
|
filteredCount: filtered.length,
|
|
103
101
|
cells: Array.from(new Set(filtered.map(r => r.cellName)))
|
|
104
102
|
});
|
|
@@ -210,7 +208,7 @@
|
|
|
210
208
|
{colorDimension}
|
|
211
209
|
{singleRootSelect}
|
|
212
210
|
{singleLevel1Select}
|
|
213
|
-
|
|
211
|
+
{treeStore}
|
|
214
212
|
{showGroupingSelector}
|
|
215
213
|
{onSearch}
|
|
216
214
|
{searchPlaceholder}
|
|
@@ -223,7 +221,7 @@
|
|
|
223
221
|
<!-- Tree View -->
|
|
224
222
|
<div class="flex-grow-1" style="min-height: 0; overflow: hidden;">
|
|
225
223
|
{#if treeStore}
|
|
226
|
-
<TreeView store={
|
|
224
|
+
<TreeView store={treeStore} showControls={true} showIndeterminate={true} height="100%" />
|
|
227
225
|
{/if}
|
|
228
226
|
</div>
|
|
229
227
|
</div>
|
|
@@ -26,6 +26,8 @@
|
|
|
26
26
|
runtimeShowMarkers?: boolean; // Runtime control for showing markers (default: true)
|
|
27
27
|
runtimeShowLegend?: boolean; // Runtime control for showing legend (default: true)
|
|
28
28
|
runtimeHoverMode?: HoverMode; // Runtime override for hover mode from global controls
|
|
29
|
+
renderDelay?: number; // Delay in ms before rendering (for staggered loading)
|
|
30
|
+
lazyRender?: boolean; // Enable Intersection Observer-based lazy rendering
|
|
29
31
|
onchartcontextmenu?: (detail: {
|
|
30
32
|
chart: ChartModel;
|
|
31
33
|
sectionId?: string;
|
|
@@ -34,12 +36,13 @@
|
|
|
34
36
|
}) => void;
|
|
35
37
|
}
|
|
36
38
|
|
|
37
|
-
let { chart, processedData, markers, plotlyLayout, enableAdaptation = true, sectionId, sectionMovingAverage, layoutMovingAverage, layoutHoverMode, layoutColoredHover = true, runtimeMAOverride, runtimeShowOriginal, runtimeShowMarkers = true, runtimeShowLegend = true, runtimeHoverMode, onchartcontextmenu }: Props = $props();
|
|
39
|
+
let { chart, processedData, markers, plotlyLayout, enableAdaptation = true, sectionId, sectionMovingAverage, layoutMovingAverage, layoutHoverMode, layoutColoredHover = true, runtimeMAOverride, runtimeShowOriginal, runtimeShowMarkers = true, runtimeShowLegend = true, runtimeHoverMode, renderDelay = 0, lazyRender = false, onchartcontextmenu }: Props = $props();
|
|
38
40
|
|
|
39
41
|
// Chart container div and state
|
|
40
42
|
let chartDiv: HTMLElement;
|
|
41
43
|
let containerSize = $state<ContainerSize>({ width: 0, height: 0 });
|
|
42
44
|
let chartInitialized = $state(false); // Track if chart has been created
|
|
45
|
+
let isVisible = $state(!lazyRender); // For lazy rendering: start visible if not lazy
|
|
43
46
|
let isHealthy = $state(checkHealth('charts'));
|
|
44
47
|
|
|
45
48
|
function handleContextMenu(event: MouseEvent) {
|
|
@@ -270,11 +273,12 @@
|
|
|
270
273
|
}
|
|
271
274
|
|
|
272
275
|
onMount(() => {
|
|
273
|
-
|
|
274
276
|
log('📈 ChartCard mounted', {
|
|
275
277
|
chartTitle: chart.title,
|
|
276
278
|
leftKPIs: chart.yLeft.length,
|
|
277
|
-
rightKPIs: chart.yRight.length
|
|
279
|
+
rightKPIs: chart.yRight.length,
|
|
280
|
+
renderDelay,
|
|
281
|
+
lazyRender
|
|
278
282
|
});
|
|
279
283
|
|
|
280
284
|
// Initial container size measurement
|
|
@@ -282,17 +286,50 @@
|
|
|
282
286
|
const rect = chartDiv.getBoundingClientRect();
|
|
283
287
|
containerSize.width = rect.width;
|
|
284
288
|
containerSize.height = rect.height;
|
|
285
|
-
|
|
286
|
-
log('📐 Initial container size', {
|
|
287
|
-
chartTitle: chart.title,
|
|
288
|
-
width: rect.width,
|
|
289
|
-
height: rect.height
|
|
290
|
-
});
|
|
291
289
|
}
|
|
292
|
-
|
|
290
|
+
|
|
291
|
+
if (!isHealthy) {
|
|
293
292
|
return;
|
|
294
293
|
}
|
|
295
|
-
|
|
294
|
+
|
|
295
|
+
// Intersection Observer for lazy rendering
|
|
296
|
+
let intersectionObserver: IntersectionObserver | null = null;
|
|
297
|
+
|
|
298
|
+
const doRender = () => {
|
|
299
|
+
if (renderDelay > 0) {
|
|
300
|
+
// Staggered rendering: delay based on chart index
|
|
301
|
+
setTimeout(() => {
|
|
302
|
+
renderChart();
|
|
303
|
+
}, renderDelay);
|
|
304
|
+
} else {
|
|
305
|
+
renderChart();
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
if (lazyRender && chartDiv) {
|
|
310
|
+
// Lazy rendering: only render when chart enters viewport
|
|
311
|
+
intersectionObserver = new IntersectionObserver(
|
|
312
|
+
(entries) => {
|
|
313
|
+
const [entry] = entries;
|
|
314
|
+
if (entry.isIntersecting && !chartInitialized) {
|
|
315
|
+
log('👁️ Chart entering viewport', { chartTitle: chart.title });
|
|
316
|
+
isVisible = true;
|
|
317
|
+
doRender();
|
|
318
|
+
// Disconnect after first render - chart stays rendered
|
|
319
|
+
intersectionObserver?.disconnect();
|
|
320
|
+
}
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
// Render 200px before entering viewport for smooth experience
|
|
324
|
+
rootMargin: '200px',
|
|
325
|
+
threshold: 0
|
|
326
|
+
}
|
|
327
|
+
);
|
|
328
|
+
intersectionObserver.observe(chartDiv);
|
|
329
|
+
} else {
|
|
330
|
+
// Immediate or staggered rendering (no lazy)
|
|
331
|
+
doRender();
|
|
332
|
+
}
|
|
296
333
|
|
|
297
334
|
// Set up ResizeObserver with debouncing to prevent excessive re-renders
|
|
298
335
|
if (chartDiv && window.ResizeObserver) {
|
|
@@ -330,12 +367,13 @@
|
|
|
330
367
|
|
|
331
368
|
resizeObserver.observe(chartDiv);
|
|
332
369
|
|
|
333
|
-
// Clean up
|
|
370
|
+
// Clean up observers on component destroy
|
|
334
371
|
return () => {
|
|
335
372
|
if (resizeTimeout) {
|
|
336
373
|
clearTimeout(resizeTimeout);
|
|
337
374
|
}
|
|
338
375
|
resizeObserver.disconnect();
|
|
376
|
+
intersectionObserver?.disconnect();
|
|
339
377
|
|
|
340
378
|
// Clean up Plotly chart
|
|
341
379
|
if (chartDiv && chartInitialized) {
|
|
@@ -344,6 +382,15 @@
|
|
|
344
382
|
}
|
|
345
383
|
};
|
|
346
384
|
}
|
|
385
|
+
|
|
386
|
+
// If no ResizeObserver, still clean up intersection observer
|
|
387
|
+
return () => {
|
|
388
|
+
intersectionObserver?.disconnect();
|
|
389
|
+
if (chartDiv && chartInitialized) {
|
|
390
|
+
Plotly.purge(chartDiv);
|
|
391
|
+
chartInitialized = false;
|
|
392
|
+
}
|
|
393
|
+
};
|
|
347
394
|
});
|
|
348
395
|
|
|
349
396
|
// React to prop changes - debounce re-renders for better performance
|
|
@@ -368,9 +415,19 @@
|
|
|
368
415
|
</script>
|
|
369
416
|
|
|
370
417
|
<div class="chart-card" role="group" oncontextmenu={handleContextMenu}>
|
|
418
|
+
{#if !isVisible || !chartInitialized}
|
|
419
|
+
<!-- Loading placeholder for lazy/staggered rendering -->
|
|
420
|
+
<div class="chart-placeholder">
|
|
421
|
+
<div class="placeholder-content">
|
|
422
|
+
<div class="placeholder-spinner"></div>
|
|
423
|
+
<span class="placeholder-text">{chart.title}</span>
|
|
424
|
+
</div>
|
|
425
|
+
</div>
|
|
426
|
+
{/if}
|
|
371
427
|
<div
|
|
372
428
|
bind:this={chartDiv}
|
|
373
429
|
class="chart-container"
|
|
430
|
+
class:hidden={!isVisible}
|
|
374
431
|
></div>
|
|
375
432
|
</div>
|
|
376
433
|
|
|
@@ -387,6 +444,7 @@
|
|
|
387
444
|
overflow: hidden; /* Prevent content overflow */
|
|
388
445
|
box-sizing: border-box; /* Include padding in size calculations */
|
|
389
446
|
contain: layout style paint; /* Browser optimization: isolate rendering from rest of page */
|
|
447
|
+
position: relative; /* For placeholder positioning */
|
|
390
448
|
}
|
|
391
449
|
|
|
392
450
|
.chart-card:hover {
|
|
@@ -402,4 +460,56 @@
|
|
|
402
460
|
box-sizing: border-box; /* Include padding in size calculations */
|
|
403
461
|
overflow: hidden; /* Prevent Plotly overflow */
|
|
404
462
|
}
|
|
463
|
+
|
|
464
|
+
.chart-container.hidden {
|
|
465
|
+
visibility: hidden;
|
|
466
|
+
position: absolute;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/* Loading placeholder styles */
|
|
470
|
+
.chart-placeholder {
|
|
471
|
+
position: absolute;
|
|
472
|
+
top: 0;
|
|
473
|
+
left: 0;
|
|
474
|
+
right: 0;
|
|
475
|
+
bottom: 0;
|
|
476
|
+
display: flex;
|
|
477
|
+
align-items: center;
|
|
478
|
+
justify-content: center;
|
|
479
|
+
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
|
|
480
|
+
z-index: 1;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
.placeholder-content {
|
|
484
|
+
display: flex;
|
|
485
|
+
flex-direction: column;
|
|
486
|
+
align-items: center;
|
|
487
|
+
gap: 0.75rem;
|
|
488
|
+
color: #6c757d;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
.placeholder-spinner {
|
|
492
|
+
width: 24px;
|
|
493
|
+
height: 24px;
|
|
494
|
+
border: 2px solid #dee2e6;
|
|
495
|
+
border-top-color: #007bff;
|
|
496
|
+
border-radius: 50%;
|
|
497
|
+
animation: spin 0.8s linear infinite;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
.placeholder-text {
|
|
501
|
+
font-size: 0.75rem;
|
|
502
|
+
font-weight: 500;
|
|
503
|
+
text-align: center;
|
|
504
|
+
max-width: 80%;
|
|
505
|
+
overflow: hidden;
|
|
506
|
+
text-overflow: ellipsis;
|
|
507
|
+
white-space: nowrap;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
@keyframes spin {
|
|
511
|
+
to {
|
|
512
|
+
transform: rotate(360deg);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
405
515
|
</style>
|
|
@@ -319,7 +319,7 @@
|
|
|
319
319
|
</script>
|
|
320
320
|
|
|
321
321
|
<!-- Reusable chart grid snippet -->
|
|
322
|
-
{#snippet chartGrid(section: Section)}
|
|
322
|
+
{#snippet chartGrid(section: Section, enableLazyRender: boolean = false)}
|
|
323
323
|
{@const gridConfig = getGridConfig(section.grid)}
|
|
324
324
|
<div
|
|
325
325
|
class="chart-grid"
|
|
@@ -327,7 +327,7 @@
|
|
|
327
327
|
style:grid-template-columns={`repeat(${gridConfig.columns}, minmax(0, 1fr))`}
|
|
328
328
|
style:grid-template-rows={`repeat(${gridConfig.rows}, minmax(0, 1fr))`}
|
|
329
329
|
>
|
|
330
|
-
{#each section.charts as chart}
|
|
330
|
+
{#each section.charts as chart, chartIndex}
|
|
331
331
|
<div class="chart-slot">
|
|
332
332
|
<ChartCard
|
|
333
333
|
{chart}
|
|
@@ -344,6 +344,8 @@
|
|
|
344
344
|
runtimeShowMarkers={globalControls.markers?.enabled}
|
|
345
345
|
runtimeShowLegend={globalControls.legend?.enabled}
|
|
346
346
|
runtimeHoverMode={globalControls.hoverMode?.mode}
|
|
347
|
+
renderDelay={chartIndex * 50}
|
|
348
|
+
lazyRender={enableLazyRender}
|
|
347
349
|
onchartcontextmenu={(detail) => handleChartContextMenu(detail, section)}
|
|
348
350
|
/>
|
|
349
351
|
</div>
|
|
@@ -395,8 +397,8 @@
|
|
|
395
397
|
class="tab-section {section.id === activeTabId ? 'active' : 'hidden'}"
|
|
396
398
|
data-section-id="{section.id}"
|
|
397
399
|
>
|
|
398
|
-
<!-- Chart Grid -->
|
|
399
|
-
{@render chartGrid(section)}
|
|
400
|
+
<!-- Chart Grid - staggered rendering only, no lazy since tabs handle visibility -->
|
|
401
|
+
{@render chartGrid(section, false)}
|
|
400
402
|
</div>
|
|
401
403
|
{/each}
|
|
402
404
|
</div>
|
|
@@ -427,8 +429,8 @@
|
|
|
427
429
|
<div class="scrollspy-content" bind:this={scrollspyContent}>
|
|
428
430
|
{#each layout.sections as section}
|
|
429
431
|
<div class="section-content" id="{section.id}">
|
|
430
|
-
<!-- Chart Grid -->
|
|
431
|
-
{@render chartGrid(section)}
|
|
432
|
+
<!-- Chart Grid - lazy + staggered rendering for scrollspy -->
|
|
433
|
+
{@render chartGrid(section, true)}
|
|
432
434
|
</div>
|
|
433
435
|
{/each}
|
|
434
436
|
</div>
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Integration Layer - Tool-Callable Functions
|
|
3
|
+
*
|
|
4
|
+
* This module provides functions designed to be called by AI assistants
|
|
5
|
+
* (e.g., ChatGPT) for coverage analysis and recommendations.
|
|
6
|
+
*
|
|
7
|
+
* Design principles:
|
|
8
|
+
* - Functions are stateless and pure where possible
|
|
9
|
+
* - Inputs and outputs are JSON-serializable
|
|
10
|
+
* - Each function has clear purpose and documentation
|
|
11
|
+
* - Error handling returns structured error objects
|
|
12
|
+
*
|
|
13
|
+
* Phase 2 Implementation Note:
|
|
14
|
+
* These functions will be exposed via a tool registry that ChatGPT
|
|
15
|
+
* can call through function calling / tool use APIs.
|
|
16
|
+
*/
|
|
17
|
+
import type { CoverageResult, AIRecommendation, AIToolFunction } from '../types';
|
|
18
|
+
/**
|
|
19
|
+
* Analyze coverage quality and generate recommendations
|
|
20
|
+
*
|
|
21
|
+
* This function takes coverage calculation results and generates
|
|
22
|
+
* structured analysis suitable for AI interpretation.
|
|
23
|
+
*
|
|
24
|
+
* In Phase 2, ChatGPT will call this with coverage data and receive
|
|
25
|
+
* structured metrics that it can analyze and explain in natural language.
|
|
26
|
+
*
|
|
27
|
+
* @param result - Coverage calculation result
|
|
28
|
+
* @returns Structured analysis data
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* // AI will call:
|
|
32
|
+
* const analysis = analyzeCoverageQuality(coverageResult);
|
|
33
|
+
*
|
|
34
|
+
* // AI receives:
|
|
35
|
+
* {
|
|
36
|
+
* overallQuality: "good",
|
|
37
|
+
* coveragePercent: 78.5,
|
|
38
|
+
* avgSignal: -82.3,
|
|
39
|
+
* issues: ["excessive_range", "weak_avg_signal"],
|
|
40
|
+
* metrics: { ... }
|
|
41
|
+
* }
|
|
42
|
+
*
|
|
43
|
+
* // AI interprets and responds:
|
|
44
|
+
* "Your coverage is good overall at 78.5%. However, I notice two issues:
|
|
45
|
+
* 1. Excessive range detected - consider adding downtilt to reduce interference
|
|
46
|
+
* 2. Average signal is weak at -82.3 dBm - most users will experience medium throughput"
|
|
47
|
+
*/
|
|
48
|
+
export declare function analyzeCoverageQuality(result: CoverageResult): {
|
|
49
|
+
overallQuality: 'excellent' | 'good' | 'fair' | 'poor';
|
|
50
|
+
coveragePercent: number;
|
|
51
|
+
avgSignal: number;
|
|
52
|
+
maxRange: number;
|
|
53
|
+
issues: string[];
|
|
54
|
+
metrics: Record<string, number>;
|
|
55
|
+
sectorBreakdown: any[];
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* Generate optimization suggestions
|
|
59
|
+
*
|
|
60
|
+
* Based on coverage analysis, generates specific parameter adjustment
|
|
61
|
+
* recommendations that can be presented to the user.
|
|
62
|
+
*
|
|
63
|
+
* In Phase 2, AI will use this to generate specific, actionable recommendations.
|
|
64
|
+
*
|
|
65
|
+
* @param result - Coverage result
|
|
66
|
+
* @returns Array of recommendations
|
|
67
|
+
*/
|
|
68
|
+
export declare function generateOptimizationSuggestions(result: CoverageResult): AIRecommendation[];
|
|
69
|
+
/**
|
|
70
|
+
* Compare two coverage scenarios
|
|
71
|
+
*
|
|
72
|
+
* Useful for before/after analysis when user changes parameters.
|
|
73
|
+
*
|
|
74
|
+
* @param before - Coverage before changes
|
|
75
|
+
* @param after - Coverage after changes
|
|
76
|
+
* @returns Comparison analysis
|
|
77
|
+
*/
|
|
78
|
+
export declare function compareCoverageScenarios(before: CoverageResult, after: CoverageResult): {
|
|
79
|
+
coverageChange: number;
|
|
80
|
+
signalChange: number;
|
|
81
|
+
rangeChange: number;
|
|
82
|
+
improvements: string[];
|
|
83
|
+
degradations: string[];
|
|
84
|
+
summary: string;
|
|
85
|
+
};
|
|
86
|
+
/**
|
|
87
|
+
* Extract key metrics for AI analysis
|
|
88
|
+
*
|
|
89
|
+
* Flattens coverage result into simple key-value pairs
|
|
90
|
+
* that are easy for AI to interpret.
|
|
91
|
+
*
|
|
92
|
+
* @param result - Coverage result
|
|
93
|
+
* @returns Flat metrics object
|
|
94
|
+
*/
|
|
95
|
+
export declare function extractAIMetrics(result: CoverageResult): Record<string, number | string>;
|
|
96
|
+
/**
|
|
97
|
+
* Registry of all AI-callable tools
|
|
98
|
+
*
|
|
99
|
+
* In Phase 2, this will be used to expose functions to ChatGPT.
|
|
100
|
+
* Each entry describes the function, its parameters, and its purpose.
|
|
101
|
+
*/
|
|
102
|
+
export declare const AI_TOOLS: AIToolFunction[];
|
|
103
|
+
/**
|
|
104
|
+
* Get AI tool by name
|
|
105
|
+
*
|
|
106
|
+
* @param name - Tool name
|
|
107
|
+
* @returns Tool function or undefined
|
|
108
|
+
*/
|
|
109
|
+
export declare function getAITool(name: string): AIToolFunction | undefined;
|
|
110
|
+
/**
|
|
111
|
+
* Execute AI tool by name
|
|
112
|
+
*
|
|
113
|
+
* @param name - Tool name
|
|
114
|
+
* @param params - Tool parameters
|
|
115
|
+
* @returns Tool result
|
|
116
|
+
*/
|
|
117
|
+
export declare function executeAITool(name: string, params: any): Promise<any>;
|