@naniteninja/profile-comparison-lib 1.0.22 → 1.0.23
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/README.md +167 -167
- package/fesm2022/naniteninja-profile-comparison-lib.mjs +1215 -48
- package/fesm2022/naniteninja-profile-comparison-lib.mjs.map +1 -1
- package/index.d.ts +101 -6
- package/package.json +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { InjectionToken, Injectable, Optional, Inject, inject, EventEmitter, ViewChild, Output, Input, Component, NgModule } from '@angular/core';
|
|
2
|
+
import { InjectionToken, Injectable, Optional, Inject, inject, EventEmitter, HostListener, ViewChild, Output, Input, Component, NgModule } from '@angular/core';
|
|
3
|
+
import { forceSimulation, forceLink, forceCollide, forceX, forceY } from 'd3-force';
|
|
3
4
|
import { switchMap as switchMap$1, map as map$1, catchError, tap as tap$1 } from 'rxjs/operators';
|
|
4
5
|
import { from, switchMap, map, Observable, of, BehaviorSubject, throwError, tap, retryWhen, concatMap, timer, catchError as catchError$1, forkJoin } from 'rxjs';
|
|
5
6
|
import * as i1 from '@angular/common/http';
|
|
@@ -13,12 +14,12 @@ var BackendMode;
|
|
|
13
14
|
(function (BackendMode) {
|
|
14
15
|
/** Use the real deployed backend (default). */
|
|
15
16
|
BackendMode[BackendMode["Real"] = 0] = "Real";
|
|
16
|
-
/** Use a local mock server at localhost:
|
|
17
|
+
/** Use a local mock server at localhost:44101. */
|
|
17
18
|
BackendMode[BackendMode["Mock"] = 1] = "Mock";
|
|
18
19
|
})(BackendMode || (BackendMode = {}));
|
|
19
20
|
const BACKEND_MODE_URLS = {
|
|
20
21
|
[BackendMode.Real]: 'https://api.test.thecyrano.app/api/profile-comparison/api',
|
|
21
|
-
[BackendMode.Mock]: 'http://localhost:
|
|
22
|
+
[BackendMode.Mock]: 'http://localhost:44101/api',
|
|
22
23
|
};
|
|
23
24
|
/**
|
|
24
25
|
* Legacy injection token for the backend base URL.
|
|
@@ -202,10 +203,10 @@ class CachePersistenceService {
|
|
|
202
203
|
static STORE_OPENAI_EMBEDDINGS = STORE_OPENAI_EMBEDDINGS;
|
|
203
204
|
static STORE_PROFILE_FACE = STORE_PROFILE_FACE;
|
|
204
205
|
static STORE_PROFILE_COMPARE = STORE_PROFILE_COMPARE;
|
|
205
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.
|
|
206
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.
|
|
206
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.24", ngImport: i0, type: CachePersistenceService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
207
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.24", ngImport: i0, type: CachePersistenceService, providedIn: 'root' });
|
|
207
208
|
}
|
|
208
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.
|
|
209
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.24", ngImport: i0, type: CachePersistenceService, decorators: [{
|
|
209
210
|
type: Injectable,
|
|
210
211
|
args: [{ providedIn: 'root' }]
|
|
211
212
|
}] });
|
|
@@ -313,10 +314,10 @@ class FileConversionService {
|
|
|
313
314
|
const mime = this.getMimeFromFilename(imageStr);
|
|
314
315
|
return this.urlToFile(imageStr, fileBaseName, mime);
|
|
315
316
|
}
|
|
316
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.
|
|
317
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.
|
|
317
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.24", ngImport: i0, type: FileConversionService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
318
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.24", ngImport: i0, type: FileConversionService, providedIn: 'root' });
|
|
318
319
|
}
|
|
319
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.
|
|
320
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.24", ngImport: i0, type: FileConversionService, decorators: [{
|
|
320
321
|
type: Injectable,
|
|
321
322
|
args: [{
|
|
322
323
|
providedIn: 'root'
|
|
@@ -432,10 +433,10 @@ class ImageCompressionService {
|
|
|
432
433
|
return `${base}.webp`;
|
|
433
434
|
return `${base}.jpg`;
|
|
434
435
|
}
|
|
435
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.
|
|
436
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.
|
|
436
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.24", ngImport: i0, type: ImageCompressionService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
437
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.24", ngImport: i0, type: ImageCompressionService, providedIn: 'root' });
|
|
437
438
|
}
|
|
438
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.
|
|
439
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.24", ngImport: i0, type: ImageCompressionService, decorators: [{
|
|
439
440
|
type: Injectable,
|
|
440
441
|
args: [{
|
|
441
442
|
providedIn: 'root'
|
|
@@ -1214,10 +1215,10 @@ class OpenAIEmbeddingService {
|
|
|
1214
1215
|
this.persistence.clear(CachePersistenceService.STORE_OPENAI_SIMILARITY);
|
|
1215
1216
|
this.persistence.clear(CachePersistenceService.STORE_OPENAI_EMBEDDINGS);
|
|
1216
1217
|
}
|
|
1217
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.
|
|
1218
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.
|
|
1218
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.24", ngImport: i0, type: OpenAIEmbeddingService, deps: [{ token: i1.HttpClient }, { token: PROFILE_COMPARISON_API_BASE_URL, optional: true }, { token: CachePersistenceService }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1219
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.24", ngImport: i0, type: OpenAIEmbeddingService, providedIn: 'root' });
|
|
1219
1220
|
}
|
|
1220
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.
|
|
1221
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.24", ngImport: i0, type: OpenAIEmbeddingService, decorators: [{
|
|
1221
1222
|
type: Injectable,
|
|
1222
1223
|
args: [{
|
|
1223
1224
|
providedIn: 'root'
|
|
@@ -1249,10 +1250,10 @@ class ProfileComparisonLibService {
|
|
|
1249
1250
|
getVersion() {
|
|
1250
1251
|
return '0.0.0';
|
|
1251
1252
|
}
|
|
1252
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.
|
|
1253
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.
|
|
1253
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.24", ngImport: i0, type: ProfileComparisonLibService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1254
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.24", ngImport: i0, type: ProfileComparisonLibService, providedIn: 'root' });
|
|
1254
1255
|
}
|
|
1255
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.
|
|
1256
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.24", ngImport: i0, type: ProfileComparisonLibService, decorators: [{
|
|
1256
1257
|
type: Injectable,
|
|
1257
1258
|
args: [{
|
|
1258
1259
|
providedIn: 'root'
|
|
@@ -1506,10 +1507,10 @@ class ProfileService {
|
|
|
1506
1507
|
this.compareInterestsCache.clear();
|
|
1507
1508
|
this.persistence.clear(CachePersistenceService.STORE_PROFILE_COMPARE);
|
|
1508
1509
|
}
|
|
1509
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.
|
|
1510
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.
|
|
1510
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.24", ngImport: i0, type: ProfileService, deps: [{ token: i1.HttpClient }, { token: PROFILE_COMPARISON_API_BASE_URL, optional: true }, { token: CachePersistenceService }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1511
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.24", ngImport: i0, type: ProfileService, providedIn: 'root' });
|
|
1511
1512
|
}
|
|
1512
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.
|
|
1513
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.24", ngImport: i0, type: ProfileService, decorators: [{
|
|
1513
1514
|
type: Injectable,
|
|
1514
1515
|
args: [{ providedIn: 'root' }]
|
|
1515
1516
|
}], ctorParameters: () => [{ type: i1.HttpClient }, { type: undefined, decorators: [{
|
|
@@ -1565,10 +1566,10 @@ class ProfileComparisonBackendService {
|
|
|
1565
1566
|
return throwError(() => err);
|
|
1566
1567
|
}));
|
|
1567
1568
|
}
|
|
1568
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.
|
|
1569
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.
|
|
1569
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.24", ngImport: i0, type: ProfileComparisonBackendService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1570
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.24", ngImport: i0, type: ProfileComparisonBackendService, providedIn: 'root' });
|
|
1570
1571
|
}
|
|
1571
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.
|
|
1572
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.24", ngImport: i0, type: ProfileComparisonBackendService, decorators: [{
|
|
1572
1573
|
type: Injectable,
|
|
1573
1574
|
args: [{ providedIn: 'root' }]
|
|
1574
1575
|
}], ctorParameters: () => [] });
|
|
@@ -1579,6 +1580,8 @@ class ProfileComparisonLibComponent {
|
|
|
1579
1580
|
fileConversionService;
|
|
1580
1581
|
imageCompressionService;
|
|
1581
1582
|
cdr;
|
|
1583
|
+
ngZone;
|
|
1584
|
+
hostRef;
|
|
1582
1585
|
getVerboseLogging;
|
|
1583
1586
|
static DEFAULT_OBJECT_POSITION = '50% 50%';
|
|
1584
1587
|
static DEFAULT_EYE_ALIGNMENT_BIAS_PX = 50;
|
|
@@ -1706,6 +1709,36 @@ class ProfileComparisonLibComponent {
|
|
|
1706
1709
|
similarity = 0;
|
|
1707
1710
|
similarityMatrixCsv = '';
|
|
1708
1711
|
matrixData = null;
|
|
1712
|
+
// ── Force-directed chip layout ──────────────────────────────────────────
|
|
1713
|
+
static SPRING_THRESHOLD = 0.32;
|
|
1714
|
+
static CROSS_LINK_MIN_SIMILARITY = 0.5;
|
|
1715
|
+
static INTRA_GROUP_THRESHOLD = 0.5;
|
|
1716
|
+
static SHARED_PROMOTION_THRESHOLD = 0.75;
|
|
1717
|
+
static MAX_CROSS_LINKS_PER_NODE = 4;
|
|
1718
|
+
static MIN_CROSS_LINKS_PER_NODE = 0;
|
|
1719
|
+
static MAX_INTRA_LINKS_PER_NODE = 2;
|
|
1720
|
+
static MAX_RELEVANT_INTERESTS_PER_SIDE = 6;
|
|
1721
|
+
static CHIP_RADIUS = 18; // half chip height for collision
|
|
1722
|
+
static CHIP_COLLISION_BORDER_PADDING = 6;
|
|
1723
|
+
static MAX_LINK_DISTANCE = 120;
|
|
1724
|
+
static CANVAS_PADDING = 40; // px below lowest node
|
|
1725
|
+
forceNodes = [];
|
|
1726
|
+
forceLinks = [];
|
|
1727
|
+
forceCanvasHeight = 300;
|
|
1728
|
+
chipFontSizePx = 14;
|
|
1729
|
+
chipPaddingXPx = 18;
|
|
1730
|
+
chipPaddingYPx = 6;
|
|
1731
|
+
chipLineHeight = 1.4;
|
|
1732
|
+
hoveredNode = null;
|
|
1733
|
+
clickedNode = null;
|
|
1734
|
+
draggedNode = null;
|
|
1735
|
+
isDragging = false;
|
|
1736
|
+
simulation;
|
|
1737
|
+
layoutRebuildRaf = null;
|
|
1738
|
+
nodeDragBounds = new Map();
|
|
1739
|
+
get activeNode() {
|
|
1740
|
+
return this.hoveredNode || this.clickedNode;
|
|
1741
|
+
}
|
|
1709
1742
|
/** When false, backend URL is not provided — show "Configure backend". */
|
|
1710
1743
|
backendConfigured = false;
|
|
1711
1744
|
/** The resolved backend URL computed from inputs and legacy token. */
|
|
@@ -1737,12 +1770,15 @@ class ProfileComparisonLibComponent {
|
|
|
1737
1770
|
shapeTextLeft;
|
|
1738
1771
|
shapeTextRight;
|
|
1739
1772
|
shapeTextCenter;
|
|
1740
|
-
|
|
1773
|
+
chipsGrid;
|
|
1774
|
+
constructor(backendService, renderer, fileConversionService, imageCompressionService, cdr, ngZone, hostRef, getVerboseLogging) {
|
|
1741
1775
|
this.backendService = backendService;
|
|
1742
1776
|
this.renderer = renderer;
|
|
1743
1777
|
this.fileConversionService = fileConversionService;
|
|
1744
1778
|
this.imageCompressionService = imageCompressionService;
|
|
1745
1779
|
this.cdr = cdr;
|
|
1780
|
+
this.ngZone = ngZone;
|
|
1781
|
+
this.hostRef = hostRef;
|
|
1746
1782
|
this.getVerboseLogging = getVerboseLogging;
|
|
1747
1783
|
}
|
|
1748
1784
|
log(...args) {
|
|
@@ -1793,6 +1829,15 @@ class ProfileComparisonLibComponent {
|
|
|
1793
1829
|
}
|
|
1794
1830
|
});
|
|
1795
1831
|
}
|
|
1832
|
+
ngOnDestroy() {
|
|
1833
|
+
if (this.simulation) {
|
|
1834
|
+
this.simulation.stop();
|
|
1835
|
+
}
|
|
1836
|
+
if (this.layoutRebuildRaf !== null) {
|
|
1837
|
+
cancelAnimationFrame(this.layoutRebuildRaf);
|
|
1838
|
+
this.layoutRebuildRaf = null;
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1796
1841
|
ngAfterViewInit() {
|
|
1797
1842
|
this.waitForImagesAndInitDrag();
|
|
1798
1843
|
}
|
|
@@ -1885,13 +1930,14 @@ class ProfileComparisonLibComponent {
|
|
|
1885
1930
|
this.alignedPerson2Interests = [];
|
|
1886
1931
|
this.displayPerson1Interests = this.person1Interests;
|
|
1887
1932
|
this.displayPerson2Interests = this.person2Interests;
|
|
1888
|
-
this.centerItem = payload.centerItem ?? [];
|
|
1933
|
+
this.centerItem = this.filterCenterItemsByThreshold(payload.centerItem ?? [], this.person1Interests ?? [], this.person2Interests ?? []);
|
|
1889
1934
|
}
|
|
1890
1935
|
else {
|
|
1891
1936
|
const rawCenter = payload.centerItem ?? [];
|
|
1892
1937
|
const list1ToUse = this.person1Interests?.length ? this.person1Interests : a1;
|
|
1893
1938
|
const list2ToUse = this.person2Interests?.length ? this.person2Interests : a2;
|
|
1894
|
-
const
|
|
1939
|
+
const filteredCenter = this.filterCenterItemsByThreshold(rawCenter, list1ToUse, list2ToUse);
|
|
1940
|
+
const reordered = this.reorderInterestsByMatrix(list1ToUse, list2ToUse, filteredCenter);
|
|
1895
1941
|
this.alignedPerson1Interests = reordered.p1;
|
|
1896
1942
|
this.alignedPerson2Interests = reordered.p2;
|
|
1897
1943
|
this.displayPerson1Interests = reordered.p1;
|
|
@@ -1914,6 +1960,7 @@ class ProfileComparisonLibComponent {
|
|
|
1914
1960
|
this.updateObjectPositionForFace(2);
|
|
1915
1961
|
if (this.firstImageData && this.secondImageData)
|
|
1916
1962
|
this.calculateFaceAlignment(this.firstImageData, this.secondImageData);
|
|
1963
|
+
this.buildForceGraph();
|
|
1917
1964
|
this.cdr.detectChanges();
|
|
1918
1965
|
},
|
|
1919
1966
|
error: (err) => {
|
|
@@ -2250,25 +2297,845 @@ class ProfileComparisonLibComponent {
|
|
|
2250
2297
|
});
|
|
2251
2298
|
});
|
|
2252
2299
|
}
|
|
2300
|
+
normalizeSimilarityToken(value) {
|
|
2301
|
+
return String(value ?? '')
|
|
2302
|
+
.trim()
|
|
2303
|
+
.toLowerCase()
|
|
2304
|
+
.replace(/[^a-z0-9]+/g, '');
|
|
2305
|
+
}
|
|
2306
|
+
findLegendEntry(label) {
|
|
2307
|
+
const legend = this.matrixData?.legend ?? [];
|
|
2308
|
+
if (!legend.length)
|
|
2309
|
+
return undefined;
|
|
2310
|
+
const exact = String(label).trim().toLowerCase();
|
|
2311
|
+
const exactMatch = legend.find(l => String(l.full).trim().toLowerCase() === exact);
|
|
2312
|
+
if (exactMatch)
|
|
2313
|
+
return exactMatch;
|
|
2314
|
+
const normalized = this.normalizeSimilarityToken(label);
|
|
2315
|
+
if (!normalized)
|
|
2316
|
+
return undefined;
|
|
2317
|
+
return legend.find(l => {
|
|
2318
|
+
const full = this.normalizeSimilarityToken(l.full);
|
|
2319
|
+
const abbr = this.normalizeSimilarityToken(l.abbr);
|
|
2320
|
+
return full === normalized || abbr === normalized;
|
|
2321
|
+
});
|
|
2322
|
+
}
|
|
2323
|
+
lookupSimilarity(rowAbbr, colAbbr) {
|
|
2324
|
+
if (!this.matrixData)
|
|
2325
|
+
return null;
|
|
2326
|
+
const colIdx = this.matrixData.headers?.indexOf(colAbbr);
|
|
2327
|
+
if (colIdx === -1 || colIdx === undefined)
|
|
2328
|
+
return null;
|
|
2329
|
+
const row = this.matrixData.rows?.find(r => r.label === rowAbbr);
|
|
2330
|
+
if (!row || !row.values || colIdx >= row.values.length)
|
|
2331
|
+
return null;
|
|
2332
|
+
const val = parseFloat(row.values[colIdx]);
|
|
2333
|
+
return isNaN(val) ? null : val;
|
|
2334
|
+
}
|
|
2253
2335
|
getSimilarity(item1, item2) {
|
|
2254
2336
|
if (!this.matrixData || !item1 || !item2)
|
|
2255
2337
|
return 0;
|
|
2256
|
-
const n1 =
|
|
2257
|
-
const n2 =
|
|
2338
|
+
const n1 = this.normalizeSimilarityToken(item1);
|
|
2339
|
+
const n2 = this.normalizeSimilarityToken(item2);
|
|
2340
|
+
if (!n1 || !n2)
|
|
2341
|
+
return 0;
|
|
2258
2342
|
if (n1 === n2)
|
|
2259
2343
|
return 1.0;
|
|
2260
|
-
const leg1 = this.
|
|
2261
|
-
const leg2 = this.
|
|
2344
|
+
const leg1 = this.findLegendEntry(item1);
|
|
2345
|
+
const leg2 = this.findLegendEntry(item2);
|
|
2262
2346
|
if (!leg1 || !leg2)
|
|
2263
2347
|
return 0;
|
|
2264
|
-
const
|
|
2265
|
-
if (
|
|
2266
|
-
return
|
|
2267
|
-
const
|
|
2268
|
-
if (
|
|
2269
|
-
return
|
|
2270
|
-
|
|
2271
|
-
|
|
2348
|
+
const direct = this.lookupSimilarity(leg1.abbr, leg2.abbr);
|
|
2349
|
+
if (direct != null)
|
|
2350
|
+
return direct;
|
|
2351
|
+
const reverse = this.lookupSimilarity(leg2.abbr, leg1.abbr);
|
|
2352
|
+
if (reverse != null)
|
|
2353
|
+
return reverse;
|
|
2354
|
+
return 0;
|
|
2355
|
+
}
|
|
2356
|
+
// ── Force-directed chip layout ──────────────────────────────────────────
|
|
2357
|
+
orderBySemanticBarycenter(sourceItems, targetItems) {
|
|
2358
|
+
const source = [...sourceItems];
|
|
2359
|
+
if (source.length <= 1 || targetItems.length === 0)
|
|
2360
|
+
return source;
|
|
2361
|
+
const ranked = source.map((label, index) => {
|
|
2362
|
+
let weightedTargetIndex = 0;
|
|
2363
|
+
let totalWeight = 0;
|
|
2364
|
+
for (let i = 0; i < targetItems.length; i++) {
|
|
2365
|
+
const sim = Math.max(0, this.getSimilarity(label, targetItems[i]));
|
|
2366
|
+
if (sim <= 0)
|
|
2367
|
+
continue;
|
|
2368
|
+
weightedTargetIndex += i * sim;
|
|
2369
|
+
totalWeight += sim;
|
|
2370
|
+
}
|
|
2371
|
+
return {
|
|
2372
|
+
label,
|
|
2373
|
+
index,
|
|
2374
|
+
barycenter: totalWeight > 0 ? weightedTargetIndex / totalWeight : index,
|
|
2375
|
+
totalWeight,
|
|
2376
|
+
};
|
|
2377
|
+
});
|
|
2378
|
+
ranked.sort((a, b) => (a.barycenter - b.barycenter) ||
|
|
2379
|
+
(b.totalWeight - a.totalWeight) ||
|
|
2380
|
+
(a.index - b.index));
|
|
2381
|
+
return ranked.map(item => item.label);
|
|
2382
|
+
}
|
|
2383
|
+
orderSharedBySemanticPull(sharedItems, p1Items, p2Items) {
|
|
2384
|
+
const shared = [...sharedItems];
|
|
2385
|
+
if (shared.length <= 1)
|
|
2386
|
+
return shared;
|
|
2387
|
+
const context = [...p1Items, ...p2Items];
|
|
2388
|
+
if (context.length === 0)
|
|
2389
|
+
return shared;
|
|
2390
|
+
const ranked = shared.map((label, index) => {
|
|
2391
|
+
let weightedIndex = 0;
|
|
2392
|
+
let totalWeight = 0;
|
|
2393
|
+
let strongest = 0;
|
|
2394
|
+
for (let i = 0; i < context.length; i++) {
|
|
2395
|
+
const sim = Math.max(0, this.getSimilarity(label, context[i]));
|
|
2396
|
+
if (sim <= 0)
|
|
2397
|
+
continue;
|
|
2398
|
+
weightedIndex += i * sim;
|
|
2399
|
+
totalWeight += sim;
|
|
2400
|
+
strongest = Math.max(strongest, sim);
|
|
2401
|
+
}
|
|
2402
|
+
return {
|
|
2403
|
+
label,
|
|
2404
|
+
index,
|
|
2405
|
+
barycenter: totalWeight > 0 ? weightedIndex / totalWeight : index,
|
|
2406
|
+
strongest,
|
|
2407
|
+
};
|
|
2408
|
+
});
|
|
2409
|
+
ranked.sort((a, b) => (b.strongest - a.strongest) ||
|
|
2410
|
+
(a.barycenter - b.barycenter) ||
|
|
2411
|
+
(a.index - b.index));
|
|
2412
|
+
return ranked.map(item => item.label);
|
|
2413
|
+
}
|
|
2414
|
+
canLinkNodes(a, b) {
|
|
2415
|
+
if (a.group === 'shared' && b.group === 'shared')
|
|
2416
|
+
return true;
|
|
2417
|
+
if (a.group === b.group)
|
|
2418
|
+
return false;
|
|
2419
|
+
if (a.group === 'shared' || b.group === 'shared')
|
|
2420
|
+
return true;
|
|
2421
|
+
return (a.group === 'p1' && b.group === 'p2') || (a.group === 'p2' && b.group === 'p1');
|
|
2422
|
+
}
|
|
2423
|
+
getLinkNodeId(ref) {
|
|
2424
|
+
return typeof ref === 'object' ? ref.id : String(ref);
|
|
2425
|
+
}
|
|
2426
|
+
getLinkPairKey(a, b) {
|
|
2427
|
+
const idA = this.getLinkNodeId(a);
|
|
2428
|
+
const idB = this.getLinkNodeId(b);
|
|
2429
|
+
return idA < idB ? `${idA}|${idB}` : `${idB}|${idA}`;
|
|
2430
|
+
}
|
|
2431
|
+
selectLinksWithCaps(candidates, maxPerNode, minPerNode = 0) {
|
|
2432
|
+
if (!candidates.length || maxPerNode <= 0)
|
|
2433
|
+
return [];
|
|
2434
|
+
const sorted = [...candidates].sort((a, b) => (b.similarity - a.similarity) ||
|
|
2435
|
+
(b.strength - a.strength));
|
|
2436
|
+
const counts = new Map();
|
|
2437
|
+
const selected = [];
|
|
2438
|
+
const selectedKeys = new Set();
|
|
2439
|
+
const getCount = (id) => counts.get(id) ?? 0;
|
|
2440
|
+
const increment = (id) => {
|
|
2441
|
+
counts.set(id, getCount(id) + 1);
|
|
2442
|
+
};
|
|
2443
|
+
const canAdd = (link) => {
|
|
2444
|
+
const sourceId = this.getLinkNodeId(link.source);
|
|
2445
|
+
const targetId = this.getLinkNodeId(link.target);
|
|
2446
|
+
if (getCount(sourceId) >= maxPerNode)
|
|
2447
|
+
return false;
|
|
2448
|
+
if (getCount(targetId) >= maxPerNode)
|
|
2449
|
+
return false;
|
|
2450
|
+
return true;
|
|
2451
|
+
};
|
|
2452
|
+
const add = (link) => {
|
|
2453
|
+
const key = this.getLinkPairKey(link.source, link.target);
|
|
2454
|
+
if (selectedKeys.has(key) || !canAdd(link))
|
|
2455
|
+
return false;
|
|
2456
|
+
selectedKeys.add(key);
|
|
2457
|
+
selected.push(link);
|
|
2458
|
+
const sourceId = this.getLinkNodeId(link.source);
|
|
2459
|
+
const targetId = this.getLinkNodeId(link.target);
|
|
2460
|
+
increment(sourceId);
|
|
2461
|
+
increment(targetId);
|
|
2462
|
+
return true;
|
|
2463
|
+
};
|
|
2464
|
+
const nodeIds = new Set();
|
|
2465
|
+
const perNodeCandidates = new Map();
|
|
2466
|
+
sorted.forEach(link => {
|
|
2467
|
+
const sourceId = this.getLinkNodeId(link.source);
|
|
2468
|
+
const targetId = this.getLinkNodeId(link.target);
|
|
2469
|
+
nodeIds.add(sourceId);
|
|
2470
|
+
nodeIds.add(targetId);
|
|
2471
|
+
const sourceBucket = perNodeCandidates.get(sourceId) ?? [];
|
|
2472
|
+
sourceBucket.push(link);
|
|
2473
|
+
perNodeCandidates.set(sourceId, sourceBucket);
|
|
2474
|
+
const targetBucket = perNodeCandidates.get(targetId) ?? [];
|
|
2475
|
+
targetBucket.push(link);
|
|
2476
|
+
perNodeCandidates.set(targetId, targetBucket);
|
|
2477
|
+
});
|
|
2478
|
+
// Phase 1: satisfy minimum significant relationships before dense global fill.
|
|
2479
|
+
if (minPerNode > 0) {
|
|
2480
|
+
let progressed = true;
|
|
2481
|
+
while (progressed) {
|
|
2482
|
+
progressed = false;
|
|
2483
|
+
nodeIds.forEach(nodeId => {
|
|
2484
|
+
while (getCount(nodeId) < minPerNode) {
|
|
2485
|
+
const next = (perNodeCandidates.get(nodeId) ?? []).find(link => {
|
|
2486
|
+
const key = this.getLinkPairKey(link.source, link.target);
|
|
2487
|
+
return !selectedKeys.has(key) && canAdd(link);
|
|
2488
|
+
});
|
|
2489
|
+
if (!next)
|
|
2490
|
+
break;
|
|
2491
|
+
if (!add(next))
|
|
2492
|
+
break;
|
|
2493
|
+
progressed = true;
|
|
2494
|
+
}
|
|
2495
|
+
});
|
|
2496
|
+
}
|
|
2497
|
+
}
|
|
2498
|
+
// Phase 2: fill remaining slots globally by strongest links.
|
|
2499
|
+
sorted.forEach(link => {
|
|
2500
|
+
add(link);
|
|
2501
|
+
});
|
|
2502
|
+
if (minPerNode > 0) {
|
|
2503
|
+
// Best-effort soft fallback for low-degree nodes if they were blocked by caps.
|
|
2504
|
+
nodeIds.forEach(nodeId => {
|
|
2505
|
+
if (getCount(nodeId) >= minPerNode)
|
|
2506
|
+
return;
|
|
2507
|
+
(perNodeCandidates.get(nodeId) ?? []).forEach(link => {
|
|
2508
|
+
if (getCount(nodeId) >= minPerNode)
|
|
2509
|
+
return;
|
|
2510
|
+
const key = this.getLinkPairKey(link.source, link.target);
|
|
2511
|
+
if (selectedKeys.has(key))
|
|
2512
|
+
return;
|
|
2513
|
+
const sourceId = this.getLinkNodeId(link.source);
|
|
2514
|
+
const targetId = this.getLinkNodeId(link.target);
|
|
2515
|
+
// Allow a slight overflow (+1) only to prevent isolated visual outliers.
|
|
2516
|
+
if ((getCount(sourceId) >= maxPerNode + 1) || (getCount(targetId) >= maxPerNode + 1))
|
|
2517
|
+
return;
|
|
2518
|
+
add(link);
|
|
2519
|
+
});
|
|
2520
|
+
});
|
|
2521
|
+
}
|
|
2522
|
+
return selected;
|
|
2523
|
+
}
|
|
2524
|
+
filterCenterItemsByThreshold(centerItems, p1Items, p2Items) {
|
|
2525
|
+
if (!centerItems?.length)
|
|
2526
|
+
return [];
|
|
2527
|
+
const threshold = ProfileComparisonLibComponent.SHARED_PROMOTION_THRESHOLD;
|
|
2528
|
+
const uniq = new Set();
|
|
2529
|
+
const selected = [];
|
|
2530
|
+
const normalize = (s) => this.normalizeSimilarityToken(s);
|
|
2531
|
+
const p1Norm = new Set((p1Items ?? []).map(normalize));
|
|
2532
|
+
const p2Norm = new Set((p2Items ?? []).map(normalize));
|
|
2533
|
+
for (const raw of centerItems) {
|
|
2534
|
+
const item = String(raw ?? '').trim();
|
|
2535
|
+
if (!item)
|
|
2536
|
+
continue;
|
|
2537
|
+
const key = normalize(item);
|
|
2538
|
+
if (!key || uniq.has(key))
|
|
2539
|
+
continue;
|
|
2540
|
+
const existsInP1 = p1Norm.has(key);
|
|
2541
|
+
const existsInP2 = p2Norm.has(key);
|
|
2542
|
+
let maxCrossSim = 0;
|
|
2543
|
+
// If the value exists on the left, compare against right-side terms.
|
|
2544
|
+
if (existsInP1 || (!existsInP1 && !existsInP2)) {
|
|
2545
|
+
for (const right of p2Items ?? []) {
|
|
2546
|
+
maxCrossSim = Math.max(maxCrossSim, this.getSimilarity(item, right));
|
|
2547
|
+
}
|
|
2548
|
+
}
|
|
2549
|
+
// If the value exists on the right, compare against left-side terms.
|
|
2550
|
+
if (existsInP2 || (!existsInP1 && !existsInP2)) {
|
|
2551
|
+
for (const left of p1Items ?? []) {
|
|
2552
|
+
maxCrossSim = Math.max(maxCrossSim, this.getSimilarity(item, left));
|
|
2553
|
+
}
|
|
2554
|
+
}
|
|
2555
|
+
if (maxCrossSim >= threshold) {
|
|
2556
|
+
selected.push(item);
|
|
2557
|
+
uniq.add(key);
|
|
2558
|
+
}
|
|
2559
|
+
}
|
|
2560
|
+
return selected;
|
|
2561
|
+
}
|
|
2562
|
+
scheduleForceGraphRebuild() {
|
|
2563
|
+
if (this.layoutRebuildRaf !== null) {
|
|
2564
|
+
cancelAnimationFrame(this.layoutRebuildRaf);
|
|
2565
|
+
this.layoutRebuildRaf = null;
|
|
2566
|
+
}
|
|
2567
|
+
this.ngZone.runOutsideAngular(() => {
|
|
2568
|
+
this.layoutRebuildRaf = requestAnimationFrame(() => {
|
|
2569
|
+
this.layoutRebuildRaf = null;
|
|
2570
|
+
this.ngZone.run(() => {
|
|
2571
|
+
if (!this.backendConfigured || !this.forceNodes.length)
|
|
2572
|
+
return;
|
|
2573
|
+
this.buildForceGraph();
|
|
2574
|
+
});
|
|
2575
|
+
});
|
|
2576
|
+
});
|
|
2577
|
+
}
|
|
2578
|
+
onWindowResize() {
|
|
2579
|
+
this.scheduleForceGraphRebuild();
|
|
2580
|
+
}
|
|
2581
|
+
buildForceGraph() {
|
|
2582
|
+
const rawP1 = this.getPerson1UniqueInterests();
|
|
2583
|
+
const rawShared = (this.centerItem || []).filter(c => c && c !== 'empty' && c !== '-' && c !== '----');
|
|
2584
|
+
const rawP2 = this.getPerson2UniqueInterests();
|
|
2585
|
+
const capped = this.selectTopSharedInterestsPerSide(rawP1, rawP2);
|
|
2586
|
+
const cappedRawP1 = capped.p1;
|
|
2587
|
+
const cappedRawP2 = capped.p2;
|
|
2588
|
+
if (cappedRawP1.length === 0 && rawShared.length === 0 && cappedRawP2.length === 0) {
|
|
2589
|
+
this.forceNodes = [];
|
|
2590
|
+
this.forceLinks = [];
|
|
2591
|
+
this.forceCanvasHeight = 180;
|
|
2592
|
+
if (this.simulation)
|
|
2593
|
+
this.simulation.stop();
|
|
2594
|
+
return;
|
|
2595
|
+
}
|
|
2596
|
+
const p1Items = this.orderBySemanticBarycenter(cappedRawP1, cappedRawP2);
|
|
2597
|
+
const p2Items = this.orderBySemanticBarycenter(cappedRawP2, cappedRawP1);
|
|
2598
|
+
const shared = this.orderSharedBySemanticPull(rawShared, p1Items, p2Items);
|
|
2599
|
+
// --- Read container width robustly ---
|
|
2600
|
+
let containerWidth = 0;
|
|
2601
|
+
try {
|
|
2602
|
+
containerWidth = this.chipsGrid?.nativeElement?.clientWidth ?? 0;
|
|
2603
|
+
}
|
|
2604
|
+
catch { /* */ }
|
|
2605
|
+
if (!containerWidth) {
|
|
2606
|
+
try {
|
|
2607
|
+
containerWidth = this.hostRef.nativeElement.getBoundingClientRect().width;
|
|
2608
|
+
}
|
|
2609
|
+
catch { /* */ }
|
|
2610
|
+
}
|
|
2611
|
+
if (!containerWidth) {
|
|
2612
|
+
try {
|
|
2613
|
+
const el = this.profileFlex?.nativeElement;
|
|
2614
|
+
containerWidth = el ? (el.clientWidth || el.getBoundingClientRect().width || 0) : 0;
|
|
2615
|
+
}
|
|
2616
|
+
catch { /* */ }
|
|
2617
|
+
}
|
|
2618
|
+
if (!containerWidth || containerWidth < 100) {
|
|
2619
|
+
this.ngZone.runOutsideAngular(() => {
|
|
2620
|
+
requestAnimationFrame(() => {
|
|
2621
|
+
this.ngZone.run(() => this.buildForceGraph());
|
|
2622
|
+
});
|
|
2623
|
+
});
|
|
2624
|
+
return;
|
|
2625
|
+
}
|
|
2626
|
+
const maxRows = Math.max(p1Items.length, shared.length, p2Items.length, 1);
|
|
2627
|
+
let availableHeight = 0;
|
|
2628
|
+
try {
|
|
2629
|
+
availableHeight = this.chipsGrid?.nativeElement?.clientHeight ?? 0;
|
|
2630
|
+
}
|
|
2631
|
+
catch { /* */ }
|
|
2632
|
+
if (!availableHeight) {
|
|
2633
|
+
try {
|
|
2634
|
+
const el = this.profileFlex?.nativeElement;
|
|
2635
|
+
const h = el ? (el.clientHeight || el.getBoundingClientRect().height || 0) : 0;
|
|
2636
|
+
availableHeight = Math.max(120, h - 90);
|
|
2637
|
+
}
|
|
2638
|
+
catch { /* */ }
|
|
2639
|
+
}
|
|
2640
|
+
if (!availableHeight || availableHeight < 120)
|
|
2641
|
+
availableHeight = 320;
|
|
2642
|
+
const baseChipH = ProfileComparisonLibComponent.CHIP_RADIUS * 2;
|
|
2643
|
+
const baseStartY = 34;
|
|
2644
|
+
const baseRowGapExtra = 14;
|
|
2645
|
+
const baseBottomPad = 30;
|
|
2646
|
+
const requiredBaseHeight = baseStartY + baseBottomPad + maxRows * baseChipH + Math.max(0, maxRows - 1) * baseRowGapExtra;
|
|
2647
|
+
const fitScale = Math.min(1, Math.max(0.35, availableHeight / Math.max(requiredBaseHeight, 1)));
|
|
2648
|
+
const chipH = Math.max(14, Math.round(baseChipH * fitScale));
|
|
2649
|
+
const chipHalfH = chipH / 2;
|
|
2650
|
+
const startY = Math.max(12, Math.round(baseStartY * fitScale));
|
|
2651
|
+
const rowGap = chipH + Math.max(5, Math.round(baseRowGapExtra * fitScale));
|
|
2652
|
+
const bottomPad = Math.max(10, Math.round(baseBottomPad * fitScale));
|
|
2653
|
+
const canvasPad = Math.max(8, Math.round(ProfileComparisonLibComponent.CANVAS_PADDING * fitScale * 0.75));
|
|
2654
|
+
// Keep rendered chip dimensions in sync with force-layout math.
|
|
2655
|
+
this.chipFontSizePx = Math.max(9.5, Number((14 * Math.max(fitScale, 0.68)).toFixed(2)));
|
|
2656
|
+
this.chipPaddingXPx = Math.max(8, Math.round(18 * Math.max(fitScale, 0.58)));
|
|
2657
|
+
this.chipPaddingYPx = Math.max(2, Math.round(6 * Math.max(fitScale, 0.5)));
|
|
2658
|
+
this.chipLineHeight = fitScale < 0.55 ? 1.15 : (fitScale < 0.8 ? 1.22 : 1.3);
|
|
2659
|
+
const approxCharWidth = this.chipFontSizePx * 0.53;
|
|
2660
|
+
const horizontalPadding = this.chipPaddingXPx * 2;
|
|
2661
|
+
const getHalfWidth = (label) => (label.length * approxCharWidth + horizontalPadding) / 2;
|
|
2662
|
+
const EDGE_PADDING = 20;
|
|
2663
|
+
const getNodeMinX = (label) => getHalfWidth(label) + EDGE_PADDING;
|
|
2664
|
+
const getNodeMaxX = (label) => containerWidth - getHalfWidth(label) - EDGE_PADDING;
|
|
2665
|
+
const maxHalfP1 = p1Items.length ? Math.max(...p1Items.map(getHalfWidth)) : chipHalfH;
|
|
2666
|
+
const maxHalfShared = shared.length ? Math.max(...shared.map(getHalfWidth)) : chipHalfH;
|
|
2667
|
+
const maxHalfP2 = p2Items.length ? Math.max(...p2Items.map(getHalfWidth)) : chipHalfH;
|
|
2668
|
+
let zoneP1 = maxHalfP1 + EDGE_PADDING;
|
|
2669
|
+
let zoneP2 = containerWidth - maxHalfP2 - EDGE_PADDING;
|
|
2670
|
+
if (zoneP2 - zoneP1 < 140) {
|
|
2671
|
+
const center = containerWidth / 2;
|
|
2672
|
+
zoneP1 = Math.max(maxHalfP1 + EDGE_PADDING, center - 70);
|
|
2673
|
+
zoneP2 = Math.min(containerWidth - maxHalfP2 - EDGE_PADDING, center + 70);
|
|
2674
|
+
}
|
|
2675
|
+
const minShared = Math.max(zoneP1 + Math.max(maxHalfP1, maxHalfShared) * 0.6, maxHalfShared + EDGE_PADDING);
|
|
2676
|
+
const maxShared = Math.min(zoneP2 - Math.max(maxHalfP2, maxHalfShared) * 0.6, containerWidth - maxHalfShared - EDGE_PADDING);
|
|
2677
|
+
const zoneShared = Math.max(minShared, Math.min(maxShared, containerWidth * 0.5));
|
|
2678
|
+
const computedMaxY = startY + chipHalfH + (maxRows - 1) * rowGap + chipHalfH + bottomPad;
|
|
2679
|
+
const safeMaxY = Math.max(startY + chipHalfH, availableHeight - chipHalfH - 8);
|
|
2680
|
+
const maxY = Math.min(computedMaxY, safeMaxY);
|
|
2681
|
+
const minCenterY = startY + chipHalfH;
|
|
2682
|
+
const verticalSpan = Math.max(0, maxY - minCenterY);
|
|
2683
|
+
const centerGuard = Math.max(12, Math.min(26, (zoneP2 - zoneP1) * 0.12));
|
|
2684
|
+
const sharedBand = Math.max(12, Math.min(22, (zoneP2 - zoneP1) * 0.09));
|
|
2685
|
+
// --- Scan matrix for max similarity to normalize spring distances ---
|
|
2686
|
+
let matrixMax = 0;
|
|
2687
|
+
if (this.matrixData?.rows) {
|
|
2688
|
+
for (const row of this.matrixData.rows) {
|
|
2689
|
+
for (const v of row.values) {
|
|
2690
|
+
const n = parseFloat(v);
|
|
2691
|
+
if (!isNaN(n) && n > matrixMax)
|
|
2692
|
+
matrixMax = n;
|
|
2693
|
+
}
|
|
2694
|
+
}
|
|
2695
|
+
}
|
|
2696
|
+
const springThreshold = ProfileComparisonLibComponent.SPRING_THRESHOLD;
|
|
2697
|
+
const crossLinkThreshold = ProfileComparisonLibComponent.CROSS_LINK_MIN_SIMILARITY;
|
|
2698
|
+
if (matrixMax <= springThreshold)
|
|
2699
|
+
matrixMax = 1.0;
|
|
2700
|
+
const simRange = matrixMax - springThreshold;
|
|
2701
|
+
const distributedYForIndex = (index, total) => {
|
|
2702
|
+
if (total <= 1)
|
|
2703
|
+
return minCenterY + verticalSpan * 0.5;
|
|
2704
|
+
const t = index / Math.max(1, total - 1);
|
|
2705
|
+
return minCenterY + verticalSpan * t;
|
|
2706
|
+
};
|
|
2707
|
+
const nodes = [];
|
|
2708
|
+
const addNodes = (items, group, zoneX) => {
|
|
2709
|
+
const total = items.length;
|
|
2710
|
+
items.forEach((label, i) => {
|
|
2711
|
+
const targetY = distributedYForIndex(i, total);
|
|
2712
|
+
nodes.push({
|
|
2713
|
+
id: `${group}-${i}-${label}`,
|
|
2714
|
+
label,
|
|
2715
|
+
group,
|
|
2716
|
+
x: zoneX,
|
|
2717
|
+
y: targetY,
|
|
2718
|
+
targetX: zoneX,
|
|
2719
|
+
targetY,
|
|
2720
|
+
});
|
|
2721
|
+
});
|
|
2722
|
+
};
|
|
2723
|
+
addNodes(p1Items, 'p1', zoneP1);
|
|
2724
|
+
addNodes(shared, 'shared', zoneShared);
|
|
2725
|
+
addNodes(p2Items, 'p2', zoneP2);
|
|
2726
|
+
// Pull very high-similarity opposite-side chips toward the shared lane.
|
|
2727
|
+
const sharedThreshold = ProfileComparisonLibComponent.SHARED_PROMOTION_THRESHOLD;
|
|
2728
|
+
const sharedRange = Math.max(0.001, 1 - sharedThreshold);
|
|
2729
|
+
const oppositePool = (node) => {
|
|
2730
|
+
if (node.group === 'p1')
|
|
2731
|
+
return nodes.filter(n => n.group === 'p2' || n.group === 'shared');
|
|
2732
|
+
if (node.group === 'p2')
|
|
2733
|
+
return nodes.filter(n => n.group === 'p1' || n.group === 'shared');
|
|
2734
|
+
return [];
|
|
2735
|
+
};
|
|
2736
|
+
nodes.forEach(node => {
|
|
2737
|
+
if (node.group === 'shared') {
|
|
2738
|
+
node.targetX = zoneShared;
|
|
2739
|
+
return;
|
|
2740
|
+
}
|
|
2741
|
+
let maxCross = 0;
|
|
2742
|
+
oppositePool(node).forEach(other => {
|
|
2743
|
+
maxCross = Math.max(maxCross, this.getSimilarity(node.label, other.label));
|
|
2744
|
+
});
|
|
2745
|
+
if (maxCross >= sharedThreshold) {
|
|
2746
|
+
const centerPull = 0.55 + Math.min(0.4, ((maxCross - sharedThreshold) / sharedRange) * 0.4);
|
|
2747
|
+
node.targetX = node.group === 'p1'
|
|
2748
|
+
? zoneP1 + (zoneShared - zoneP1) * centerPull
|
|
2749
|
+
: zoneP2 + (zoneShared - zoneP2) * centerPull;
|
|
2750
|
+
}
|
|
2751
|
+
});
|
|
2752
|
+
// Vertical placement should be driven by cross-list similarity, not by
|
|
2753
|
+
// whichever list happens to have fewer items.
|
|
2754
|
+
const semanticPool = (node) => {
|
|
2755
|
+
if (node.group === 'p1')
|
|
2756
|
+
return nodes.filter(n => n.group === 'p2' || n.group === 'shared');
|
|
2757
|
+
if (node.group === 'p2')
|
|
2758
|
+
return nodes.filter(n => n.group === 'p1' || n.group === 'shared');
|
|
2759
|
+
return nodes.filter(n => n.group === 'p1' || n.group === 'p2');
|
|
2760
|
+
};
|
|
2761
|
+
const ySimilarityFloor = Math.max(0.18, springThreshold - 0.06);
|
|
2762
|
+
for (let pass = 0; pass < 3; pass++) {
|
|
2763
|
+
const snapshotY = new Map();
|
|
2764
|
+
nodes.forEach(node => {
|
|
2765
|
+
snapshotY.set(node.id, node.targetY ?? (minCenterY + verticalSpan * 0.5));
|
|
2766
|
+
});
|
|
2767
|
+
nodes.forEach(node => {
|
|
2768
|
+
const pool = semanticPool(node);
|
|
2769
|
+
if (!pool.length)
|
|
2770
|
+
return;
|
|
2771
|
+
let weightedY = 0;
|
|
2772
|
+
let totalWeight = 0;
|
|
2773
|
+
pool.forEach(other => {
|
|
2774
|
+
const similarity = this.getSimilarity(node.label, other.label);
|
|
2775
|
+
if (similarity < ySimilarityFloor)
|
|
2776
|
+
return;
|
|
2777
|
+
const weight = Math.pow(Math.max(0.001, similarity - ySimilarityFloor + 0.05), 2);
|
|
2778
|
+
const otherY = snapshotY.get(other.id) ?? (other.targetY ?? minCenterY);
|
|
2779
|
+
weightedY += otherY * weight;
|
|
2780
|
+
totalWeight += weight;
|
|
2781
|
+
});
|
|
2782
|
+
if (totalWeight <= 0)
|
|
2783
|
+
return;
|
|
2784
|
+
const semanticY = weightedY / totalWeight;
|
|
2785
|
+
const currentY = snapshotY.get(node.id) ?? semanticY;
|
|
2786
|
+
const blend = node.group === 'shared' ? 0.72 : 0.62;
|
|
2787
|
+
node.targetY = Math.max(minCenterY, Math.min(maxY, currentY + (semanticY - currentY) * blend));
|
|
2788
|
+
});
|
|
2789
|
+
}
|
|
2790
|
+
const groupCounts = {
|
|
2791
|
+
p1: p1Items.length,
|
|
2792
|
+
shared: shared.length,
|
|
2793
|
+
p2: p2Items.length,
|
|
2794
|
+
};
|
|
2795
|
+
const fullVerticalSpan = Math.max(rowGap, maxY - minCenterY);
|
|
2796
|
+
const buildNodeBounds = (node) => {
|
|
2797
|
+
const halfW = getHalfWidth(node.label);
|
|
2798
|
+
const globalMinX = getNodeMinX(node.label);
|
|
2799
|
+
const globalMaxX = getNodeMaxX(node.label);
|
|
2800
|
+
const leftLimit = zoneShared - halfW - centerGuard;
|
|
2801
|
+
const rightLimit = zoneShared + halfW + centerGuard;
|
|
2802
|
+
let minX = globalMinX;
|
|
2803
|
+
let maxX = globalMaxX;
|
|
2804
|
+
if (node.group === 'p1') {
|
|
2805
|
+
maxX = Math.min(globalMaxX, leftLimit);
|
|
2806
|
+
if (maxX < minX)
|
|
2807
|
+
maxX = minX;
|
|
2808
|
+
}
|
|
2809
|
+
else if (node.group === 'p2') {
|
|
2810
|
+
minX = Math.max(globalMinX, rightLimit);
|
|
2811
|
+
if (minX > maxX)
|
|
2812
|
+
minX = maxX;
|
|
2813
|
+
}
|
|
2814
|
+
else {
|
|
2815
|
+
minX = Math.max(globalMinX, zoneShared - sharedBand);
|
|
2816
|
+
maxX = Math.min(globalMaxX, zoneShared + sharedBand);
|
|
2817
|
+
if (minX > maxX) {
|
|
2818
|
+
const centerX = Math.max(globalMinX, Math.min(globalMaxX, zoneShared));
|
|
2819
|
+
minX = centerX;
|
|
2820
|
+
maxX = centerX;
|
|
2821
|
+
}
|
|
2822
|
+
}
|
|
2823
|
+
const baseY = node.targetY ?? (minCenterY + fullVerticalSpan * 0.5);
|
|
2824
|
+
const groupCount = groupCounts[node.group];
|
|
2825
|
+
let ySlack = rowGap * 2.2;
|
|
2826
|
+
if (node.group === 'shared') {
|
|
2827
|
+
ySlack = Math.max(rowGap * 4.2, fullVerticalSpan * 0.58);
|
|
2828
|
+
}
|
|
2829
|
+
else {
|
|
2830
|
+
const laneSpan = groupCount > 1 ? fullVerticalSpan / (groupCount - 1) : fullVerticalSpan;
|
|
2831
|
+
ySlack = Math.min(fullVerticalSpan, Math.max(rowGap * 2.4, laneSpan * 0.95));
|
|
2832
|
+
}
|
|
2833
|
+
let minY = Math.max(minCenterY, baseY - ySlack);
|
|
2834
|
+
let maxYForNode = Math.min(maxY, baseY + ySlack);
|
|
2835
|
+
if (minY > maxYForNode) {
|
|
2836
|
+
minY = minCenterY;
|
|
2837
|
+
maxYForNode = maxY;
|
|
2838
|
+
}
|
|
2839
|
+
return { minX, maxX, minY, maxY: maxYForNode };
|
|
2840
|
+
};
|
|
2841
|
+
const nodeBounds = new Map();
|
|
2842
|
+
nodes.forEach(node => nodeBounds.set(node.id, buildNodeBounds(node)));
|
|
2843
|
+
this.nodeDragBounds = new Map(nodeBounds);
|
|
2844
|
+
const clampNodeToBounds = (node) => {
|
|
2845
|
+
const bounds = nodeBounds.get(node.id) ?? buildNodeBounds(node);
|
|
2846
|
+
node.x = Math.max(bounds.minX, Math.min(bounds.maxX, node.x ?? (node.targetX ?? zoneShared)));
|
|
2847
|
+
node.y = Math.max(bounds.minY, Math.min(bounds.maxY, node.y ?? (node.targetY ?? minCenterY)));
|
|
2848
|
+
};
|
|
2849
|
+
const crossCandidates = [];
|
|
2850
|
+
const intraCandidates = [];
|
|
2851
|
+
const MIN_DIST = 62;
|
|
2852
|
+
const SPAN = 92;
|
|
2853
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
2854
|
+
for (let j = i + 1; j < nodes.length; j++) {
|
|
2855
|
+
if (!this.canLinkNodes(nodes[i], nodes[j]))
|
|
2856
|
+
continue;
|
|
2857
|
+
const similarity = this.getSimilarity(nodes[i].label, nodes[j].label);
|
|
2858
|
+
if (similarity < crossLinkThreshold)
|
|
2859
|
+
continue;
|
|
2860
|
+
const normSim = Math.min(1, (similarity - springThreshold) / simRange);
|
|
2861
|
+
const distance = MIN_DIST + (1 - normSim) * SPAN;
|
|
2862
|
+
crossCandidates.push({
|
|
2863
|
+
kind: 'cross',
|
|
2864
|
+
source: nodes[i],
|
|
2865
|
+
target: nodes[j],
|
|
2866
|
+
strength: 0.32 + normSim * 0.68,
|
|
2867
|
+
similarity,
|
|
2868
|
+
distance,
|
|
2869
|
+
});
|
|
2870
|
+
}
|
|
2871
|
+
}
|
|
2872
|
+
const intraThreshold = ProfileComparisonLibComponent.INTRA_GROUP_THRESHOLD;
|
|
2873
|
+
const intraRange = Math.max(0.001, matrixMax - intraThreshold);
|
|
2874
|
+
['p1', 'p2'].forEach(group => {
|
|
2875
|
+
const groupNodes = nodes.filter(n => n.group === group);
|
|
2876
|
+
for (let i = 0; i < groupNodes.length; i++) {
|
|
2877
|
+
for (let j = i + 1; j < groupNodes.length; j++) {
|
|
2878
|
+
const similarity = this.getSimilarity(groupNodes[i].label, groupNodes[j].label);
|
|
2879
|
+
if (similarity <= intraThreshold)
|
|
2880
|
+
continue;
|
|
2881
|
+
const normSim = Math.min(1, (similarity - intraThreshold) / intraRange);
|
|
2882
|
+
intraCandidates.push({
|
|
2883
|
+
kind: 'intra',
|
|
2884
|
+
source: groupNodes[i],
|
|
2885
|
+
target: groupNodes[j],
|
|
2886
|
+
strength: 0.08 + normSim * 0.2,
|
|
2887
|
+
similarity,
|
|
2888
|
+
distance: 50 + (1 - normSim) * 42,
|
|
2889
|
+
});
|
|
2890
|
+
}
|
|
2891
|
+
}
|
|
2892
|
+
});
|
|
2893
|
+
const crossLinks = this.selectLinksWithCaps(crossCandidates, ProfileComparisonLibComponent.MAX_CROSS_LINKS_PER_NODE, ProfileComparisonLibComponent.MIN_CROSS_LINKS_PER_NODE);
|
|
2894
|
+
// Shared chips can otherwise become vertical outliers if greedy caps saturate
|
|
2895
|
+
// their best opposite-side neighbors too early. Ensure a minimum local anchor set.
|
|
2896
|
+
const crossCounts = new Map();
|
|
2897
|
+
const crossKeys = new Set();
|
|
2898
|
+
const countCross = (id) => crossCounts.get(id) ?? 0;
|
|
2899
|
+
const bumpCross = (id) => {
|
|
2900
|
+
crossCounts.set(id, countCross(id) + 1);
|
|
2901
|
+
};
|
|
2902
|
+
const recordCross = (link) => {
|
|
2903
|
+
const sourceId = this.getLinkNodeId(link.source);
|
|
2904
|
+
const targetId = this.getLinkNodeId(link.target);
|
|
2905
|
+
const key = this.getLinkPairKey(sourceId, targetId);
|
|
2906
|
+
if (crossKeys.has(key))
|
|
2907
|
+
return;
|
|
2908
|
+
crossKeys.add(key);
|
|
2909
|
+
crossLinks.push(link);
|
|
2910
|
+
bumpCross(sourceId);
|
|
2911
|
+
bumpCross(targetId);
|
|
2912
|
+
};
|
|
2913
|
+
crossLinks.forEach(link => {
|
|
2914
|
+
const sourceId = this.getLinkNodeId(link.source);
|
|
2915
|
+
const targetId = this.getLinkNodeId(link.target);
|
|
2916
|
+
const key = this.getLinkPairKey(sourceId, targetId);
|
|
2917
|
+
crossKeys.add(key);
|
|
2918
|
+
bumpCross(sourceId);
|
|
2919
|
+
bumpCross(targetId);
|
|
2920
|
+
});
|
|
2921
|
+
if (ProfileComparisonLibComponent.MIN_CROSS_LINKS_PER_NODE > 0) {
|
|
2922
|
+
const sharedNodesForAnchors = nodes.filter(n => n.group === 'shared');
|
|
2923
|
+
sharedNodesForAnchors.forEach(sharedNode => {
|
|
2924
|
+
const sharedId = sharedNode.id;
|
|
2925
|
+
const incidentCandidates = crossCandidates
|
|
2926
|
+
.filter(link => {
|
|
2927
|
+
const sourceId = this.getLinkNodeId(link.source);
|
|
2928
|
+
const targetId = this.getLinkNodeId(link.target);
|
|
2929
|
+
return sourceId === sharedId || targetId === sharedId;
|
|
2930
|
+
})
|
|
2931
|
+
.sort((a, b) => (b.similarity - a.similarity) || (b.strength - a.strength));
|
|
2932
|
+
const requiredAnchors = Math.min(3, incidentCandidates.length);
|
|
2933
|
+
for (const candidate of incidentCandidates) {
|
|
2934
|
+
if (countCross(sharedId) >= requiredAnchors)
|
|
2935
|
+
break;
|
|
2936
|
+
const sourceId = this.getLinkNodeId(candidate.source);
|
|
2937
|
+
const targetId = this.getLinkNodeId(candidate.target);
|
|
2938
|
+
const key = this.getLinkPairKey(sourceId, targetId);
|
|
2939
|
+
if (crossKeys.has(key))
|
|
2940
|
+
continue;
|
|
2941
|
+
const otherId = sourceId === sharedId ? targetId : sourceId;
|
|
2942
|
+
if (countCross(otherId) > ProfileComparisonLibComponent.MAX_CROSS_LINKS_PER_NODE + 1)
|
|
2943
|
+
continue;
|
|
2944
|
+
recordCross(candidate);
|
|
2945
|
+
}
|
|
2946
|
+
});
|
|
2947
|
+
}
|
|
2948
|
+
const intraLinks = this.selectLinksWithCaps(intraCandidates, ProfileComparisonLibComponent.MAX_INTRA_LINKS_PER_NODE);
|
|
2949
|
+
const links = [...crossLinks, ...intraLinks];
|
|
2950
|
+
const nodeById = new Map(nodes.map(n => [n.id, n]));
|
|
2951
|
+
const crossAdjacency = new Map();
|
|
2952
|
+
const addAdjacency = (source, target, similarity) => {
|
|
2953
|
+
const bucket = crossAdjacency.get(source.id) ?? [];
|
|
2954
|
+
bucket.push({ node: target, similarity });
|
|
2955
|
+
crossAdjacency.set(source.id, bucket);
|
|
2956
|
+
};
|
|
2957
|
+
crossLinks.forEach(link => {
|
|
2958
|
+
const source = typeof link.source === 'object' ? link.source : nodeById.get(String(link.source));
|
|
2959
|
+
const target = typeof link.target === 'object' ? link.target : nodeById.get(String(link.target));
|
|
2960
|
+
if (!source || !target)
|
|
2961
|
+
return;
|
|
2962
|
+
addAdjacency(source, target, link.similarity);
|
|
2963
|
+
addAdjacency(target, source, link.similarity);
|
|
2964
|
+
});
|
|
2965
|
+
const crossAlignForce = (alpha) => {
|
|
2966
|
+
crossAdjacency.forEach((neighbors, nodeId) => {
|
|
2967
|
+
const node = nodeById.get(nodeId);
|
|
2968
|
+
if (!node || !neighbors.length)
|
|
2969
|
+
return;
|
|
2970
|
+
let weightedY = 0;
|
|
2971
|
+
let totalWeight = 0;
|
|
2972
|
+
neighbors.forEach(({ node: other, similarity }) => {
|
|
2973
|
+
const weight = Math.max(0.05, similarity);
|
|
2974
|
+
weightedY += (other.y ?? node.y ?? 0) * weight;
|
|
2975
|
+
totalWeight += weight;
|
|
2976
|
+
});
|
|
2977
|
+
if (totalWeight <= 0)
|
|
2978
|
+
return;
|
|
2979
|
+
const targetY = weightedY / totalWeight;
|
|
2980
|
+
const dy = targetY - (node.y ?? targetY);
|
|
2981
|
+
const alignStrength = node.group === 'shared'
|
|
2982
|
+
? (neighbors.length <= 2 ? 0.62 : 0.52)
|
|
2983
|
+
: 0.36;
|
|
2984
|
+
node.vy = (node.vy ?? 0) + dy * alignStrength * alpha;
|
|
2985
|
+
});
|
|
2986
|
+
};
|
|
2987
|
+
this.ngZone.runOutsideAngular(() => {
|
|
2988
|
+
const sim = forceSimulation(nodes)
|
|
2989
|
+
.alphaDecay(0.02)
|
|
2990
|
+
.stop()
|
|
2991
|
+
.force('link', forceLink(links)
|
|
2992
|
+
.id(d => d.id)
|
|
2993
|
+
.distance(d => d.distance)
|
|
2994
|
+
.strength(d => d.kind === 'cross' ? d.strength * 0.9 : d.strength * 0.65))
|
|
2995
|
+
.force('collide', forceCollide((d) => {
|
|
2996
|
+
const baseRadius = getHalfWidth(d.label) * 0.55 + ProfileComparisonLibComponent.CHIP_COLLISION_BORDER_PADDING;
|
|
2997
|
+
return Math.min(58, Math.max(ProfileComparisonLibComponent.CHIP_RADIUS + 8, baseRadius));
|
|
2998
|
+
})
|
|
2999
|
+
.strength(1)
|
|
3000
|
+
.iterations(2))
|
|
3001
|
+
.force('xBound', forceX(d => d.targetX ?? zoneShared)
|
|
3002
|
+
.strength(d => d.group === 'shared' ? 1.15 : 0.85))
|
|
3003
|
+
.force('yTargetSeed', forceY(d => d.targetY ?? minCenterY).strength(0.34))
|
|
3004
|
+
.force('crossAlign', crossAlignForce);
|
|
3005
|
+
for (let i = 0; i < 420 && sim.alpha() > 0.0035; i++) {
|
|
3006
|
+
sim.tick();
|
|
3007
|
+
nodes.forEach(n => {
|
|
3008
|
+
clampNodeToBounds(n);
|
|
3009
|
+
});
|
|
3010
|
+
}
|
|
3011
|
+
const enforceGroupStack = (group) => {
|
|
3012
|
+
const groupNodes = nodes
|
|
3013
|
+
.filter(n => n.group === group)
|
|
3014
|
+
.sort((a, b) => (a.y ?? minCenterY) - (b.y ?? minCenterY));
|
|
3015
|
+
for (let i = 1; i < groupNodes.length; i++) {
|
|
3016
|
+
const prevY = groupNodes[i - 1].y ?? minCenterY;
|
|
3017
|
+
const minAllowed = prevY + rowGap * 0.92;
|
|
3018
|
+
if ((groupNodes[i].y ?? minCenterY) < minAllowed) {
|
|
3019
|
+
groupNodes[i].y = minAllowed;
|
|
3020
|
+
}
|
|
3021
|
+
}
|
|
3022
|
+
};
|
|
3023
|
+
enforceGroupStack('p1');
|
|
3024
|
+
enforceGroupStack('shared');
|
|
3025
|
+
enforceGroupStack('p2');
|
|
3026
|
+
nodes.forEach(n => {
|
|
3027
|
+
clampNodeToBounds(n);
|
|
3028
|
+
n.originalFx = n.x;
|
|
3029
|
+
n.originalFy = n.y;
|
|
3030
|
+
});
|
|
3031
|
+
sim.force('xBound', null);
|
|
3032
|
+
sim.force('yTargetSeed', null);
|
|
3033
|
+
sim.force('crossAlign', null);
|
|
3034
|
+
sim.force('xTarget', forceX(d => d.originalFx ?? d.x ?? 0)
|
|
3035
|
+
.strength(d => d.group === 'shared' ? 0.16 : 0.075));
|
|
3036
|
+
sim.force('yTarget', forceY(d => d.originalFy ?? d.y ?? 0).strength(0.085));
|
|
3037
|
+
this.simulation = sim;
|
|
3038
|
+
this.ngZone.run(() => {
|
|
3039
|
+
this.forceNodes = [...nodes];
|
|
3040
|
+
this.forceLinks = links;
|
|
3041
|
+
const maxBottom = Math.max(...nodes.map(n => (n.y ?? 0) + chipHalfH));
|
|
3042
|
+
this.forceCanvasHeight = Math.ceil(Math.min(maxBottom + canvasPad, availableHeight));
|
|
3043
|
+
this.cdr.detectChanges();
|
|
3044
|
+
});
|
|
3045
|
+
sim.on('tick', () => {
|
|
3046
|
+
this.ngZone.run(() => {
|
|
3047
|
+
nodes.forEach(n => {
|
|
3048
|
+
clampNodeToBounds(n);
|
|
3049
|
+
});
|
|
3050
|
+
this.forceNodes = [...nodes];
|
|
3051
|
+
const maxBottom = Math.max(...nodes.map(n => (n.y ?? 0) + chipHalfH));
|
|
3052
|
+
this.forceCanvasHeight = Math.ceil(Math.min(maxBottom + canvasPad, availableHeight));
|
|
3053
|
+
this.cdr.detectChanges();
|
|
3054
|
+
});
|
|
3055
|
+
});
|
|
3056
|
+
});
|
|
3057
|
+
}
|
|
3058
|
+
selectTopSharedInterestsPerSide(person1Items, person2Items) {
|
|
3059
|
+
const limit = ProfileComparisonLibComponent.MAX_RELEVANT_INTERESTS_PER_SIDE;
|
|
3060
|
+
const sanitize = (items) => {
|
|
3061
|
+
const seen = new Set();
|
|
3062
|
+
const cleaned = [];
|
|
3063
|
+
(items || []).forEach(item => {
|
|
3064
|
+
const value = String(item ?? '').trim();
|
|
3065
|
+
if (!value)
|
|
3066
|
+
return;
|
|
3067
|
+
const key = value.toLowerCase();
|
|
3068
|
+
if (seen.has(key))
|
|
3069
|
+
return;
|
|
3070
|
+
seen.add(key);
|
|
3071
|
+
cleaned.push(value);
|
|
3072
|
+
});
|
|
3073
|
+
return cleaned;
|
|
3074
|
+
};
|
|
3075
|
+
const p1 = sanitize(person1Items);
|
|
3076
|
+
const p2 = sanitize(person2Items);
|
|
3077
|
+
if (limit <= 0)
|
|
3078
|
+
return { p1: [], p2: [] };
|
|
3079
|
+
if (!p1.length || !p2.length) {
|
|
3080
|
+
return {
|
|
3081
|
+
p1: p1.slice(0, limit),
|
|
3082
|
+
p2: p2.slice(0, limit),
|
|
3083
|
+
};
|
|
3084
|
+
}
|
|
3085
|
+
const pairs = [];
|
|
3086
|
+
p1.forEach(item1 => {
|
|
3087
|
+
p2.forEach(item2 => {
|
|
3088
|
+
pairs.push({ p1: item1, p2: item2, similarity: this.getSimilarity(item1, item2) });
|
|
3089
|
+
});
|
|
3090
|
+
});
|
|
3091
|
+
pairs.sort((a, b) => b.similarity - a.similarity);
|
|
3092
|
+
const pickedP1 = new Set();
|
|
3093
|
+
const pickedP2 = new Set();
|
|
3094
|
+
const orderedP1 = [];
|
|
3095
|
+
const orderedP2 = [];
|
|
3096
|
+
for (const pair of pairs) {
|
|
3097
|
+
if (orderedP1.length >= limit && orderedP2.length >= limit)
|
|
3098
|
+
break;
|
|
3099
|
+
const canAddP1 = !pickedP1.has(pair.p1) && orderedP1.length < limit;
|
|
3100
|
+
const canAddP2 = !pickedP2.has(pair.p2) && orderedP2.length < limit;
|
|
3101
|
+
if (!canAddP1 && !canAddP2)
|
|
3102
|
+
continue;
|
|
3103
|
+
if (canAddP1) {
|
|
3104
|
+
pickedP1.add(pair.p1);
|
|
3105
|
+
orderedP1.push(pair.p1);
|
|
3106
|
+
}
|
|
3107
|
+
if (canAddP2) {
|
|
3108
|
+
pickedP2.add(pair.p2);
|
|
3109
|
+
orderedP2.push(pair.p2);
|
|
3110
|
+
}
|
|
3111
|
+
}
|
|
3112
|
+
const rankRemaining = (items, opposite, selected) => {
|
|
3113
|
+
return items
|
|
3114
|
+
.filter(item => !selected.has(item))
|
|
3115
|
+
.map((item, index) => ({
|
|
3116
|
+
item,
|
|
3117
|
+
index,
|
|
3118
|
+
score: Math.max(...opposite.map(other => this.getSimilarity(item, other)), 0),
|
|
3119
|
+
}))
|
|
3120
|
+
.sort((a, b) => (b.score - a.score) || (a.index - b.index))
|
|
3121
|
+
.map(entry => entry.item);
|
|
3122
|
+
};
|
|
3123
|
+
rankRemaining(p1, p2, pickedP1).forEach(item => {
|
|
3124
|
+
if (orderedP1.length >= limit)
|
|
3125
|
+
return;
|
|
3126
|
+
pickedP1.add(item);
|
|
3127
|
+
orderedP1.push(item);
|
|
3128
|
+
});
|
|
3129
|
+
rankRemaining(p2, p1, pickedP2).forEach(item => {
|
|
3130
|
+
if (orderedP2.length >= limit)
|
|
3131
|
+
return;
|
|
3132
|
+
pickedP2.add(item);
|
|
3133
|
+
orderedP2.push(item);
|
|
3134
|
+
});
|
|
3135
|
+
return {
|
|
3136
|
+
p1: orderedP1.slice(0, limit),
|
|
3137
|
+
p2: orderedP2.slice(0, limit),
|
|
3138
|
+
};
|
|
2272
3139
|
}
|
|
2273
3140
|
reorderInterestsByMatrix(p1List, p2List, centerList) {
|
|
2274
3141
|
if (!this.matrixData)
|
|
@@ -2572,13 +3439,292 @@ class ProfileComparisonLibComponent {
|
|
|
2572
3439
|
isValidFaceData(face) {
|
|
2573
3440
|
return !!face && typeof face.x === 'number' && face.width > 0 && face.height > 0;
|
|
2574
3441
|
}
|
|
2575
|
-
|
|
2576
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.25", type: ProfileComparisonLibComponent, isStandalone: false, selector: "lib-profile-comparison", inputs: { config: "config", user1File: "user1File", user2File: "user2File", backendMode: "backendMode", backendUrl: "backendUrl", fadeAllEdges: "fadeAllEdges", person1Name: "person1Name", person2Name: "person2Name", imageProxyPath: "imageProxyPath" }, outputs: { matrixDataChange: "matrixDataChange", rawLLMOutputChange: "rawLLMOutputChange", viewProfileClick: "viewProfileClick" }, viewQueries: [{ propertyName: "leftContainer", first: true, predicate: ["leftContainer"], descendants: true }, { propertyName: "rightContainer", first: true, predicate: ["rightContainer"], descendants: true }, { propertyName: "profileFlex", first: true, predicate: ["profileFlex"], descendants: true }, { propertyName: "profileImgLeft", first: true, predicate: ["profileImgLeft"], descendants: true }, { propertyName: "profileImgRight", first: true, predicate: ["profileImgRight"], descendants: true }, { propertyName: "shapeContainer", first: true, predicate: ["shapeContainer"], descendants: true }, { propertyName: "shapeBg", first: true, predicate: ["shapeBg"], descendants: true }, { propertyName: "shapeBg1", first: true, predicate: ["shapeBg1"], descendants: true }, { propertyName: "shapeBg2", first: true, predicate: ["shapeBg2"], descendants: true }, { propertyName: "shapeTextLeft", first: true, predicate: ["shapeTextLeft"], descendants: true }, { propertyName: "shapeTextRight", first: true, predicate: ["shapeTextRight"], descendants: true }, { propertyName: "shapeTextCenter", first: true, predicate: ["shapeTextCenter"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "<div class=\"configure-backend-message\" *ngIf=\"!backendConfigured\">\n Configure backend\n</div>\n\n<div class=\"profile-screen\" *ngIf=\"backendConfigured\">\n <div #profileFlex class=\"profile-flex\" [class.fade-all]=\"fadeAllEdges\">\n <svg class=\"image-noise-overlay\" style=\"position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; overflow: hidden; z-index: 0;\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 320 320\" fill=\"none\" preserveAspectRatio=\"none\">\n <g filter=\"url(#filter0_n_1_1753)\">\n <rect width=\"100%\" height=\"100%\" fill=\"url(#paint0_radial_1_1753)\" fill-opacity=\"0.33\"/>\n </g>\n <defs>\n <filter id=\"filter0_n_1_1753\" x=\"0%\" y=\"0%\" width=\"100%\" height=\"100%\" filterUnits=\"objectBoundingBox\" color-interpolation-filters=\"sRGB\">\n <feFlood flood-opacity=\"0\" result=\"BackgroundImageFix\"/>\n <feBlend mode=\"normal\" in=\"SourceGraphic\" in2=\"BackgroundImageFix\" result=\"shape\"/>\n <feTurbulence type=\"fractalNoise\" baseFrequency=\"0.10869565606117249 0.10869565606117249\" stitchTiles=\"stitch\" numOctaves=\"3\" result=\"noise\" seed=\"7339\"/>\n <feColorMatrix in=\"noise\" type=\"luminanceToAlpha\" result=\"alphaNoise\"/>\n <feComponentTransfer in=\"alphaNoise\" result=\"coloredNoise1\">\n <feFuncA type=\"discrete\" tableValues=\"0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 \"/>\n </feComponentTransfer>\n <feComposite operator=\"in\" in2=\"shape\" in=\"coloredNoise1\" result=\"noise1Clipped\"/>\n <feFlood flood-color=\"rgba(0, 0, 0, 0.25)\" result=\"color1Flood\"/>\n <feComposite operator=\"in\" in2=\"noise1Clipped\" in=\"color1Flood\" result=\"color1\"/>\n <feMerge result=\"effect1_noise_1_1753\">\n <feMergeNode in=\"shape\"/>\n <feMergeNode in=\"color1\"/>\n </feMerge>\n </filter>\n <radialGradient id=\"paint0_radial_1_1753\" cx=\"50%\" cy=\"50%\" r=\"50%\" fx=\"50%\" fy=\"50%\">\n <stop offset=\"0%\" stop-opacity=\"0\"/>\n <stop offset=\"100%\" stop-opacity=\"0.25\"/>\n </radialGradient>\n </defs>\n </svg>\n <div #profileImgLeft class=\"profile-img left\" [class.fade-all]=\"fadeAllEdges\">\n <img\n [src]=\"user1Image\"\n alt=\"User 1\"\n [style.transform]=\"user1Transform\"\n [style.top]=\"user1Top\"\n [style.height]=\"user1Height\"\n [style.object-position]=\"user1ObjectPosition\"\n (load)=\"onUserImageLoad(1, $event)\"\n />\n </div>\n <div #profileImgRight class=\"profile-img right\" [class.fade-all]=\"fadeAllEdges\">\n <img\n [src]=\"user2Image\"\n alt=\"User 2\"\n [style.transform]=\"user2Transform\"\n [style.top]=\"user2Top\"\n [style.height]=\"user2Height\"\n [style.object-position]=\"user2ObjectPosition\"\n (load)=\"onUserImageLoad(2, $event)\"\n />\n </div>\n <div class=\"image-shade-overlay\"></div>\n\n <!-- Frosted Glass Overlay Layer -->\n <div class=\"overlay-container\">\n <!-- Top Headers -->\n <div class=\"header-label left\">{{ person1Name }}</div>\n <div class=\"header-label center\">Shared</div>\n <div class=\"header-label right\">{{ person2Name }}</div>\n\n <!-- Chips Columns Grid -->\n <div class=\"chips-grid\">\n <!-- Person 1 Unique Interests -->\n <div class=\"chips-column left\">\n <ng-container *ngFor=\"let interest of getPerson1UniqueInterests(); let i = index\">\n <div class=\"frosted-chip person-a\" [style.margin-right.px]=\"getChipOffset(i, 'left')\" [style.margin-top.px]=\"(i > 0 ? 4 : 0)\">\n <span class=\"chip-label\">{{ interest }}</span>\n </div>\n </ng-container>\n </div>\n\n <!-- Shared Interests (Center Column) -->\n <div class=\"chips-column center\">\n <ng-container *ngFor=\"let interest of centerItem; let i = index\">\n <div *ngIf=\"interest !== 'empty'\" class=\"frosted-chip shared\" [style.margin-left.px]=\"getChipOffset(i, 'center')\" [style.margin-top.px]=\"(i > 0 ? 4 : 0)\">\n <span class=\"chip-label\">{{ interest }}</span>\n </div>\n <div *ngIf=\"interest === 'empty'\" class=\"frosted-chip ghost-chip shared\" [style.margin-left.px]=\"getChipOffset(i, 'center')\" [style.margin-top.px]=\"(i > 0 ? 4 : 0)\">\n <span class=\"chip-label\"> </span>\n </div>\n </ng-container>\n </div>\n\n <!-- Person 2 Unique Interests -->\n <div class=\"chips-column right\">\n <ng-container *ngFor=\"let interest of getPerson2UniqueInterests(); let i = index\">\n <div class=\"frosted-chip person-b\" [style.margin-left.px]=\"getChipOffset(i, 'right')\" [style.margin-top.px]=\"(i > 0 ? 4 : 0)\">\n <span class=\"chip-label\">{{ interest }}</span>\n </div>\n </ng-container>\n </div>\n </div>\n\n <!-- Bottom Actions -->\n <div class=\"view-profile left\" (click)=\"onViewProfile('left')\">View Profile</div>\n <div class=\"view-profile right\" (click)=\"onViewProfile('right')\">View Profile</div>\n </div>\n\n <!-- Loading indicator for alignment process -->\n <div *ngIf=\"isAligning\" class=\"loading-indicator\">\n <div class=\"loading-spinner\"></div>\n </div>\n</div>\n\n\n", styles: [":host{display:block;width:100%;max-width:100%;overflow-x:hidden!important;box-sizing:border-box!important}.profile-img.left{position:absolute;top:0;bottom:0;left:0;width:58%;height:100%;margin:0;z-index:1;-webkit-mask-image:linear-gradient(to right,black 0%,black 45%,transparent 92%);mask-image:linear-gradient(to right,black 0%,black 45%,transparent 92%)}.profile-img.right{position:absolute;inset:0 0 0 auto;width:58%;height:100%;margin:0;z-index:2;-webkit-mask-image:linear-gradient(to right,transparent 8%,black 55%,black 100%);mask-image:linear-gradient(to right,transparent 8%,black 55%,black 100%)}@media(min-width:768px){.profile-img.left{width:65%;-webkit-mask-image:linear-gradient(to right,black 0%,black 40%,transparent 90%);mask-image:linear-gradient(to right,black 0%,black 40%,transparent 90%)}.profile-img.right{width:65%;-webkit-mask-image:linear-gradient(to right,transparent 10%,black 60%,black 100%);mask-image:linear-gradient(to right,transparent 10%,black 60%,black 100%)}}.configure-backend-message{padding:1rem;text-align:center;color:#bdc3c7;background:#34495e;border-radius:8px}.backend-error-message{padding:.75rem 1rem;text-align:center;color:#fef3c7;background:#b4530940;border:1px solid rgba(245,158,11,.5);border-radius:8px;margin-bottom:.5rem}.profile-screen{width:100%;max-width:768px;margin:0 auto;overflow:hidden;position:relative;border-radius:12px}.profile-flex{display:flex;width:100%;height:440px;overflow:hidden;background:transparent;gap:0;margin:0;padding:0;position:relative;box-shadow:none;border:none;--overlap-size: 40px}.profile-flex.fade-all{-webkit-mask-image:linear-gradient(to right,transparent 0%,black 18%,black 82%,transparent 100%),linear-gradient(to bottom,transparent 0%,black 15%,black 85%,transparent 100%);-webkit-mask-composite:intersect;mask-image:linear-gradient(to right,transparent 0%,black 18%,black 82%,transparent 100%),linear-gradient(to bottom,transparent 0%,black 15%,black 85%,transparent 100%);mask-composite:intersect}.profile-flex .image-shade-overlay{position:absolute;inset:0;width:100%;height:100%;pointer-events:none;z-index:3;background:radial-gradient(ellipse at center,transparent 40%,rgba(41,37,45,.25) 100%)}.profile-flex .image-noise-overlay{position:absolute;inset:0;width:100%;height:100%;max-width:100%;max-height:100%;pointer-events:none;overflow:hidden;z-index:0}.shape-bg1,.shape-bg2{position:absolute;max-width:250px;opacity:1;transition:transform .2s ease-out}.shape-bg1{z-index:1;left:0}.shape-bg2{right:0}.shape-bg.fade-all .shape-bg1{-webkit-mask-image:linear-gradient(to right,transparent 0%,black 20%,black 100%);mask-image:linear-gradient(to right,transparent 0%,black 20%,black 100%)}.shape-bg.fade-all .shape-bg2{-webkit-mask-image:linear-gradient(to left,transparent 0%,black 20%,black 100%);mask-image:linear-gradient(to left,transparent 0%,black 20%,black 100%)}.shape-bg svg{cursor:grab}.shape-bg svg:active{cursor:grabbing}.profile-img{height:100%;position:relative;flex-shrink:0;background:transparent;margin:0;padding:0;left:0;top:0;border:none;box-sizing:border-box;overflow:hidden;box-shadow:none;outline:none}.profile-img img{transition:transform .3s ease;width:100%;height:100%;object-fit:cover;display:block;object-position:center center;opacity:1;transform-origin:center center;border:none;outline:none;box-shadow:none;position:relative;top:0;left:0;filter:contrast(1.2) brightness(1.1) saturate(1.1);image-rendering:auto}.overlay-container{position:absolute;inset:0;width:100%;height:100%;pointer-events:none;z-index:10;-webkit-user-select:none;user-select:none;overflow:hidden;background:#29252da6;backdrop-filter:blur(.5px);-webkit-backdrop-filter:blur(.5px)}.header-label{position:absolute;top:20px;font-family:Gilroy,sans-serif;text-shadow:0 1px 8px rgba(0,0,0,.8);pointer-events:none}.header-label.left{left:5%;transform:none;font-size:14px;font-weight:400;color:#fffffff2}.header-label.center{left:50%;transform:translate(-50%);font-size:12px;font-weight:400;color:#fffc;letter-spacing:0}.header-label.right{right:5%;left:auto;transform:none;font-size:14px;font-weight:400;color:#fffffff2}.chips-grid{position:absolute;inset:45px 0;display:flex;padding:0;overflow-y:auto;overscroll-behavior:none;scrollbar-width:none;box-sizing:border-box}.chips-grid::-webkit-scrollbar{display:none}.chips-column{flex:1;display:flex;flex-direction:column;gap:8px;padding:4px 10px;box-sizing:border-box}.chips-column.left{align-items:flex-end}.chips-column.center{align-items:center}.chips-column.right{align-items:flex-start}.frosted-chip{position:relative;cursor:pointer;pointer-events:auto;transition:transform .2s ease,box-shadow .2s ease,background .2s ease;flex-shrink:0;padding:6px 18px;border-radius:999px;display:inline-flex;align-items:center;justify-content:center;border:none;background:#ffffff30;box-shadow:inset 0 1.5px 2px #ffffff59;backdrop-filter:blur(.5px);-webkit-backdrop-filter:blur(.5px)}.frosted-chip:hover{transform:scale(1.04);background:#ffffff29}.frosted-chip .chip-label{position:relative;color:#a8a8a8;text-align:center;font-family:Gilroy,sans-serif;font-size:14px;font-style:normal;font-weight:400;line-height:140%;letter-spacing:-.01em;text-shadow:none;white-space:nowrap;pointer-events:none;z-index:1}.ghost-chip{visibility:hidden!important;pointer-events:none!important}.view-profile{position:absolute;bottom:20px!important;color:#ffffff80;text-shadow:0 2px 4.2px #000;font-family:Calistoga,serif;font-size:18px;font-style:normal;font-weight:400;line-height:normal;cursor:pointer;pointer-events:auto;transition:opacity .2s ease,transform .2s ease}.view-profile:hover{opacity:.85}.view-profile.left{left:5%;transform:none}.view-profile.left:hover{transform:translateY(-2px)}.view-profile.right{right:5%;left:auto;transform:none}.view-profile.right:hover{transform:translateY(-2px)}#drag-preview{position:absolute;pointer-events:none;display:none;background:#000000b3;color:#fff;padding:5px 10px;border-radius:5px;z-index:9999;max-width:200px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.shape-text-container{width:70px;position:relative;overflow:hidden}.draggable{width:70px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;cursor:grab;-webkit-user-select:none;user-select:none;position:relative;display:inline-block}.dragging{width:auto;text-overflow:clip;cursor:grabbing;position:absolute;z-index:1000}.p-wrapper{width:70px;overflow-x:hidden}.shape-text-center{display:flex;justify-content:space-between;flex-direction:column;gap:10px;height:100%;margin-top:55px}.loading-indicator{position:absolute;inset:0;width:100%;height:100%;background:#00000040;backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px);color:#fff;z-index:100;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12px;pointer-events:auto}.loading-spinner{width:44px;height:44px;border:3.5px solid rgba(255,255,255,.25);border-top:3.5px solid #ffffff;border-radius:50%;animation:spin .8s linear infinite;box-shadow:0 0 15px #fff3}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.loading-indicator p{margin:0;font-size:14px;font-weight:500;color:#ffffffd9;letter-spacing:.02em}.api-key-modal-overlay{position:fixed;inset:0;background:#0f172a99;-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px);display:flex;align-items:center;justify-content:center;z-index:2000}.api-key-modal{width:min(640px,92vw);background:#0f172a;color:#e2e8f0;border:1px solid #334155;border-radius:12px;box-shadow:0 20px 50px #00000073;padding:16px 18px}.api-key-modal-header{display:flex;align-items:center;justify-content:space-between;gap:12px}.api-key-modal-header h2{margin:0;font-size:20px;font-weight:700;color:#e2e8f0}.warning-icon{color:#fbbf24}.api-key-modal .close-btn{background:#0b1220;border:1px solid #334155;color:#94a3b8;border-radius:8px;padding:6px;cursor:pointer;line-height:0}.api-key-modal-body{margin-top:12px;display:flex;flex-direction:column;gap:10px}.modal-message,.modal-instruction{margin:0;color:#cbd5e1}.modal-message strong{color:#e5e7eb}.api-key-input-group{display:flex;align-items:center;gap:10px}.api-key-input{flex:1 1 auto;background:#0a1020;color:#e2e8f0;border:1px solid #334155;border-radius:8px;padding:10px 12px}.api-key-input:focus{outline:none;border-color:#2563eb;box-shadow:0 0 0 3px #2563eb33}.api-key-load-btn{background:#2563eb;color:#fff;border:none;border-radius:8px;padding:10px 12px;cursor:pointer;font-weight:600}.api-key-load-btn:hover{background:#1d4ed8}.modal-hint{display:flex;align-items:center;gap:8px;color:#94a3b8}.modal-hint a{color:#93c5fd}@media(max-width:1023px){.profile-screen{max-width:640px}.profile-flex{height:400px}.profile-img{height:550px}}@media(max-width:767px){.profile-screen,.profile-flex,.overlay-container{width:100%;max-width:100%;overflow-x:hidden;box-sizing:border-box}.profile-screen{border-radius:0}.profile-flex{height:360px}.profile-img{height:510px}.chips-grid{width:100%;max-width:100%;padding:0 4px;top:50px;bottom:50px;box-sizing:border-box}.chips-column{padding:2px}.chips-column.left{align-items:flex-end}.chips-column.center{align-items:center}.chips-column.right{align-items:flex-start}.frosted-chip{padding:4px 10px;max-width:100%;box-sizing:border-box}.frosted-chip .chip-label{font-size:11px;max-width:100%;overflow:hidden;text-overflow:ellipsis}.view-profile{bottom:14px}}@media(max-width:480px){.profile-flex{height:340px}.profile-img{height:480px}.frosted-chip{padding:3px 8px}.frosted-chip .chip-label{font-size:10px}.header-label{top:14px}.header-label.left{font-size:12px}.header-label.center{font-size:10px}.header-label.right{font-size:12px}}\n"], dependencies: [{ kind: "directive", type: i2_1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i2_1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] });
|
|
3442
|
+
onNodeMouseEnter(node) {
|
|
3443
|
+
this.hoveredNode = node;
|
|
3444
|
+
}
|
|
3445
|
+
onNodeMouseLeave(node) {
|
|
3446
|
+
this.hoveredNode = null;
|
|
3447
|
+
}
|
|
3448
|
+
onNodeClick(node, event) {
|
|
3449
|
+
event.stopPropagation();
|
|
3450
|
+
if (this.clickedNode === node) {
|
|
3451
|
+
this.clickedNode = null;
|
|
3452
|
+
this.updateAttractionForce(null);
|
|
3453
|
+
}
|
|
3454
|
+
else {
|
|
3455
|
+
this.clickedNode = node;
|
|
3456
|
+
this.updateAttractionForce(node);
|
|
3457
|
+
}
|
|
3458
|
+
}
|
|
3459
|
+
onDocumentClick() {
|
|
3460
|
+
this.clickedNode = null;
|
|
3461
|
+
if (!this.isDragging) {
|
|
3462
|
+
this.updateAttractionForce(null);
|
|
3463
|
+
}
|
|
3464
|
+
}
|
|
3465
|
+
/**
|
|
3466
|
+
* Installs (or removes) a custom d3 force that pulls every linked node toward
|
|
3467
|
+
* the anchor, stopping at an orbit radius scaled by similarity:
|
|
3468
|
+
* highest-sim → MIN_ORBIT (touches anchor), lowest-sim → MAX_ORBIT.
|
|
3469
|
+
*
|
|
3470
|
+
* While active, the xTarget/yTarget restoring forces are muted so they don't
|
|
3471
|
+
* fight the attraction. They are restored the moment attraction is cleared.
|
|
3472
|
+
*/
|
|
3473
|
+
updateAttractionForce(anchorNode) {
|
|
3474
|
+
if (!this.simulation)
|
|
3475
|
+
return;
|
|
3476
|
+
if (!anchorNode) {
|
|
3477
|
+
this.simulation.force('attraction', null);
|
|
3478
|
+
// Restore restoring forces so nodes spring back home
|
|
3479
|
+
const xT = this.simulation.force('xTarget');
|
|
3480
|
+
if (xT)
|
|
3481
|
+
xT.strength(0.1);
|
|
3482
|
+
const yT = this.simulation.force('yTarget');
|
|
3483
|
+
if (yT)
|
|
3484
|
+
yT.strength(0.1);
|
|
3485
|
+
const col = this.simulation.force('collide');
|
|
3486
|
+
if (col)
|
|
3487
|
+
col.strength(1.0);
|
|
3488
|
+
this.simulation.alphaTarget(0);
|
|
3489
|
+
this.simulation.alpha(0.6).restart();
|
|
3490
|
+
return;
|
|
3491
|
+
}
|
|
3492
|
+
// Collect connected node IDs and their similarity values
|
|
3493
|
+
const linkSim = new Map();
|
|
3494
|
+
this.forceLinks.forEach(link => {
|
|
3495
|
+
const srcId = typeof link.source === 'object' ? link.source.id : link.source;
|
|
3496
|
+
const tgtId = typeof link.target === 'object' ? link.target.id : link.target;
|
|
3497
|
+
const sim = Number(link.similarity ?? link.strength ?? 0.5);
|
|
3498
|
+
if (srcId === anchorNode.id)
|
|
3499
|
+
linkSim.set(String(tgtId), sim);
|
|
3500
|
+
if (tgtId === anchorNode.id)
|
|
3501
|
+
linkSim.set(String(srcId), sim);
|
|
3502
|
+
});
|
|
3503
|
+
const simValues = Array.from(linkSim.values());
|
|
3504
|
+
const maxSim = simValues.length ? Math.max(...simValues) : 1;
|
|
3505
|
+
// Closest chip (highest sim) touches the anchor (36px ≈ one chip height).
|
|
3506
|
+
// Others are spread tightly between 36–80px.
|
|
3507
|
+
const MIN_ORBIT = 36; // px — highest-sim chip: touching
|
|
3508
|
+
const MAX_ORBIT = 80; // px — lowest-sim chip: still very close
|
|
3509
|
+
const PULL = 0.5; // strong pull — overcomes any residual forces
|
|
3510
|
+
// Mute the forces that would fight the pull while attraction is active
|
|
3511
|
+
const xT = this.simulation.force('xTarget');
|
|
3512
|
+
if (xT)
|
|
3513
|
+
xT.strength(0);
|
|
3514
|
+
const yT = this.simulation.force('yTarget');
|
|
3515
|
+
if (yT)
|
|
3516
|
+
yT.strength(0);
|
|
3517
|
+
const col = this.simulation.force('collide');
|
|
3518
|
+
if (col)
|
|
3519
|
+
col.strength(0.35); // keep slight chip separation while still allowing orbiting
|
|
3520
|
+
const attractionForce = (alpha) => {
|
|
3521
|
+
const ax = anchorNode.x ?? 0;
|
|
3522
|
+
const ay = anchorNode.y ?? 0;
|
|
3523
|
+
this.forceNodes.forEach(n => {
|
|
3524
|
+
if (n.id === anchorNode.id)
|
|
3525
|
+
return;
|
|
3526
|
+
const sim = linkSim.get(n.id);
|
|
3527
|
+
if (sim === undefined)
|
|
3528
|
+
return; // not connected — leave untouched
|
|
3529
|
+
const normalizedSim = maxSim > 0 ? sim / maxSim : 0;
|
|
3530
|
+
const orbitDist = MIN_ORBIT + (MAX_ORBIT - MIN_ORBIT) * (1 - normalizedSim);
|
|
3531
|
+
const dx = ax - (n.x ?? 0);
|
|
3532
|
+
const dy = ay - (n.y ?? 0);
|
|
3533
|
+
const dist = Math.sqrt(dx * dx + dy * dy) || 1;
|
|
3534
|
+
if (dist > orbitDist) {
|
|
3535
|
+
// Pull toward orbit boundary — proportional to gap remaining
|
|
3536
|
+
const factor = PULL * alpha * (dist - orbitDist) / dist;
|
|
3537
|
+
n.vx = (n.vx ?? 0) + dx * factor;
|
|
3538
|
+
n.vy = (n.vy ?? 0) + dy * factor;
|
|
3539
|
+
}
|
|
3540
|
+
});
|
|
3541
|
+
};
|
|
3542
|
+
this.simulation.force('attraction', attractionForce);
|
|
3543
|
+
// alphaTarget 0.3 keeps alpha high so attraction stays strong indefinitely
|
|
3544
|
+
this.simulation.alphaTarget(0.3).restart();
|
|
3545
|
+
}
|
|
3546
|
+
isNodeConnected(node) {
|
|
3547
|
+
if (!this.activeNode)
|
|
3548
|
+
return true;
|
|
3549
|
+
if (this.activeNode === node)
|
|
3550
|
+
return true;
|
|
3551
|
+
return this.forceLinks.some(link => {
|
|
3552
|
+
const srcId = (link.source && typeof link.source === 'object') ? link.source.id : link.source;
|
|
3553
|
+
const tgtId = (link.target && typeof link.target === 'object') ? link.target.id : link.target;
|
|
3554
|
+
return (srcId === this.activeNode.id && tgtId === node.id) ||
|
|
3555
|
+
(tgtId === this.activeNode.id && srcId === node.id);
|
|
3556
|
+
});
|
|
3557
|
+
}
|
|
3558
|
+
isLinkConnectedToActive(link) {
|
|
3559
|
+
if (!this.activeNode)
|
|
3560
|
+
return false;
|
|
3561
|
+
const srcId = (link.source && typeof link.source === 'object') ? link.source.id : link.source;
|
|
3562
|
+
const tgtId = (link.target && typeof link.target === 'object') ? link.target.id : link.target;
|
|
3563
|
+
return srcId === this.activeNode.id || tgtId === this.activeNode.id;
|
|
3564
|
+
}
|
|
3565
|
+
/**
|
|
3566
|
+
* Returns true if the link connects a p1 node to a p2 node (a direct left↔right match),
|
|
3567
|
+
* as opposed to a connection involving a shared-pool chip.
|
|
3568
|
+
*/
|
|
3569
|
+
isCrossGroupLink(link) {
|
|
3570
|
+
const src = this.findNode(link.source);
|
|
3571
|
+
const tgt = this.findNode(link.target);
|
|
3572
|
+
if (!src || !tgt)
|
|
3573
|
+
return false;
|
|
3574
|
+
return (src.group === 'p1' && tgt.group === 'p2') ||
|
|
3575
|
+
(src.group === 'p2' && tgt.group === 'p1');
|
|
3576
|
+
}
|
|
3577
|
+
/**
|
|
3578
|
+
* All force links sorted so secondary (shared-chip) links render first and
|
|
3579
|
+
* primary (p1↔p2 direct match) links render last — SVG paints last-child on top.
|
|
3580
|
+
*/
|
|
3581
|
+
get sortedForceLinks() {
|
|
3582
|
+
return [...this.forceLinks].sort((a, b) => (this.isCrossGroupLink(a) ? 1 : 0) - (this.isCrossGroupLink(b) ? 1 : 0));
|
|
3583
|
+
}
|
|
3584
|
+
getLinkSourceX(link) {
|
|
3585
|
+
const node = this.findNode(link.source);
|
|
3586
|
+
return node ? (node.x ?? 0) : 0;
|
|
3587
|
+
}
|
|
3588
|
+
getLinkSourceY(link) {
|
|
3589
|
+
const node = this.findNode(link.source);
|
|
3590
|
+
return node ? (node.y ?? 0) : 0;
|
|
3591
|
+
}
|
|
3592
|
+
getLinkTargetX(link) {
|
|
3593
|
+
const node = this.findNode(link.target);
|
|
3594
|
+
return node ? (node.x ?? 0) : 0;
|
|
3595
|
+
}
|
|
3596
|
+
getLinkTargetY(link) {
|
|
3597
|
+
const node = this.findNode(link.target);
|
|
3598
|
+
return node ? (node.y ?? 0) : 0;
|
|
3599
|
+
}
|
|
3600
|
+
findNode(ref) {
|
|
3601
|
+
if (!ref)
|
|
3602
|
+
return undefined;
|
|
3603
|
+
const id = typeof ref === 'object' ? ref.id : ref;
|
|
3604
|
+
return this.forceNodes.find(n => n.id === id);
|
|
3605
|
+
}
|
|
3606
|
+
onNodeDragStart(node, event) {
|
|
3607
|
+
if (event.cancelable) {
|
|
3608
|
+
event.preventDefault();
|
|
3609
|
+
}
|
|
3610
|
+
event.stopPropagation();
|
|
3611
|
+
this.draggedNode = node;
|
|
3612
|
+
this.isDragging = true;
|
|
3613
|
+
this.clickedNode = node;
|
|
3614
|
+
const touchEvent = event;
|
|
3615
|
+
const mouseEvent = event;
|
|
3616
|
+
const isTouch = !!(touchEvent.touches && touchEvent.touches.length);
|
|
3617
|
+
const clientX = isTouch ? touchEvent.touches[0].clientX : mouseEvent.clientX;
|
|
3618
|
+
const clientY = isTouch ? touchEvent.touches[0].clientY : mouseEvent.clientY;
|
|
3619
|
+
const rect = this.chipsGrid.nativeElement.getBoundingClientRect();
|
|
3620
|
+
const scrollTop = this.chipsGrid.nativeElement.scrollTop;
|
|
3621
|
+
const relX = clientX - rect.left;
|
|
3622
|
+
const relY = clientY - rect.top + scrollTop;
|
|
3623
|
+
const bounds = this.nodeDragBounds.get(node.id);
|
|
3624
|
+
if (bounds) {
|
|
3625
|
+
node.fx = Math.max(bounds.minX, Math.min(bounds.maxX, relX));
|
|
3626
|
+
node.fy = Math.max(bounds.minY, Math.min(bounds.maxY, relY));
|
|
3627
|
+
}
|
|
3628
|
+
else {
|
|
3629
|
+
node.fx = relX;
|
|
3630
|
+
node.fy = relY;
|
|
3631
|
+
}
|
|
3632
|
+
// Restore interactive forces for dragging
|
|
3633
|
+
if (this.simulation) {
|
|
3634
|
+
const linkForce = this.simulation.force('link');
|
|
3635
|
+
if (linkForce)
|
|
3636
|
+
linkForce.strength((d) => d.strength * 0.6);
|
|
3637
|
+
const collideForce = this.simulation.force('collide');
|
|
3638
|
+
if (collideForce)
|
|
3639
|
+
collideForce.strength(1.0);
|
|
3640
|
+
const xTarget = this.simulation.force('xTarget');
|
|
3641
|
+
if (xTarget)
|
|
3642
|
+
xTarget.strength(0.08);
|
|
3643
|
+
const yTarget = this.simulation.force('yTarget');
|
|
3644
|
+
if (yTarget)
|
|
3645
|
+
yTarget.strength(0.08);
|
|
3646
|
+
}
|
|
3647
|
+
// Pull connected chips toward the dragged node
|
|
3648
|
+
this.updateAttractionForce(node);
|
|
3649
|
+
// Wake up the simulation target to keep it highly active during drag
|
|
3650
|
+
this.simulation.alphaTarget(0.3).restart();
|
|
3651
|
+
}
|
|
3652
|
+
onDocumentDragMove(event) {
|
|
3653
|
+
if (!this.isDragging || !this.draggedNode)
|
|
3654
|
+
return;
|
|
3655
|
+
if (event.cancelable) {
|
|
3656
|
+
event.preventDefault();
|
|
3657
|
+
}
|
|
3658
|
+
const touchEvent = event;
|
|
3659
|
+
const mouseEvent = event;
|
|
3660
|
+
const isTouch = !!(touchEvent.touches && touchEvent.touches.length);
|
|
3661
|
+
const clientX = isTouch ? touchEvent.touches[0].clientX : mouseEvent.clientX;
|
|
3662
|
+
const clientY = isTouch ? touchEvent.touches[0].clientY : mouseEvent.clientY;
|
|
3663
|
+
const rect = this.chipsGrid.nativeElement.getBoundingClientRect();
|
|
3664
|
+
const scrollTop = this.chipsGrid.nativeElement.scrollTop;
|
|
3665
|
+
// Clamp dragged position so chip can't go off-edge (same estimate used in layout pass).
|
|
3666
|
+
const charWidth = this.chipFontSizePx * 0.53;
|
|
3667
|
+
const halfW = (this.draggedNode.label.length * charWidth + this.chipPaddingXPx * 2) / 2;
|
|
3668
|
+
const edgePad = 20;
|
|
3669
|
+
const relX = clientX - rect.left;
|
|
3670
|
+
const relY = clientY - rect.top + scrollTop;
|
|
3671
|
+
const fallbackMinX = halfW + edgePad;
|
|
3672
|
+
const fallbackMaxX = rect.width - halfW - edgePad;
|
|
3673
|
+
const fallbackMinY = 30;
|
|
3674
|
+
const fallbackMaxY = Math.max(fallbackMinY, this.forceCanvasHeight - 30);
|
|
3675
|
+
const bounds = this.nodeDragBounds.get(this.draggedNode.id);
|
|
3676
|
+
const minX = bounds?.minX ?? fallbackMinX;
|
|
3677
|
+
const maxX = bounds?.maxX ?? fallbackMaxX;
|
|
3678
|
+
const minY = bounds?.minY ?? fallbackMinY;
|
|
3679
|
+
const maxY = bounds?.maxY ?? fallbackMaxY;
|
|
3680
|
+
const clampedX = Math.max(minX, Math.min(maxX, relX));
|
|
3681
|
+
const clampedY = Math.max(minY, Math.min(maxY, relY));
|
|
3682
|
+
const anchorX = this.draggedNode.originalFx ?? clampedX;
|
|
3683
|
+
const anchorY = this.draggedNode.originalFy ?? clampedY;
|
|
3684
|
+
const xElasticity = this.draggedNode.group === 'shared' ? 0.62 : 0.72;
|
|
3685
|
+
const yElasticity = this.draggedNode.group === 'shared' ? 0.58 : 0.68;
|
|
3686
|
+
this.draggedNode.fx = anchorX + (clampedX - anchorX) * xElasticity;
|
|
3687
|
+
this.draggedNode.fy = anchorY + (clampedY - anchorY) * yElasticity;
|
|
3688
|
+
// Keep active force simulation running
|
|
3689
|
+
this.simulation.alpha(0.3).restart();
|
|
3690
|
+
}
|
|
3691
|
+
onDocumentDragEnd() {
|
|
3692
|
+
if (!this.isDragging || !this.draggedNode)
|
|
3693
|
+
return;
|
|
3694
|
+
// Release the node. Clear fx and fy so it is no longer pinned to the mouse
|
|
3695
|
+
this.draggedNode.fx = undefined;
|
|
3696
|
+
this.draggedNode.fy = undefined;
|
|
3697
|
+
// Remove attraction so connected chips return home
|
|
3698
|
+
this.simulation.force('attraction', null);
|
|
3699
|
+
// Set other forces to 0 so all chips glide directly to their starting coordinates
|
|
3700
|
+
if (this.simulation) {
|
|
3701
|
+
const linkForce = this.simulation.force('link');
|
|
3702
|
+
if (linkForce)
|
|
3703
|
+
linkForce.strength(0);
|
|
3704
|
+
const collideForce = this.simulation.force('collide');
|
|
3705
|
+
if (collideForce)
|
|
3706
|
+
collideForce.strength(0.92);
|
|
3707
|
+
// Pull nodes smoothly and slowly back to their exact starting points
|
|
3708
|
+
const xTarget = this.simulation.force('xTarget');
|
|
3709
|
+
if (xTarget)
|
|
3710
|
+
xTarget.strength(0.13);
|
|
3711
|
+
const yTarget = this.simulation.force('yTarget');
|
|
3712
|
+
if (yTarget)
|
|
3713
|
+
yTarget.strength(0.13);
|
|
3714
|
+
}
|
|
3715
|
+
// Cool down simulation targets
|
|
3716
|
+
this.simulation.alphaTarget(0);
|
|
3717
|
+
this.simulation.alpha(0.6).restart();
|
|
3718
|
+
this.draggedNode = null;
|
|
3719
|
+
this.isDragging = false;
|
|
3720
|
+
}
|
|
3721
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.24", ngImport: i0, type: ProfileComparisonLibComponent, deps: [{ token: ProfileComparisonBackendService }, { token: i0.Renderer2 }, { token: FileConversionService }, { token: ImageCompressionService }, { token: i0.ChangeDetectorRef }, { token: i0.NgZone }, { token: i0.ElementRef }, { token: PROFILE_COMPARISON_VERBOSE_LOGGING, optional: true }], target: i0.ɵɵFactoryTarget.Component });
|
|
3722
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.24", type: ProfileComparisonLibComponent, isStandalone: false, selector: "lib-profile-comparison", inputs: { config: "config", user1File: "user1File", user2File: "user2File", backendMode: "backendMode", backendUrl: "backendUrl", fadeAllEdges: "fadeAllEdges", person1Name: "person1Name", person2Name: "person2Name", imageProxyPath: "imageProxyPath" }, outputs: { matrixDataChange: "matrixDataChange", rawLLMOutputChange: "rawLLMOutputChange", viewProfileClick: "viewProfileClick" }, host: { listeners: { "window:resize": "onWindowResize()", "document:click": "onDocumentClick()", "document:mousemove": "onDocumentDragMove($event)", "document:touchmove": "onDocumentDragMove($event)", "document:mouseup": "onDocumentDragEnd()", "document:touchend": "onDocumentDragEnd()" } }, viewQueries: [{ propertyName: "leftContainer", first: true, predicate: ["leftContainer"], descendants: true }, { propertyName: "rightContainer", first: true, predicate: ["rightContainer"], descendants: true }, { propertyName: "profileFlex", first: true, predicate: ["profileFlex"], descendants: true }, { propertyName: "profileImgLeft", first: true, predicate: ["profileImgLeft"], descendants: true }, { propertyName: "profileImgRight", first: true, predicate: ["profileImgRight"], descendants: true }, { propertyName: "shapeContainer", first: true, predicate: ["shapeContainer"], descendants: true }, { propertyName: "shapeBg", first: true, predicate: ["shapeBg"], descendants: true }, { propertyName: "shapeBg1", first: true, predicate: ["shapeBg1"], descendants: true }, { propertyName: "shapeBg2", first: true, predicate: ["shapeBg2"], descendants: true }, { propertyName: "shapeTextLeft", first: true, predicate: ["shapeTextLeft"], descendants: true }, { propertyName: "shapeTextRight", first: true, predicate: ["shapeTextRight"], descendants: true }, { propertyName: "shapeTextCenter", first: true, predicate: ["shapeTextCenter"], descendants: true }, { propertyName: "chipsGrid", first: true, predicate: ["chipsGrid"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "<div class=\"configure-backend-message\" *ngIf=\"!backendConfigured\">\r\n Configure backend\r\n</div>\r\n\r\n<div class=\"profile-screen\" *ngIf=\"backendConfigured\">\r\n <div #profileFlex class=\"profile-flex\" [class.fade-all]=\"fadeAllEdges\">\r\n <svg class=\"image-noise-overlay\" style=\"position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; overflow: hidden; z-index: 0;\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 320 320\" fill=\"none\" preserveAspectRatio=\"none\">\r\n <g filter=\"url(#filter0_n_1_1753)\">\r\n <rect width=\"100%\" height=\"100%\" fill=\"url(#paint0_radial_1_1753)\" fill-opacity=\"0.33\"/>\r\n </g>\r\n <defs>\r\n <filter id=\"filter0_n_1_1753\" x=\"0%\" y=\"0%\" width=\"100%\" height=\"100%\" filterUnits=\"objectBoundingBox\" color-interpolation-filters=\"sRGB\">\r\n <feFlood flood-opacity=\"0\" result=\"BackgroundImageFix\"/>\r\n <feBlend mode=\"normal\" in=\"SourceGraphic\" in2=\"BackgroundImageFix\" result=\"shape\"/>\r\n <feTurbulence type=\"fractalNoise\" baseFrequency=\"0.10869565606117249 0.10869565606117249\" stitchTiles=\"stitch\" numOctaves=\"3\" result=\"noise\" seed=\"7339\"/>\r\n <feColorMatrix in=\"noise\" type=\"luminanceToAlpha\" result=\"alphaNoise\"/>\r\n <feComponentTransfer in=\"alphaNoise\" result=\"coloredNoise1\">\r\n <feFuncA type=\"discrete\" tableValues=\"0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 \"/>\r\n </feComponentTransfer>\r\n <feComposite operator=\"in\" in2=\"shape\" in=\"coloredNoise1\" result=\"noise1Clipped\"/>\r\n <feFlood flood-color=\"rgba(0, 0, 0, 0.25)\" result=\"color1Flood\"/>\r\n <feComposite operator=\"in\" in2=\"noise1Clipped\" in=\"color1Flood\" result=\"color1\"/>\r\n <feMerge result=\"effect1_noise_1_1753\">\r\n <feMergeNode in=\"shape\"/>\r\n <feMergeNode in=\"color1\"/>\r\n </feMerge>\r\n </filter>\r\n <radialGradient id=\"paint0_radial_1_1753\" cx=\"50%\" cy=\"50%\" r=\"50%\" fx=\"50%\" fy=\"50%\">\r\n <stop offset=\"0%\" stop-opacity=\"0\"/>\r\n <stop offset=\"100%\" stop-opacity=\"0.25\"/>\r\n </radialGradient>\r\n </defs>\r\n </svg>\r\n <div #profileImgLeft class=\"profile-img left\" [class.fade-all]=\"fadeAllEdges\">\r\n <img\r\n [src]=\"user1Image\"\r\n alt=\"User 1\"\r\n [style.transform]=\"user1Transform\"\r\n [style.top]=\"user1Top\"\r\n [style.height]=\"user1Height\"\r\n [style.object-position]=\"user1ObjectPosition\"\r\n (load)=\"onUserImageLoad(1, $event)\"\r\n />\r\n </div>\r\n <div #profileImgRight class=\"profile-img right\" [class.fade-all]=\"fadeAllEdges\">\r\n <img\r\n [src]=\"user2Image\"\r\n alt=\"User 2\"\r\n [style.transform]=\"user2Transform\"\r\n [style.top]=\"user2Top\"\r\n [style.height]=\"user2Height\"\r\n [style.object-position]=\"user2ObjectPosition\"\r\n (load)=\"onUserImageLoad(2, $event)\"\r\n />\r\n </div>\r\n <div class=\"image-shade-overlay\"></div>\r\n\r\n <!-- Frosted Glass Overlay Layer -->\r\n <div class=\"overlay-container\">\r\n <!-- Top Headers \u2014 absolutely positioned overlays, exactly as original -->\r\n <div class=\"header-label left\">{{ person1Name }}</div>\r\n <div class=\"header-label center\">Shared</div>\r\n <div class=\"header-label right\">{{ person2Name }}</div>\r\n\r\n <!-- Force-directed chip canvas (scrollable) -->\r\n <div class=\"chips-grid\" #chipsGrid>\r\n <div class=\"force-canvas\" [style.height.px]=\"forceCanvasHeight\">\r\n <!-- SVG Connections Layer -->\r\n <svg class=\"connections-svg\" [style.height.px]=\"forceCanvasHeight\">\r\n <line\r\n *ngFor=\"let link of sortedForceLinks\"\r\n [attr.x1]=\"getLinkSourceX(link)\"\r\n [attr.y1]=\"getLinkSourceY(link)\"\r\n [attr.x2]=\"getLinkTargetX(link)\"\r\n [attr.y2]=\"getLinkTargetY(link)\"\r\n class=\"connection-line\"\r\n [class.active]=\"isLinkConnectedToActive(link)\"\r\n [class.cross-group]=\"isCrossGroupLink(link)\"\r\n ></line>\r\n </svg>\r\n\r\n <div\r\n *ngFor=\"let node of forceNodes\"\r\n class=\"frosted-chip\"\r\n [class.person-a]=\"node.group === 'p1'\"\r\n [class.shared]=\"node.group === 'shared'\"\r\n [class.person-b]=\"node.group === 'p2'\"\r\n [class.active]=\"activeNode === node\"\r\n [class.dimmed]=\"activeNode && !isNodeConnected(node)\"\r\n [style.left.px]=\"node.x\"\r\n [style.top.px]=\"node.y\"\r\n [style.padding]=\"chipPaddingYPx + 'px ' + chipPaddingXPx + 'px'\"\r\n (mouseenter)=\"onNodeMouseEnter(node)\"\r\n (mouseleave)=\"onNodeMouseLeave(node)\"\r\n (click)=\"onNodeClick(node, $event)\"\r\n (mousedown)=\"onNodeDragStart(node, $event)\"\r\n (touchstart)=\"onNodeDragStart(node, $event)\"\r\n >\r\n <span\r\n class=\"chip-label\"\r\n [style.font-size.px]=\"chipFontSizePx\"\r\n [style.line-height]=\"chipLineHeight\"\r\n >{{ node.label }}</span>\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <!-- Bottom Actions -->\r\n <div class=\"view-profile left\" (click)=\"onViewProfile('left')\">View Profile</div>\r\n <div class=\"view-profile right\" (click)=\"onViewProfile('right')\">View Profile</div>\r\n </div>\r\n\r\n <!-- Loading indicator for alignment process -->\r\n <div *ngIf=\"isAligning\" class=\"loading-indicator\">\r\n <div class=\"loading-spinner\"></div>\r\n </div>\r\n</div>\r\n\r\n\r\n", styles: ["@charset \"UTF-8\";:host{display:block;width:100%;max-width:100%;overflow-x:hidden!important;box-sizing:border-box!important}.profile-img.left{position:absolute;top:0;bottom:0;left:0;width:58%;height:100%;margin:0;z-index:1;-webkit-mask-image:linear-gradient(to right,black 0%,black 45%,transparent 92%);mask-image:linear-gradient(to right,black 0%,black 45%,transparent 92%)}.profile-img.right{position:absolute;inset:0 0 0 auto;width:58%;height:100%;margin:0;z-index:2;-webkit-mask-image:linear-gradient(to right,transparent 8%,black 55%,black 100%);mask-image:linear-gradient(to right,transparent 8%,black 55%,black 100%)}@media(min-width:768px){.profile-img.left{width:65%;-webkit-mask-image:linear-gradient(to right,black 0%,black 40%,transparent 90%);mask-image:linear-gradient(to right,black 0%,black 40%,transparent 90%)}.profile-img.right{width:65%;-webkit-mask-image:linear-gradient(to right,transparent 10%,black 60%,black 100%);mask-image:linear-gradient(to right,transparent 10%,black 60%,black 100%)}}.configure-backend-message{padding:1rem;text-align:center;color:#bdc3c7;background:#34495e;border-radius:8px}.backend-error-message{padding:.75rem 1rem;text-align:center;color:#fef3c7;background:#b4530940;border:1px solid rgba(245,158,11,.5);border-radius:8px;margin-bottom:.5rem}.profile-screen{width:100%;max-width:768px;margin:0 auto;overflow:hidden;position:relative;border-radius:12px}.profile-flex{display:flex;width:100%;height:440px;overflow:hidden;background:transparent;gap:0;margin:0;padding:0;position:relative;box-shadow:none;border:none;--overlap-size: 40px}.profile-flex.fade-all{-webkit-mask-image:linear-gradient(to right,transparent 0%,black 18%,black 82%,transparent 100%),linear-gradient(to bottom,transparent 0%,black 15%,black 85%,transparent 100%);-webkit-mask-composite:intersect;mask-image:linear-gradient(to right,transparent 0%,black 18%,black 82%,transparent 100%),linear-gradient(to bottom,transparent 0%,black 15%,black 85%,transparent 100%);mask-composite:intersect}.profile-flex .image-shade-overlay{position:absolute;inset:0;width:100%;height:100%;pointer-events:none;z-index:3;background:radial-gradient(ellipse at center,transparent 40%,rgba(41,37,45,.25) 100%)}.profile-flex .image-noise-overlay{position:absolute;inset:0;width:100%;height:100%;max-width:100%;max-height:100%;pointer-events:none;overflow:hidden;z-index:0}.shape-bg1,.shape-bg2{position:absolute;max-width:250px;opacity:1;transition:transform .2s ease-out}.shape-bg1{z-index:1;left:0}.shape-bg2{right:0}.shape-bg.fade-all .shape-bg1{-webkit-mask-image:linear-gradient(to right,transparent 0%,black 20%,black 100%);mask-image:linear-gradient(to right,transparent 0%,black 20%,black 100%)}.shape-bg.fade-all .shape-bg2{-webkit-mask-image:linear-gradient(to left,transparent 0%,black 20%,black 100%);mask-image:linear-gradient(to left,transparent 0%,black 20%,black 100%)}.shape-bg svg{cursor:grab}.shape-bg svg:active{cursor:grabbing}.profile-img{height:100%;position:relative;flex-shrink:0;background:transparent;margin:0;padding:0;left:0;top:0;border:none;box-sizing:border-box;overflow:hidden;box-shadow:none;outline:none}.profile-img img{transition:transform .3s ease;width:100%;height:100%;object-fit:cover;display:block;object-position:center center;opacity:1;transform-origin:center center;border:none;outline:none;box-shadow:none;position:relative;top:0;left:0;filter:contrast(1.2) brightness(1.1) saturate(1.1);image-rendering:auto}.overlay-container{position:absolute;inset:0;width:100%;height:100%;pointer-events:none;z-index:10;-webkit-user-select:none;user-select:none;overflow:hidden;background:#29252da6;backdrop-filter:blur(.5px);-webkit-backdrop-filter:blur(.5px);--name-label-font-size: 14px}.header-label{position:absolute;top:20px;font-family:Gilroy,sans-serif;text-shadow:0 1px 8px rgba(0,0,0,.8);pointer-events:none}.header-label.left{left:25%;transform:translate(-50%);font-size:var(--name-label-font-size);font-weight:400;color:#fffffff2;text-align:center}.header-label.center{left:50%;transform:translate(-50%);font-size:12px;font-weight:400;color:#fffc;letter-spacing:0}.header-label.right{left:75%;transform:translate(-50%);font-size:var(--name-label-font-size);font-weight:400;color:#fffffff2;text-align:center}.chips-grid{position:absolute;inset:45px 0;overflow-y:auto;overscroll-behavior:none;scrollbar-width:none;box-sizing:border-box}.chips-grid::-webkit-scrollbar{display:none}.force-canvas{position:relative;width:100%;box-sizing:border-box;min-height:120px}.connections-svg{position:absolute;top:0;left:0;width:100%;pointer-events:none;z-index:1}@keyframes lineGlow{0%{opacity:0;stroke-dashoffset:8}30%{opacity:.15}to{opacity:.6;stroke-dashoffset:0}}.connection-line{stroke:#fe3c72d1;stroke-width:1.5px;stroke-dasharray:4;opacity:0;pointer-events:none;transition:opacity .3s ease}.connection-line.active{animation:lineGlow .7s cubic-bezier(.4,0,.2,1) forwards}.frosted-chip{position:absolute;cursor:pointer;pointer-events:auto;transition:transform .2s ease,box-shadow .2s ease,background .2s ease,opacity .3s ease;flex-shrink:0;padding:6px 18px;border-radius:999px;display:inline-flex;align-items:center;justify-content:center;transform:translate(-50%,-50%);z-index:2;border:1px solid rgba(255,255,255,.24);background:#ffffff30;box-shadow:inset 0 1.5px 2px #ffffff59,0 0 0 1px #0f172a38;backdrop-filter:blur(.5px);-webkit-backdrop-filter:blur(.5px)}.frosted-chip:hover{transform:translate(-50%,-50%) scale(1.04);background:#ffffff29;cursor:grab}.frosted-chip.active{background:#ffffff4d;box-shadow:0 0 15px #fe3c7261,inset 0 1.5px 2px #ffffff80,0 0 0 1px #ffffff52;transform:translate(-50%,-50%) scale(1.06);z-index:10;cursor:grabbing}.frosted-chip.active .chip-label{color:#fff!important}.frosted-chip.dimmed{opacity:.35}.frosted-chip .chip-label{position:relative;color:#a8a8a8;text-align:center;font-family:Gilroy,sans-serif;font-size:14px;font-style:normal;font-weight:400;line-height:140%;letter-spacing:-.01em;text-shadow:none;white-space:nowrap;pointer-events:none;z-index:1}.ghost-chip{visibility:hidden!important;pointer-events:none!important}.view-profile{position:absolute;bottom:8px;color:#ffffff80;text-shadow:0 2px 4.2px #000;font-family:Calistoga,serif;font-size:var(--name-label-font-size);font-style:normal;font-weight:400;line-height:normal;cursor:pointer;pointer-events:auto;transition:opacity .2s ease,transform .2s ease}.view-profile:hover{opacity:.85}.view-profile.left{left:8%;width:34%;text-align:left;transform:none}.view-profile.left:hover{transform:translateY(-2px)}.view-profile.right{right:8%;width:34%;text-align:right;left:auto;transform:none}.view-profile.right:hover{transform:translateY(-2px)}#drag-preview{position:absolute;pointer-events:none;display:none;background:#000000b3;color:#fff;padding:5px 10px;border-radius:5px;z-index:9999;max-width:200px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.shape-text-container{width:70px;position:relative;overflow:hidden}.draggable{width:70px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;cursor:grab;-webkit-user-select:none;user-select:none;position:relative;display:inline-block}.dragging{width:auto;text-overflow:clip;cursor:grabbing;position:absolute;z-index:1000}.p-wrapper{width:70px;overflow-x:hidden}.shape-text-center{display:flex;justify-content:space-between;flex-direction:column;gap:10px;height:100%;margin-top:55px}.loading-indicator{position:absolute;inset:0;width:100%;height:100%;background:#00000040;backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px);color:#fff;z-index:100;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12px;pointer-events:auto}.loading-spinner{width:44px;height:44px;border:3.5px solid rgba(255,255,255,.25);border-top:3.5px solid #ffffff;border-radius:50%;animation:spin .8s linear infinite;box-shadow:0 0 15px #fff3}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.loading-indicator p{margin:0;font-size:14px;font-weight:500;color:#ffffffd9;letter-spacing:.02em}.api-key-modal-overlay{position:fixed;inset:0;background:#0f172a99;-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px);display:flex;align-items:center;justify-content:center;z-index:2000}.api-key-modal{width:min(640px,92vw);background:#0f172a;color:#e2e8f0;border:1px solid #334155;border-radius:12px;box-shadow:0 20px 50px #00000073;padding:16px 18px}.api-key-modal-header{display:flex;align-items:center;justify-content:space-between;gap:12px}.api-key-modal-header h2{margin:0;font-size:20px;font-weight:700;color:#e2e8f0}.warning-icon{color:#fbbf24}.api-key-modal .close-btn{background:#0b1220;border:1px solid #334155;color:#94a3b8;border-radius:8px;padding:6px;cursor:pointer;line-height:0}.api-key-modal-body{margin-top:12px;display:flex;flex-direction:column;gap:10px}.modal-message,.modal-instruction{margin:0;color:#cbd5e1}.modal-message strong{color:#e5e7eb}.api-key-input-group{display:flex;align-items:center;gap:10px}.api-key-input{flex:1 1 auto;background:#0a1020;color:#e2e8f0;border:1px solid #334155;border-radius:8px;padding:10px 12px}.api-key-input:focus{outline:none;border-color:#2563eb;box-shadow:0 0 0 3px #2563eb33}.api-key-load-btn{background:#2563eb;color:#fff;border:none;border-radius:8px;padding:10px 12px;cursor:pointer;font-weight:600}.api-key-load-btn:hover{background:#1d4ed8}.modal-hint{display:flex;align-items:center;gap:8px;color:#94a3b8}.modal-hint a{color:#93c5fd}@media(max-width:1023px){.profile-screen{max-width:640px}.profile-flex{height:400px}.profile-img{height:550px}}@media(max-width:767px){.profile-screen,.profile-flex,.overlay-container{width:100%;max-width:100%;overflow-x:hidden;box-sizing:border-box}.profile-screen{border-radius:0}.profile-flex{height:360px}.profile-img{height:510px}.chips-grid{width:100%;max-width:100%;padding:0;top:45px;bottom:50px;box-sizing:border-box}.frosted-chip{padding:4px 10px;max-width:100%;box-sizing:border-box}.frosted-chip .chip-label{font-size:11px;max-width:100%;overflow:hidden;text-overflow:ellipsis}.view-profile{bottom:10px}}@media(max-width:480px){.overlay-container{--name-label-font-size: 12px}.profile-flex{height:340px}.profile-img{height:480px}.frosted-chip{padding:3px 8px}.frosted-chip .chip-label,.header-label.center{font-size:10px}}\n"], dependencies: [{ kind: "directive", type: i2_1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i2_1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] });
|
|
2577
3723
|
}
|
|
2578
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.
|
|
3724
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.24", ngImport: i0, type: ProfileComparisonLibComponent, decorators: [{
|
|
2579
3725
|
type: Component,
|
|
2580
|
-
args: [{ selector: 'lib-profile-comparison', standalone: false, template: "<div class=\"configure-backend-message\" *ngIf=\"!backendConfigured\">\n Configure backend\n</div>\n\n<div class=\"profile-screen\" *ngIf=\"backendConfigured\">\n <div #profileFlex class=\"profile-flex\" [class.fade-all]=\"fadeAllEdges\">\n <svg class=\"image-noise-overlay\" style=\"position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; overflow: hidden; z-index: 0;\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 320 320\" fill=\"none\" preserveAspectRatio=\"none\">\n <g filter=\"url(#filter0_n_1_1753)\">\n <rect width=\"100%\" height=\"100%\" fill=\"url(#paint0_radial_1_1753)\" fill-opacity=\"0.33\"/>\n </g>\n <defs>\n <filter id=\"filter0_n_1_1753\" x=\"0%\" y=\"0%\" width=\"100%\" height=\"100%\" filterUnits=\"objectBoundingBox\" color-interpolation-filters=\"sRGB\">\n <feFlood flood-opacity=\"0\" result=\"BackgroundImageFix\"/>\n <feBlend mode=\"normal\" in=\"SourceGraphic\" in2=\"BackgroundImageFix\" result=\"shape\"/>\n <feTurbulence type=\"fractalNoise\" baseFrequency=\"0.10869565606117249 0.10869565606117249\" stitchTiles=\"stitch\" numOctaves=\"3\" result=\"noise\" seed=\"7339\"/>\n <feColorMatrix in=\"noise\" type=\"luminanceToAlpha\" result=\"alphaNoise\"/>\n <feComponentTransfer in=\"alphaNoise\" result=\"coloredNoise1\">\n <feFuncA type=\"discrete\" tableValues=\"0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 \"/>\n </feComponentTransfer>\n <feComposite operator=\"in\" in2=\"shape\" in=\"coloredNoise1\" result=\"noise1Clipped\"/>\n <feFlood flood-color=\"rgba(0, 0, 0, 0.25)\" result=\"color1Flood\"/>\n <feComposite operator=\"in\" in2=\"noise1Clipped\" in=\"color1Flood\" result=\"color1\"/>\n <feMerge result=\"effect1_noise_1_1753\">\n <feMergeNode in=\"shape\"/>\n <feMergeNode in=\"color1\"/>\n </feMerge>\n </filter>\n <radialGradient id=\"paint0_radial_1_1753\" cx=\"50%\" cy=\"50%\" r=\"50%\" fx=\"50%\" fy=\"50%\">\n <stop offset=\"0%\" stop-opacity=\"0\"/>\n <stop offset=\"100%\" stop-opacity=\"0.25\"/>\n </radialGradient>\n </defs>\n </svg>\n <div #profileImgLeft class=\"profile-img left\" [class.fade-all]=\"fadeAllEdges\">\n <img\n [src]=\"user1Image\"\n alt=\"User 1\"\n [style.transform]=\"user1Transform\"\n [style.top]=\"user1Top\"\n [style.height]=\"user1Height\"\n [style.object-position]=\"user1ObjectPosition\"\n (load)=\"onUserImageLoad(1, $event)\"\n />\n </div>\n <div #profileImgRight class=\"profile-img right\" [class.fade-all]=\"fadeAllEdges\">\n <img\n [src]=\"user2Image\"\n alt=\"User 2\"\n [style.transform]=\"user2Transform\"\n [style.top]=\"user2Top\"\n [style.height]=\"user2Height\"\n [style.object-position]=\"user2ObjectPosition\"\n (load)=\"onUserImageLoad(2, $event)\"\n />\n </div>\n <div class=\"image-shade-overlay\"></div>\n\n <!-- Frosted Glass Overlay Layer -->\n <div class=\"overlay-container\">\n <!-- Top Headers -->\n <div class=\"header-label left\">{{ person1Name }}</div>\n <div class=\"header-label center\">Shared</div>\n <div class=\"header-label right\">{{ person2Name }}</div>\n\n <!-- Chips Columns Grid -->\n <div class=\"chips-grid\">\n <!-- Person 1 Unique Interests -->\n <div class=\"chips-column left\">\n <ng-container *ngFor=\"let interest of getPerson1UniqueInterests(); let i = index\">\n <div class=\"frosted-chip person-a\" [style.margin-right.px]=\"getChipOffset(i, 'left')\" [style.margin-top.px]=\"(i > 0 ? 4 : 0)\">\n <span class=\"chip-label\">{{ interest }}</span>\n </div>\n </ng-container>\n </div>\n\n <!-- Shared Interests (Center Column) -->\n <div class=\"chips-column center\">\n <ng-container *ngFor=\"let interest of centerItem; let i = index\">\n <div *ngIf=\"interest !== 'empty'\" class=\"frosted-chip shared\" [style.margin-left.px]=\"getChipOffset(i, 'center')\" [style.margin-top.px]=\"(i > 0 ? 4 : 0)\">\n <span class=\"chip-label\">{{ interest }}</span>\n </div>\n <div *ngIf=\"interest === 'empty'\" class=\"frosted-chip ghost-chip shared\" [style.margin-left.px]=\"getChipOffset(i, 'center')\" [style.margin-top.px]=\"(i > 0 ? 4 : 0)\">\n <span class=\"chip-label\"> </span>\n </div>\n </ng-container>\n </div>\n\n <!-- Person 2 Unique Interests -->\n <div class=\"chips-column right\">\n <ng-container *ngFor=\"let interest of getPerson2UniqueInterests(); let i = index\">\n <div class=\"frosted-chip person-b\" [style.margin-left.px]=\"getChipOffset(i, 'right')\" [style.margin-top.px]=\"(i > 0 ? 4 : 0)\">\n <span class=\"chip-label\">{{ interest }}</span>\n </div>\n </ng-container>\n </div>\n </div>\n\n <!-- Bottom Actions -->\n <div class=\"view-profile left\" (click)=\"onViewProfile('left')\">View Profile</div>\n <div class=\"view-profile right\" (click)=\"onViewProfile('right')\">View Profile</div>\n </div>\n\n <!-- Loading indicator for alignment process -->\n <div *ngIf=\"isAligning\" class=\"loading-indicator\">\n <div class=\"loading-spinner\"></div>\n </div>\n</div>\n\n\n", styles: [":host{display:block;width:100%;max-width:100%;overflow-x:hidden!important;box-sizing:border-box!important}.profile-img.left{position:absolute;top:0;bottom:0;left:0;width:58%;height:100%;margin:0;z-index:1;-webkit-mask-image:linear-gradient(to right,black 0%,black 45%,transparent 92%);mask-image:linear-gradient(to right,black 0%,black 45%,transparent 92%)}.profile-img.right{position:absolute;inset:0 0 0 auto;width:58%;height:100%;margin:0;z-index:2;-webkit-mask-image:linear-gradient(to right,transparent 8%,black 55%,black 100%);mask-image:linear-gradient(to right,transparent 8%,black 55%,black 100%)}@media(min-width:768px){.profile-img.left{width:65%;-webkit-mask-image:linear-gradient(to right,black 0%,black 40%,transparent 90%);mask-image:linear-gradient(to right,black 0%,black 40%,transparent 90%)}.profile-img.right{width:65%;-webkit-mask-image:linear-gradient(to right,transparent 10%,black 60%,black 100%);mask-image:linear-gradient(to right,transparent 10%,black 60%,black 100%)}}.configure-backend-message{padding:1rem;text-align:center;color:#bdc3c7;background:#34495e;border-radius:8px}.backend-error-message{padding:.75rem 1rem;text-align:center;color:#fef3c7;background:#b4530940;border:1px solid rgba(245,158,11,.5);border-radius:8px;margin-bottom:.5rem}.profile-screen{width:100%;max-width:768px;margin:0 auto;overflow:hidden;position:relative;border-radius:12px}.profile-flex{display:flex;width:100%;height:440px;overflow:hidden;background:transparent;gap:0;margin:0;padding:0;position:relative;box-shadow:none;border:none;--overlap-size: 40px}.profile-flex.fade-all{-webkit-mask-image:linear-gradient(to right,transparent 0%,black 18%,black 82%,transparent 100%),linear-gradient(to bottom,transparent 0%,black 15%,black 85%,transparent 100%);-webkit-mask-composite:intersect;mask-image:linear-gradient(to right,transparent 0%,black 18%,black 82%,transparent 100%),linear-gradient(to bottom,transparent 0%,black 15%,black 85%,transparent 100%);mask-composite:intersect}.profile-flex .image-shade-overlay{position:absolute;inset:0;width:100%;height:100%;pointer-events:none;z-index:3;background:radial-gradient(ellipse at center,transparent 40%,rgba(41,37,45,.25) 100%)}.profile-flex .image-noise-overlay{position:absolute;inset:0;width:100%;height:100%;max-width:100%;max-height:100%;pointer-events:none;overflow:hidden;z-index:0}.shape-bg1,.shape-bg2{position:absolute;max-width:250px;opacity:1;transition:transform .2s ease-out}.shape-bg1{z-index:1;left:0}.shape-bg2{right:0}.shape-bg.fade-all .shape-bg1{-webkit-mask-image:linear-gradient(to right,transparent 0%,black 20%,black 100%);mask-image:linear-gradient(to right,transparent 0%,black 20%,black 100%)}.shape-bg.fade-all .shape-bg2{-webkit-mask-image:linear-gradient(to left,transparent 0%,black 20%,black 100%);mask-image:linear-gradient(to left,transparent 0%,black 20%,black 100%)}.shape-bg svg{cursor:grab}.shape-bg svg:active{cursor:grabbing}.profile-img{height:100%;position:relative;flex-shrink:0;background:transparent;margin:0;padding:0;left:0;top:0;border:none;box-sizing:border-box;overflow:hidden;box-shadow:none;outline:none}.profile-img img{transition:transform .3s ease;width:100%;height:100%;object-fit:cover;display:block;object-position:center center;opacity:1;transform-origin:center center;border:none;outline:none;box-shadow:none;position:relative;top:0;left:0;filter:contrast(1.2) brightness(1.1) saturate(1.1);image-rendering:auto}.overlay-container{position:absolute;inset:0;width:100%;height:100%;pointer-events:none;z-index:10;-webkit-user-select:none;user-select:none;overflow:hidden;background:#29252da6;backdrop-filter:blur(.5px);-webkit-backdrop-filter:blur(.5px)}.header-label{position:absolute;top:20px;font-family:Gilroy,sans-serif;text-shadow:0 1px 8px rgba(0,0,0,.8);pointer-events:none}.header-label.left{left:5%;transform:none;font-size:14px;font-weight:400;color:#fffffff2}.header-label.center{left:50%;transform:translate(-50%);font-size:12px;font-weight:400;color:#fffc;letter-spacing:0}.header-label.right{right:5%;left:auto;transform:none;font-size:14px;font-weight:400;color:#fffffff2}.chips-grid{position:absolute;inset:45px 0;display:flex;padding:0;overflow-y:auto;overscroll-behavior:none;scrollbar-width:none;box-sizing:border-box}.chips-grid::-webkit-scrollbar{display:none}.chips-column{flex:1;display:flex;flex-direction:column;gap:8px;padding:4px 10px;box-sizing:border-box}.chips-column.left{align-items:flex-end}.chips-column.center{align-items:center}.chips-column.right{align-items:flex-start}.frosted-chip{position:relative;cursor:pointer;pointer-events:auto;transition:transform .2s ease,box-shadow .2s ease,background .2s ease;flex-shrink:0;padding:6px 18px;border-radius:999px;display:inline-flex;align-items:center;justify-content:center;border:none;background:#ffffff30;box-shadow:inset 0 1.5px 2px #ffffff59;backdrop-filter:blur(.5px);-webkit-backdrop-filter:blur(.5px)}.frosted-chip:hover{transform:scale(1.04);background:#ffffff29}.frosted-chip .chip-label{position:relative;color:#a8a8a8;text-align:center;font-family:Gilroy,sans-serif;font-size:14px;font-style:normal;font-weight:400;line-height:140%;letter-spacing:-.01em;text-shadow:none;white-space:nowrap;pointer-events:none;z-index:1}.ghost-chip{visibility:hidden!important;pointer-events:none!important}.view-profile{position:absolute;bottom:20px!important;color:#ffffff80;text-shadow:0 2px 4.2px #000;font-family:Calistoga,serif;font-size:18px;font-style:normal;font-weight:400;line-height:normal;cursor:pointer;pointer-events:auto;transition:opacity .2s ease,transform .2s ease}.view-profile:hover{opacity:.85}.view-profile.left{left:5%;transform:none}.view-profile.left:hover{transform:translateY(-2px)}.view-profile.right{right:5%;left:auto;transform:none}.view-profile.right:hover{transform:translateY(-2px)}#drag-preview{position:absolute;pointer-events:none;display:none;background:#000000b3;color:#fff;padding:5px 10px;border-radius:5px;z-index:9999;max-width:200px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.shape-text-container{width:70px;position:relative;overflow:hidden}.draggable{width:70px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;cursor:grab;-webkit-user-select:none;user-select:none;position:relative;display:inline-block}.dragging{width:auto;text-overflow:clip;cursor:grabbing;position:absolute;z-index:1000}.p-wrapper{width:70px;overflow-x:hidden}.shape-text-center{display:flex;justify-content:space-between;flex-direction:column;gap:10px;height:100%;margin-top:55px}.loading-indicator{position:absolute;inset:0;width:100%;height:100%;background:#00000040;backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px);color:#fff;z-index:100;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12px;pointer-events:auto}.loading-spinner{width:44px;height:44px;border:3.5px solid rgba(255,255,255,.25);border-top:3.5px solid #ffffff;border-radius:50%;animation:spin .8s linear infinite;box-shadow:0 0 15px #fff3}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.loading-indicator p{margin:0;font-size:14px;font-weight:500;color:#ffffffd9;letter-spacing:.02em}.api-key-modal-overlay{position:fixed;inset:0;background:#0f172a99;-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px);display:flex;align-items:center;justify-content:center;z-index:2000}.api-key-modal{width:min(640px,92vw);background:#0f172a;color:#e2e8f0;border:1px solid #334155;border-radius:12px;box-shadow:0 20px 50px #00000073;padding:16px 18px}.api-key-modal-header{display:flex;align-items:center;justify-content:space-between;gap:12px}.api-key-modal-header h2{margin:0;font-size:20px;font-weight:700;color:#e2e8f0}.warning-icon{color:#fbbf24}.api-key-modal .close-btn{background:#0b1220;border:1px solid #334155;color:#94a3b8;border-radius:8px;padding:6px;cursor:pointer;line-height:0}.api-key-modal-body{margin-top:12px;display:flex;flex-direction:column;gap:10px}.modal-message,.modal-instruction{margin:0;color:#cbd5e1}.modal-message strong{color:#e5e7eb}.api-key-input-group{display:flex;align-items:center;gap:10px}.api-key-input{flex:1 1 auto;background:#0a1020;color:#e2e8f0;border:1px solid #334155;border-radius:8px;padding:10px 12px}.api-key-input:focus{outline:none;border-color:#2563eb;box-shadow:0 0 0 3px #2563eb33}.api-key-load-btn{background:#2563eb;color:#fff;border:none;border-radius:8px;padding:10px 12px;cursor:pointer;font-weight:600}.api-key-load-btn:hover{background:#1d4ed8}.modal-hint{display:flex;align-items:center;gap:8px;color:#94a3b8}.modal-hint a{color:#93c5fd}@media(max-width:1023px){.profile-screen{max-width:640px}.profile-flex{height:400px}.profile-img{height:550px}}@media(max-width:767px){.profile-screen,.profile-flex,.overlay-container{width:100%;max-width:100%;overflow-x:hidden;box-sizing:border-box}.profile-screen{border-radius:0}.profile-flex{height:360px}.profile-img{height:510px}.chips-grid{width:100%;max-width:100%;padding:0 4px;top:50px;bottom:50px;box-sizing:border-box}.chips-column{padding:2px}.chips-column.left{align-items:flex-end}.chips-column.center{align-items:center}.chips-column.right{align-items:flex-start}.frosted-chip{padding:4px 10px;max-width:100%;box-sizing:border-box}.frosted-chip .chip-label{font-size:11px;max-width:100%;overflow:hidden;text-overflow:ellipsis}.view-profile{bottom:14px}}@media(max-width:480px){.profile-flex{height:340px}.profile-img{height:480px}.frosted-chip{padding:3px 8px}.frosted-chip .chip-label{font-size:10px}.header-label{top:14px}.header-label.left{font-size:12px}.header-label.center{font-size:10px}.header-label.right{font-size:12px}}\n"] }]
|
|
2581
|
-
}], ctorParameters: () => [{ type: ProfileComparisonBackendService }, { type: i0.Renderer2 }, { type: FileConversionService }, { type: ImageCompressionService }, { type: i0.ChangeDetectorRef }, { type: undefined, decorators: [{
|
|
3726
|
+
args: [{ selector: 'lib-profile-comparison', standalone: false, template: "<div class=\"configure-backend-message\" *ngIf=\"!backendConfigured\">\r\n Configure backend\r\n</div>\r\n\r\n<div class=\"profile-screen\" *ngIf=\"backendConfigured\">\r\n <div #profileFlex class=\"profile-flex\" [class.fade-all]=\"fadeAllEdges\">\r\n <svg class=\"image-noise-overlay\" style=\"position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; overflow: hidden; z-index: 0;\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 320 320\" fill=\"none\" preserveAspectRatio=\"none\">\r\n <g filter=\"url(#filter0_n_1_1753)\">\r\n <rect width=\"100%\" height=\"100%\" fill=\"url(#paint0_radial_1_1753)\" fill-opacity=\"0.33\"/>\r\n </g>\r\n <defs>\r\n <filter id=\"filter0_n_1_1753\" x=\"0%\" y=\"0%\" width=\"100%\" height=\"100%\" filterUnits=\"objectBoundingBox\" color-interpolation-filters=\"sRGB\">\r\n <feFlood flood-opacity=\"0\" result=\"BackgroundImageFix\"/>\r\n <feBlend mode=\"normal\" in=\"SourceGraphic\" in2=\"BackgroundImageFix\" result=\"shape\"/>\r\n <feTurbulence type=\"fractalNoise\" baseFrequency=\"0.10869565606117249 0.10869565606117249\" stitchTiles=\"stitch\" numOctaves=\"3\" result=\"noise\" seed=\"7339\"/>\r\n <feColorMatrix in=\"noise\" type=\"luminanceToAlpha\" result=\"alphaNoise\"/>\r\n <feComponentTransfer in=\"alphaNoise\" result=\"coloredNoise1\">\r\n <feFuncA type=\"discrete\" tableValues=\"0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 \"/>\r\n </feComponentTransfer>\r\n <feComposite operator=\"in\" in2=\"shape\" in=\"coloredNoise1\" result=\"noise1Clipped\"/>\r\n <feFlood flood-color=\"rgba(0, 0, 0, 0.25)\" result=\"color1Flood\"/>\r\n <feComposite operator=\"in\" in2=\"noise1Clipped\" in=\"color1Flood\" result=\"color1\"/>\r\n <feMerge result=\"effect1_noise_1_1753\">\r\n <feMergeNode in=\"shape\"/>\r\n <feMergeNode in=\"color1\"/>\r\n </feMerge>\r\n </filter>\r\n <radialGradient id=\"paint0_radial_1_1753\" cx=\"50%\" cy=\"50%\" r=\"50%\" fx=\"50%\" fy=\"50%\">\r\n <stop offset=\"0%\" stop-opacity=\"0\"/>\r\n <stop offset=\"100%\" stop-opacity=\"0.25\"/>\r\n </radialGradient>\r\n </defs>\r\n </svg>\r\n <div #profileImgLeft class=\"profile-img left\" [class.fade-all]=\"fadeAllEdges\">\r\n <img\r\n [src]=\"user1Image\"\r\n alt=\"User 1\"\r\n [style.transform]=\"user1Transform\"\r\n [style.top]=\"user1Top\"\r\n [style.height]=\"user1Height\"\r\n [style.object-position]=\"user1ObjectPosition\"\r\n (load)=\"onUserImageLoad(1, $event)\"\r\n />\r\n </div>\r\n <div #profileImgRight class=\"profile-img right\" [class.fade-all]=\"fadeAllEdges\">\r\n <img\r\n [src]=\"user2Image\"\r\n alt=\"User 2\"\r\n [style.transform]=\"user2Transform\"\r\n [style.top]=\"user2Top\"\r\n [style.height]=\"user2Height\"\r\n [style.object-position]=\"user2ObjectPosition\"\r\n (load)=\"onUserImageLoad(2, $event)\"\r\n />\r\n </div>\r\n <div class=\"image-shade-overlay\"></div>\r\n\r\n <!-- Frosted Glass Overlay Layer -->\r\n <div class=\"overlay-container\">\r\n <!-- Top Headers \u2014 absolutely positioned overlays, exactly as original -->\r\n <div class=\"header-label left\">{{ person1Name }}</div>\r\n <div class=\"header-label center\">Shared</div>\r\n <div class=\"header-label right\">{{ person2Name }}</div>\r\n\r\n <!-- Force-directed chip canvas (scrollable) -->\r\n <div class=\"chips-grid\" #chipsGrid>\r\n <div class=\"force-canvas\" [style.height.px]=\"forceCanvasHeight\">\r\n <!-- SVG Connections Layer -->\r\n <svg class=\"connections-svg\" [style.height.px]=\"forceCanvasHeight\">\r\n <line\r\n *ngFor=\"let link of sortedForceLinks\"\r\n [attr.x1]=\"getLinkSourceX(link)\"\r\n [attr.y1]=\"getLinkSourceY(link)\"\r\n [attr.x2]=\"getLinkTargetX(link)\"\r\n [attr.y2]=\"getLinkTargetY(link)\"\r\n class=\"connection-line\"\r\n [class.active]=\"isLinkConnectedToActive(link)\"\r\n [class.cross-group]=\"isCrossGroupLink(link)\"\r\n ></line>\r\n </svg>\r\n\r\n <div\r\n *ngFor=\"let node of forceNodes\"\r\n class=\"frosted-chip\"\r\n [class.person-a]=\"node.group === 'p1'\"\r\n [class.shared]=\"node.group === 'shared'\"\r\n [class.person-b]=\"node.group === 'p2'\"\r\n [class.active]=\"activeNode === node\"\r\n [class.dimmed]=\"activeNode && !isNodeConnected(node)\"\r\n [style.left.px]=\"node.x\"\r\n [style.top.px]=\"node.y\"\r\n [style.padding]=\"chipPaddingYPx + 'px ' + chipPaddingXPx + 'px'\"\r\n (mouseenter)=\"onNodeMouseEnter(node)\"\r\n (mouseleave)=\"onNodeMouseLeave(node)\"\r\n (click)=\"onNodeClick(node, $event)\"\r\n (mousedown)=\"onNodeDragStart(node, $event)\"\r\n (touchstart)=\"onNodeDragStart(node, $event)\"\r\n >\r\n <span\r\n class=\"chip-label\"\r\n [style.font-size.px]=\"chipFontSizePx\"\r\n [style.line-height]=\"chipLineHeight\"\r\n >{{ node.label }}</span>\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <!-- Bottom Actions -->\r\n <div class=\"view-profile left\" (click)=\"onViewProfile('left')\">View Profile</div>\r\n <div class=\"view-profile right\" (click)=\"onViewProfile('right')\">View Profile</div>\r\n </div>\r\n\r\n <!-- Loading indicator for alignment process -->\r\n <div *ngIf=\"isAligning\" class=\"loading-indicator\">\r\n <div class=\"loading-spinner\"></div>\r\n </div>\r\n</div>\r\n\r\n\r\n", styles: ["@charset \"UTF-8\";:host{display:block;width:100%;max-width:100%;overflow-x:hidden!important;box-sizing:border-box!important}.profile-img.left{position:absolute;top:0;bottom:0;left:0;width:58%;height:100%;margin:0;z-index:1;-webkit-mask-image:linear-gradient(to right,black 0%,black 45%,transparent 92%);mask-image:linear-gradient(to right,black 0%,black 45%,transparent 92%)}.profile-img.right{position:absolute;inset:0 0 0 auto;width:58%;height:100%;margin:0;z-index:2;-webkit-mask-image:linear-gradient(to right,transparent 8%,black 55%,black 100%);mask-image:linear-gradient(to right,transparent 8%,black 55%,black 100%)}@media(min-width:768px){.profile-img.left{width:65%;-webkit-mask-image:linear-gradient(to right,black 0%,black 40%,transparent 90%);mask-image:linear-gradient(to right,black 0%,black 40%,transparent 90%)}.profile-img.right{width:65%;-webkit-mask-image:linear-gradient(to right,transparent 10%,black 60%,black 100%);mask-image:linear-gradient(to right,transparent 10%,black 60%,black 100%)}}.configure-backend-message{padding:1rem;text-align:center;color:#bdc3c7;background:#34495e;border-radius:8px}.backend-error-message{padding:.75rem 1rem;text-align:center;color:#fef3c7;background:#b4530940;border:1px solid rgba(245,158,11,.5);border-radius:8px;margin-bottom:.5rem}.profile-screen{width:100%;max-width:768px;margin:0 auto;overflow:hidden;position:relative;border-radius:12px}.profile-flex{display:flex;width:100%;height:440px;overflow:hidden;background:transparent;gap:0;margin:0;padding:0;position:relative;box-shadow:none;border:none;--overlap-size: 40px}.profile-flex.fade-all{-webkit-mask-image:linear-gradient(to right,transparent 0%,black 18%,black 82%,transparent 100%),linear-gradient(to bottom,transparent 0%,black 15%,black 85%,transparent 100%);-webkit-mask-composite:intersect;mask-image:linear-gradient(to right,transparent 0%,black 18%,black 82%,transparent 100%),linear-gradient(to bottom,transparent 0%,black 15%,black 85%,transparent 100%);mask-composite:intersect}.profile-flex .image-shade-overlay{position:absolute;inset:0;width:100%;height:100%;pointer-events:none;z-index:3;background:radial-gradient(ellipse at center,transparent 40%,rgba(41,37,45,.25) 100%)}.profile-flex .image-noise-overlay{position:absolute;inset:0;width:100%;height:100%;max-width:100%;max-height:100%;pointer-events:none;overflow:hidden;z-index:0}.shape-bg1,.shape-bg2{position:absolute;max-width:250px;opacity:1;transition:transform .2s ease-out}.shape-bg1{z-index:1;left:0}.shape-bg2{right:0}.shape-bg.fade-all .shape-bg1{-webkit-mask-image:linear-gradient(to right,transparent 0%,black 20%,black 100%);mask-image:linear-gradient(to right,transparent 0%,black 20%,black 100%)}.shape-bg.fade-all .shape-bg2{-webkit-mask-image:linear-gradient(to left,transparent 0%,black 20%,black 100%);mask-image:linear-gradient(to left,transparent 0%,black 20%,black 100%)}.shape-bg svg{cursor:grab}.shape-bg svg:active{cursor:grabbing}.profile-img{height:100%;position:relative;flex-shrink:0;background:transparent;margin:0;padding:0;left:0;top:0;border:none;box-sizing:border-box;overflow:hidden;box-shadow:none;outline:none}.profile-img img{transition:transform .3s ease;width:100%;height:100%;object-fit:cover;display:block;object-position:center center;opacity:1;transform-origin:center center;border:none;outline:none;box-shadow:none;position:relative;top:0;left:0;filter:contrast(1.2) brightness(1.1) saturate(1.1);image-rendering:auto}.overlay-container{position:absolute;inset:0;width:100%;height:100%;pointer-events:none;z-index:10;-webkit-user-select:none;user-select:none;overflow:hidden;background:#29252da6;backdrop-filter:blur(.5px);-webkit-backdrop-filter:blur(.5px);--name-label-font-size: 14px}.header-label{position:absolute;top:20px;font-family:Gilroy,sans-serif;text-shadow:0 1px 8px rgba(0,0,0,.8);pointer-events:none}.header-label.left{left:25%;transform:translate(-50%);font-size:var(--name-label-font-size);font-weight:400;color:#fffffff2;text-align:center}.header-label.center{left:50%;transform:translate(-50%);font-size:12px;font-weight:400;color:#fffc;letter-spacing:0}.header-label.right{left:75%;transform:translate(-50%);font-size:var(--name-label-font-size);font-weight:400;color:#fffffff2;text-align:center}.chips-grid{position:absolute;inset:45px 0;overflow-y:auto;overscroll-behavior:none;scrollbar-width:none;box-sizing:border-box}.chips-grid::-webkit-scrollbar{display:none}.force-canvas{position:relative;width:100%;box-sizing:border-box;min-height:120px}.connections-svg{position:absolute;top:0;left:0;width:100%;pointer-events:none;z-index:1}@keyframes lineGlow{0%{opacity:0;stroke-dashoffset:8}30%{opacity:.15}to{opacity:.6;stroke-dashoffset:0}}.connection-line{stroke:#fe3c72d1;stroke-width:1.5px;stroke-dasharray:4;opacity:0;pointer-events:none;transition:opacity .3s ease}.connection-line.active{animation:lineGlow .7s cubic-bezier(.4,0,.2,1) forwards}.frosted-chip{position:absolute;cursor:pointer;pointer-events:auto;transition:transform .2s ease,box-shadow .2s ease,background .2s ease,opacity .3s ease;flex-shrink:0;padding:6px 18px;border-radius:999px;display:inline-flex;align-items:center;justify-content:center;transform:translate(-50%,-50%);z-index:2;border:1px solid rgba(255,255,255,.24);background:#ffffff30;box-shadow:inset 0 1.5px 2px #ffffff59,0 0 0 1px #0f172a38;backdrop-filter:blur(.5px);-webkit-backdrop-filter:blur(.5px)}.frosted-chip:hover{transform:translate(-50%,-50%) scale(1.04);background:#ffffff29;cursor:grab}.frosted-chip.active{background:#ffffff4d;box-shadow:0 0 15px #fe3c7261,inset 0 1.5px 2px #ffffff80,0 0 0 1px #ffffff52;transform:translate(-50%,-50%) scale(1.06);z-index:10;cursor:grabbing}.frosted-chip.active .chip-label{color:#fff!important}.frosted-chip.dimmed{opacity:.35}.frosted-chip .chip-label{position:relative;color:#a8a8a8;text-align:center;font-family:Gilroy,sans-serif;font-size:14px;font-style:normal;font-weight:400;line-height:140%;letter-spacing:-.01em;text-shadow:none;white-space:nowrap;pointer-events:none;z-index:1}.ghost-chip{visibility:hidden!important;pointer-events:none!important}.view-profile{position:absolute;bottom:8px;color:#ffffff80;text-shadow:0 2px 4.2px #000;font-family:Calistoga,serif;font-size:var(--name-label-font-size);font-style:normal;font-weight:400;line-height:normal;cursor:pointer;pointer-events:auto;transition:opacity .2s ease,transform .2s ease}.view-profile:hover{opacity:.85}.view-profile.left{left:8%;width:34%;text-align:left;transform:none}.view-profile.left:hover{transform:translateY(-2px)}.view-profile.right{right:8%;width:34%;text-align:right;left:auto;transform:none}.view-profile.right:hover{transform:translateY(-2px)}#drag-preview{position:absolute;pointer-events:none;display:none;background:#000000b3;color:#fff;padding:5px 10px;border-radius:5px;z-index:9999;max-width:200px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.shape-text-container{width:70px;position:relative;overflow:hidden}.draggable{width:70px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;cursor:grab;-webkit-user-select:none;user-select:none;position:relative;display:inline-block}.dragging{width:auto;text-overflow:clip;cursor:grabbing;position:absolute;z-index:1000}.p-wrapper{width:70px;overflow-x:hidden}.shape-text-center{display:flex;justify-content:space-between;flex-direction:column;gap:10px;height:100%;margin-top:55px}.loading-indicator{position:absolute;inset:0;width:100%;height:100%;background:#00000040;backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px);color:#fff;z-index:100;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12px;pointer-events:auto}.loading-spinner{width:44px;height:44px;border:3.5px solid rgba(255,255,255,.25);border-top:3.5px solid #ffffff;border-radius:50%;animation:spin .8s linear infinite;box-shadow:0 0 15px #fff3}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.loading-indicator p{margin:0;font-size:14px;font-weight:500;color:#ffffffd9;letter-spacing:.02em}.api-key-modal-overlay{position:fixed;inset:0;background:#0f172a99;-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px);display:flex;align-items:center;justify-content:center;z-index:2000}.api-key-modal{width:min(640px,92vw);background:#0f172a;color:#e2e8f0;border:1px solid #334155;border-radius:12px;box-shadow:0 20px 50px #00000073;padding:16px 18px}.api-key-modal-header{display:flex;align-items:center;justify-content:space-between;gap:12px}.api-key-modal-header h2{margin:0;font-size:20px;font-weight:700;color:#e2e8f0}.warning-icon{color:#fbbf24}.api-key-modal .close-btn{background:#0b1220;border:1px solid #334155;color:#94a3b8;border-radius:8px;padding:6px;cursor:pointer;line-height:0}.api-key-modal-body{margin-top:12px;display:flex;flex-direction:column;gap:10px}.modal-message,.modal-instruction{margin:0;color:#cbd5e1}.modal-message strong{color:#e5e7eb}.api-key-input-group{display:flex;align-items:center;gap:10px}.api-key-input{flex:1 1 auto;background:#0a1020;color:#e2e8f0;border:1px solid #334155;border-radius:8px;padding:10px 12px}.api-key-input:focus{outline:none;border-color:#2563eb;box-shadow:0 0 0 3px #2563eb33}.api-key-load-btn{background:#2563eb;color:#fff;border:none;border-radius:8px;padding:10px 12px;cursor:pointer;font-weight:600}.api-key-load-btn:hover{background:#1d4ed8}.modal-hint{display:flex;align-items:center;gap:8px;color:#94a3b8}.modal-hint a{color:#93c5fd}@media(max-width:1023px){.profile-screen{max-width:640px}.profile-flex{height:400px}.profile-img{height:550px}}@media(max-width:767px){.profile-screen,.profile-flex,.overlay-container{width:100%;max-width:100%;overflow-x:hidden;box-sizing:border-box}.profile-screen{border-radius:0}.profile-flex{height:360px}.profile-img{height:510px}.chips-grid{width:100%;max-width:100%;padding:0;top:45px;bottom:50px;box-sizing:border-box}.frosted-chip{padding:4px 10px;max-width:100%;box-sizing:border-box}.frosted-chip .chip-label{font-size:11px;max-width:100%;overflow:hidden;text-overflow:ellipsis}.view-profile{bottom:10px}}@media(max-width:480px){.overlay-container{--name-label-font-size: 12px}.profile-flex{height:340px}.profile-img{height:480px}.frosted-chip{padding:3px 8px}.frosted-chip .chip-label,.header-label.center{font-size:10px}}\n"] }]
|
|
3727
|
+
}], ctorParameters: () => [{ type: ProfileComparisonBackendService }, { type: i0.Renderer2 }, { type: FileConversionService }, { type: ImageCompressionService }, { type: i0.ChangeDetectorRef }, { type: i0.NgZone }, { type: i0.ElementRef }, { type: undefined, decorators: [{
|
|
2582
3728
|
type: Optional
|
|
2583
3729
|
}, {
|
|
2584
3730
|
type: Inject,
|
|
@@ -2643,22 +3789,43 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
|
|
|
2643
3789
|
}], shapeTextCenter: [{
|
|
2644
3790
|
type: ViewChild,
|
|
2645
3791
|
args: ['shapeTextCenter']
|
|
3792
|
+
}], chipsGrid: [{
|
|
3793
|
+
type: ViewChild,
|
|
3794
|
+
args: ['chipsGrid']
|
|
3795
|
+
}], onWindowResize: [{
|
|
3796
|
+
type: HostListener,
|
|
3797
|
+
args: ['window:resize']
|
|
3798
|
+
}], onDocumentClick: [{
|
|
3799
|
+
type: HostListener,
|
|
3800
|
+
args: ['document:click']
|
|
3801
|
+
}], onDocumentDragMove: [{
|
|
3802
|
+
type: HostListener,
|
|
3803
|
+
args: ['document:mousemove', ['$event']]
|
|
3804
|
+
}, {
|
|
3805
|
+
type: HostListener,
|
|
3806
|
+
args: ['document:touchmove', ['$event']]
|
|
3807
|
+
}], onDocumentDragEnd: [{
|
|
3808
|
+
type: HostListener,
|
|
3809
|
+
args: ['document:mouseup']
|
|
3810
|
+
}, {
|
|
3811
|
+
type: HostListener,
|
|
3812
|
+
args: ['document:touchend']
|
|
2646
3813
|
}] } });
|
|
2647
3814
|
|
|
2648
3815
|
class ProfileComparisonLibModule {
|
|
2649
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.
|
|
2650
|
-
static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "20.3.
|
|
3816
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.24", ngImport: i0, type: ProfileComparisonLibModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
|
|
3817
|
+
static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "20.3.24", ngImport: i0, type: ProfileComparisonLibModule, declarations: [ProfileComparisonLibComponent], imports: [CommonModule,
|
|
2651
3818
|
HttpClientModule,
|
|
2652
3819
|
FormsModule,
|
|
2653
3820
|
ReactiveFormsModule], exports: [ProfileComparisonLibComponent] });
|
|
2654
|
-
static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "20.3.
|
|
3821
|
+
static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "20.3.24", ngImport: i0, type: ProfileComparisonLibModule, providers: [
|
|
2655
3822
|
ProfileComparisonBackendService
|
|
2656
3823
|
], imports: [CommonModule,
|
|
2657
3824
|
HttpClientModule,
|
|
2658
3825
|
FormsModule,
|
|
2659
3826
|
ReactiveFormsModule] });
|
|
2660
3827
|
}
|
|
2661
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.
|
|
3828
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.24", ngImport: i0, type: ProfileComparisonLibModule, decorators: [{
|
|
2662
3829
|
type: NgModule,
|
|
2663
3830
|
args: [{
|
|
2664
3831
|
declarations: [
|