@naniteninja/profile-comparison-lib 1.0.3 → 1.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,24 +1,56 @@
1
1
  import * as i0 from '@angular/core';
2
- import { InjectionToken, Injectable, Optional, Inject, EventEmitter, ViewChild, Output, Input, Component, NgModule } from '@angular/core';
3
- import { switchMap, map, catchError, shareReplay, tap as tap$1 } from 'rxjs/operators';
4
- import { from, throwError, of, forkJoin, defer, switchMap as switchMap$1, map as map$1, Observable, BehaviorSubject, tap, retryWhen, concatMap, timer, catchError as catchError$1 } from 'rxjs';
5
- import * as use from '@tensorflow-models/universal-sentence-encoder';
6
- import '@tensorflow/tfjs';
2
+ import { InjectionToken, Injectable, Optional, Inject, inject, EventEmitter, ViewChild, Output, Input, Component, NgModule } from '@angular/core';
3
+ import { switchMap as switchMap$1, map as map$1, catchError, tap as tap$1 } from 'rxjs/operators';
4
+ import { from, switchMap, map, of, Observable, BehaviorSubject, throwError, tap, retryWhen, concatMap, timer, forkJoin, catchError as catchError$1 } from 'rxjs';
7
5
  import * as i1 from '@angular/common/http';
8
- import { HttpHeaders, HttpClientModule } from '@angular/common/http';
6
+ import { HttpHeaders, HttpClient, HttpClientModule } from '@angular/common/http';
9
7
  import * as i2 from '@angular/common';
10
8
  import { CommonModule } from '@angular/common';
9
+ import * as i3 from '@naniteninja/ionic-lib';
10
+ import { LibMarqueeModule } from '@naniteninja/ionic-lib';
11
11
  import { FormsModule, ReactiveFormsModule } from '@angular/forms';
12
12
 
13
+ /** Backend mode for the profile comparison component. */
14
+ var BackendMode;
15
+ (function (BackendMode) {
16
+ /** Use the real deployed backend (default). */
17
+ BackendMode[BackendMode["Real"] = 0] = "Real";
18
+ /** Use a local mock server at localhost:3000. */
19
+ BackendMode[BackendMode["Mock"] = 1] = "Mock";
20
+ })(BackendMode || (BackendMode = {}));
21
+ const BACKEND_MODE_URLS = {
22
+ [BackendMode.Real]: 'https://api.test.thecyrano.app/api/profile-comparison/api',
23
+ [BackendMode.Mock]: 'http://localhost:3000/api',
24
+ };
13
25
  /**
14
- * When provided, ProfileService and OpenAIEmbeddingService call this backend base URL
15
- * (e.g. 'http://localhost:3000/api') for all profile/embedding endpoints instead of
16
- * calling external APIs directly with keys. Can be a string or a getter function for runtime toggle.
17
- * When not provided or getter returns null, the lib uses the direct path with keys.
26
+ * Legacy injection token for the backend base URL.
27
+ * Prefer using the [backendMode] or [backendUrl] @Input on the component instead.
28
+ * Kept for backward compatibility lowest priority when resolving the URL.
18
29
  */
19
30
  const PROFILE_COMPARISON_API_BASE_URL = new InjectionToken('PROFILE_COMPARISON_API_BASE_URL');
20
31
  /** Optional getter: when true, lib and backend service log steps to console for diagnosis. */
21
32
  const PROFILE_COMPARISON_VERBOSE_LOGGING = new InjectionToken('PROFILE_COMPARISON_VERBOSE_LOGGING');
33
+ /**
34
+ * Resolves the active backend URL from inputs, with fallback to injection token.
35
+ *
36
+ * Priority:
37
+ * 1. Explicit backendUrl (custom URL string)
38
+ * 2. BackendMode enum (Real or Mock)
39
+ * 3. Legacy PROFILE_COMPARISON_API_BASE_URL token
40
+ */
41
+ function resolveBackendUrl(backendUrl, backendMode, legacyTokenUrl) {
42
+ if (backendUrl != null && backendUrl.trim().length > 0) {
43
+ return backendUrl.trim();
44
+ }
45
+ const modeUrl = BACKEND_MODE_URLS[backendMode];
46
+ if (modeUrl) {
47
+ return modeUrl;
48
+ }
49
+ if (legacyTokenUrl != null && legacyTokenUrl.trim().length > 0) {
50
+ return legacyTokenUrl.trim();
51
+ }
52
+ return null;
53
+ }
22
54
 
23
55
  const DB_NAME = 'profile-comparison-cache';
24
56
  const DB_VERSION = 1;
@@ -172,788 +204,14 @@ class CachePersistenceService {
172
204
  static STORE_OPENAI_EMBEDDINGS = STORE_OPENAI_EMBEDDINGS;
173
205
  static STORE_PROFILE_FACE = STORE_PROFILE_FACE;
174
206
  static STORE_PROFILE_COMPARE = STORE_PROFILE_COMPARE;
175
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: CachePersistenceService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
176
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: CachePersistenceService, providedIn: 'root' });
207
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: CachePersistenceService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
208
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: CachePersistenceService, providedIn: 'root' });
177
209
  }
178
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: CachePersistenceService, decorators: [{
210
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: CachePersistenceService, decorators: [{
179
211
  type: Injectable,
180
212
  args: [{ providedIn: 'root' }]
181
213
  }] });
182
214
 
183
- class EmbeddingService {
184
- model$ = null;
185
- // Get embedding for a single text (returns Observable)
186
- getEmbedding(text) {
187
- return this.loadModel().pipe(switchMap((model) => from(model.embed([text]))), switchMap((embeddings) => {
188
- const t2d = embeddings;
189
- return from(t2d.array()).pipe(map((arr) => {
190
- t2d.dispose();
191
- return Float32Array.from(arr[0]);
192
- }));
193
- }), catchError((err) => throwError(() => new Error('Model failed to load or embed: ' + err))));
194
- }
195
- /**
196
- * Group-align two lists based on semantic similarity, allowing one-to-many relationships.
197
- *
198
- * Design goals:
199
- * - Semantic relevance takes priority over original order or spacing.
200
- * - Each term in listB is assigned to the single most similar anchor in listA (if above threshold).
201
- * - For an anchor in listA with multiple highly related listB terms, they are emitted as consecutive rows:
202
- * [A, B1], ['-', B2], ['-', B3], ... so they render adjacently and remain visually connected.
203
- * - Low-similarity/unassigned terms from either side are appended at the end and never break groups.
204
- */
205
- groupAlignLists(listA, listB, threshold = 0.25, preserveLeftOrder = true, exclusivityMargin = 0.06, leftCohesion = 0.23) {
206
- // Short-circuit simple cases
207
- if ((!listA || listA.length === 0) && (!listB || listB.length === 0)) {
208
- return of([]);
209
- }
210
- if (!listA || listA.length === 0) {
211
- return of(listB.map((b) => ({ left: '-', right: b, score: 0 })));
212
- }
213
- if (!listB || listB.length === 0) {
214
- return of(listA.map((a) => ({ left: a, right: '-', score: 0 })));
215
- }
216
- return this.loadModel().pipe(switchMap((model) => forkJoin([
217
- from(model.embed(listA)),
218
- from(model.embed(listB))
219
- ]).pipe(switchMap(([tA, tB]) => {
220
- return forkJoin([from(tA.array()), from(tB.array())]).pipe(map((tensorArrays) => {
221
- const [arrA, arrB] = tensorArrays;
222
- // Dispose tensors to avoid memory leaks
223
- tA.dispose();
224
- tB.dispose();
225
- const aCount = listA.length;
226
- const bCount = listB.length;
227
- // Pre-convert to Float32Array for faster cosine computations
228
- const vecA = new Array(aCount);
229
- const vecB = new Array(bCount);
230
- for (let i = 0; i < aCount; i++)
231
- vecA[i] = Float32Array.from(arrA[i]);
232
- for (let j = 0; j < bCount; j++)
233
- vecB[j] = Float32Array.from(arrB[j]);
234
- // Build full cross-list similarity matrix S[i][j] = sim(A_i, B_j)
235
- const S = new Array(aCount);
236
- for (let i = 0; i < aCount; i++) {
237
- const row = new Array(bCount);
238
- for (let j = 0; j < bCount; j++) {
239
- const emb = this.cosineSimilarity(vecA[i], vecB[j]);
240
- const lex = this.computeLexicalBoost(listA[i], listB[j]);
241
- // Apply domain gating to prevent mismatched category-vs-activity alignments
242
- const cap = this.domainCompatibilityCap(listA[i], listB[j]);
243
- row[j] = Math.min(Math.max(emb, lex), cap);
244
- }
245
- S[i] = row;
246
- }
247
- // Build within-list similarity for A (A↔A) to allow left-side extras to attach to cohesive anchors.
248
- // We use the same hybrid semantic/lexical metric.
249
- const SAA = new Array(aCount);
250
- for (let i = 0; i < aCount; i++) {
251
- const rowAA = new Array(aCount);
252
- for (let k = 0; k < aCount; k++) {
253
- if (i === k) {
254
- rowAA[k] = 1; // self
255
- }
256
- else {
257
- const embAA = this.cosineSimilarity(vecA[i], vecA[k]);
258
- const lexAA = this.computeLexicalBoost(listA[i], listA[k]);
259
- rowAA[k] = Math.max(embAA, lexAA);
260
- }
261
- }
262
- SAA[i] = rowAA;
263
- }
264
- // Build thresholded neighborhoods
265
- const neighborsA = new Map();
266
- const neighborsB = new Map();
267
- for (let i = 0; i < aCount; i++) {
268
- const row = [];
269
- for (let j = 0; j < bCount; j++) {
270
- const s = S[i][j];
271
- if (s >= threshold) {
272
- row.push({ j, score: s });
273
- const rev = neighborsB.get(j) || [];
274
- rev.push({ i, score: s });
275
- neighborsB.set(j, rev);
276
- }
277
- }
278
- // sort descending for deterministic preference lists
279
- row.sort((a, b) => b.score - a.score);
280
- if (row.length)
281
- neighborsA.set(i, row);
282
- }
283
- // Identify exclusive vs ambiguous B nodes
284
- const exclusiveForA = new Map();
285
- const ambiguousB = [];
286
- const unassignedB = [];
287
- for (let j = 0; j < bCount; j++) {
288
- const list = (neighborsB.get(j) || []).sort((a, b) => b.score - a.score);
289
- if (list.length === 0) {
290
- unassignedB.push(j);
291
- }
292
- else if (list.length === 1) {
293
- const i = list[0].i;
294
- const arr = exclusiveForA.get(i) || [];
295
- arr.push({ j, score: list[0].score });
296
- exclusiveForA.set(i, arr);
297
- }
298
- else {
299
- // If best is significantly better than second-best, treat as exclusive to best
300
- const best = list[0];
301
- const second = list[1];
302
- if (best.score - second.score >= exclusivityMargin) {
303
- const arr = exclusiveForA.get(best.i) || [];
304
- arr.push({ j, score: best.score });
305
- exclusiveForA.set(best.i, arr);
306
- }
307
- else {
308
- ambiguousB.push(j);
309
- }
310
- }
311
- }
312
- // A-side exclusivity: if an anchor A has a clearly dominant best B, prefer that exclusive pairing.
313
- const bOwner = new Map();
314
- for (const [i, arr] of exclusiveForA.entries()) {
315
- for (const x of arr) {
316
- const ex = bOwner.get(x.j);
317
- if (!ex || x.score > ex.score)
318
- bOwner.set(x.j, { i, score: x.score });
319
- }
320
- }
321
- const ambSetForA = new Set(ambiguousB);
322
- for (let i = 0; i < aCount; i++) {
323
- const list = (neighborsA.get(i) || [])
324
- .slice()
325
- .sort((a, b) => b.score - a.score);
326
- if (!list.length)
327
- continue;
328
- const top = list[0];
329
- const second = list[1];
330
- const hasClearLead = !second || top.score - second.score >= exclusivityMargin;
331
- if (top.score >= threshold && hasClearLead) {
332
- const current = bOwner.get(top.j);
333
- if (!current || top.score > current.score + 1e-6) {
334
- // Reassign this B exclusively to A=i
335
- // Remove from any previous exclusiveForA groups
336
- for (const [ai, arr] of exclusiveForA.entries()) {
337
- exclusiveForA.set(ai, arr.filter((x) => x.j !== top.j));
338
- }
339
- const arr = exclusiveForA.get(i) || [];
340
- if (!arr.some((x) => x.j === top.j))
341
- arr.push({ j: top.j, score: top.score });
342
- arr.sort((u, v) => v.score - u.score);
343
- exclusiveForA.set(i, arr);
344
- bOwner.set(top.j, { i, score: top.score });
345
- ambSetForA.delete(top.j);
346
- }
347
- }
348
- }
349
- ambiguousB.splice(0, ambiguousB.length, ...Array.from(ambSetForA.values()));
350
- // Mutual-best pairing stage (within small tolerance) to prioritize exclusive high-similarity pairs
351
- const mutualTolerance = 0.03;
352
- const bestBForA = new Array(aCount);
353
- for (let i = 0; i < aCount; i++) {
354
- let bBest = -1;
355
- let bBestScore = -Infinity;
356
- for (let j = 0; j < bCount; j++) {
357
- const s = S[i][j];
358
- if (s > bBestScore) {
359
- bBestScore = s;
360
- bBest = j;
361
- }
362
- }
363
- bestBForA[i] = { j: bBest, score: bBestScore };
364
- }
365
- const bestAForB = new Array(bCount);
366
- for (let j = 0; j < bCount; j++) {
367
- let aBest = -1;
368
- let aBestScore = -Infinity;
369
- for (let i = 0; i < aCount; i++) {
370
- const s = S[i][j];
371
- if (s > aBestScore) {
372
- aBestScore = s;
373
- aBest = i;
374
- }
375
- }
376
- bestAForB[j] = { i: aBest, score: aBestScore };
377
- }
378
- // Promote mutual-best pairs to exclusive, removing from ambiguous list if present
379
- const ambSet = new Set(ambiguousB);
380
- for (let i = 0; i < aCount; i++) {
381
- const b = bestBForA[i].j;
382
- if (b >= 0 && S[i][b] >= threshold) {
383
- const a = bestAForB[b].i;
384
- const score = S[i][b];
385
- // within tolerance of best on both sides
386
- const isWithinA = score >= bestBForA[i].score - mutualTolerance;
387
- const isWithinB = score >= bestAForB[b].score - mutualTolerance;
388
- if (a === i && isWithinA && isWithinB) {
389
- // Remove from any previous exclusive lists under other anchors
390
- for (const [ai, arr] of exclusiveForA.entries()) {
391
- exclusiveForA.set(ai, arr.filter((x) => x.j !== b));
392
- }
393
- // Add as exclusive to this anchor
394
- const arr = exclusiveForA.get(i) || [];
395
- // Avoid duplicate insert
396
- if (!arr.some((x) => x.j === b)) {
397
- arr.push({ j: b, score });
398
- arr.sort((u, v) => v.score - u.score);
399
- exclusiveForA.set(i, arr);
400
- }
401
- // Remove from ambiguous set
402
- ambSet.delete(b);
403
- }
404
- }
405
- }
406
- ambiguousB.splice(0, ambiguousB.length, ...Array.from(ambSet.values()));
407
- // Build symmetric anchor heads from mutual-best pairs (above threshold)
408
- const anchorHeads = new Map(); // A_i -> head B_j
409
- for (let i = 0; i < aCount; i++) {
410
- const b = bestBForA[i].j;
411
- if (b >= 0) {
412
- const score = S[i][b];
413
- const isWithinA = score >= bestBForA[i].score - mutualTolerance;
414
- const isWithinB = score >= bestAForB[b].score - mutualTolerance;
415
- if (bestAForB[b].i === i &&
416
- score >= threshold &&
417
- isWithinA &&
418
- isWithinB) {
419
- anchorHeads.set(i, b);
420
- }
421
- }
422
- }
423
- // Resolve ambiguous many-to-many by B-centric greedy assignment:
424
- // each ambiguous B chooses its single most similar A (enables one-to-many on A side).
425
- const matchedAmbigForA = new Map();
426
- for (const j of ambiguousB) {
427
- let bestI = -1;
428
- let bestScore = -Infinity;
429
- for (let i = 0; i < aCount; i++) {
430
- const s = S[i][j];
431
- if (s > bestScore) {
432
- bestScore = s;
433
- bestI = i;
434
- }
435
- }
436
- if (bestI >= 0 && bestScore >= threshold) {
437
- const arr = matchedAmbigForA.get(bestI) || [];
438
- arr.push({ j, score: bestScore });
439
- matchedAmbigForA.set(bestI, arr);
440
- }
441
- }
442
- // Merge exclusive and matched ambiguous into final assignments per A
443
- const assignedToA = new Map();
444
- for (let i = 0; i < aCount; i++) {
445
- const list = [];
446
- const ex = exclusiveForA.get(i) || [];
447
- for (const x of ex)
448
- list.push(x);
449
- const amb = matchedAmbigForA.get(i) || [];
450
- for (const x of amb)
451
- list.push(x);
452
- if (list.length) {
453
- list.sort((a, b) => b.score - a.score);
454
- assignedToA.set(i, list);
455
- }
456
- }
457
- // Promote anchors for left items that have strong assigned matches even if
458
- // they were not mutual-best. This guarantees group creation around
459
- // semantically coherent anchors such as Fish, so items like Clams attach there.
460
- for (const [i, lst] of assignedToA.entries()) {
461
- if (!anchorHeads.has(i) && lst.length) {
462
- anchorHeads.set(i, lst[0].j); // head = top assigned right item
463
- }
464
- }
465
- // Track which B have been assigned (exclusive or ambiguous)
466
- const assignedBSet = new Set();
467
- for (const arr of assignedToA.values()) {
468
- for (const x of arr)
469
- assignedBSet.add(x.j);
470
- }
471
- // Also mark mutual-best anchor head B's as assigned so they are not appended as leftovers
472
- // even if they weren't already in assignedToA for their group.
473
- for (const [ai, headJ] of anchorHeads.entries()) {
474
- assignedBSet.add(headJ);
475
- }
476
- // Any B not assigned (and not a head) goes to the bottom unassigned list
477
- const leftoverB = [];
478
- for (let j = 0; j < bCount; j++) {
479
- if (!assignedBSet.has(j))
480
- leftoverB.push(j);
481
- }
482
- // Determine ranking for each A_i.
483
- // Priority 1: anchors with at least one assigned B (highly related) go to the TOP.
484
- // Priority 2: among them, sort by descending top assigned similarity.
485
- // Anchors with NO assignment are pushed to the BOTTOM and sorted by their best potential similarity.
486
- const rankA = [];
487
- for (let i = 0; i < aCount; i++) {
488
- const assigned = assignedToA.get(i) || [];
489
- let top = 0;
490
- if (assigned.length > 0) {
491
- top = assigned.reduce((m, x) => x.score > m ? x.score : m, 0);
492
- }
493
- else {
494
- // If nothing assigned, consider the best potential similarity (even if below threshold)
495
- let best = 0;
496
- for (let j = 0; j < bCount; j++)
497
- best = Math.max(best, S[i][j]);
498
- top = best;
499
- }
500
- rankA.push({
501
- i,
502
- score: top,
503
- hasAssignment: assigned.length > 0,
504
- });
505
- }
506
- rankA.sort((x, y) => Number(y.hasAssignment) - Number(x.hasAssignment) ||
507
- y.score - x.score);
508
- // Within each group, sort assigned B by descending similarity to the anchor
509
- for (const [i, list] of assignedToA.entries()) {
510
- list.sort((a, b) => b.score - a.score);
511
- assignedToA.set(i, list);
512
- }
513
- // Determine anchor groups strictly from symmetric mutual-best pairs
514
- const anchorSet = new Set(anchorHeads.keys());
515
- // Early-out: if no anchors (no cross-list matches above threshold),
516
- // fall back to simple left-only rows followed by right-only rows
517
- if (anchorSet.size === 0) {
518
- const rowsNoAnchors = [];
519
- for (let i = 0; i < aCount; i++)
520
- rowsNoAnchors.push({
521
- left: listA[i],
522
- right: '-',
523
- score: 0,
524
- });
525
- for (const j of leftoverB)
526
- rowsNoAnchors.push({
527
- left: '-',
528
- right: listB[j],
529
- score: 0,
530
- });
531
- return rowsNoAnchors;
532
- }
533
- // Map each B to its owning anchor A for quick lookup (based on assignedToA)
534
- const ownerOfB = new Array(bCount).fill(-1);
535
- for (const [ai, lst] of assignedToA.entries()) {
536
- for (const x of lst)
537
- ownerOfB[x.j] = ai;
538
- }
539
- const leftUsed = new Set();
540
- // Mark anchors as used so they won't be re-assigned as extras
541
- for (const ai of anchorSet.values())
542
- leftUsed.add(ai);
543
- // Pre-bucket extras: group -> (right j within that group) -> extras[]
544
- const extrasByGroup = new Map();
545
- for (let k = 0; k < aCount; k++) {
546
- if (leftUsed.has(k))
547
- continue; // skip anchors
548
- let bestGroup = -1;
549
- let bestRightJ = -1;
550
- let bestScore = -Infinity;
551
- // Evaluate affinity against each anchor group
552
- for (const ai of anchorSet.values()) {
553
- const groupList = assignedToA.get(ai) || [];
554
- // Ensure the head B is present in the consideration list
555
- const headJ = anchorHeads.get(ai);
556
- const listWithHead = groupList.some((x) => x.j === headJ)
557
- ? groupList
558
- : [{ j: headJ, score: S[ai][headJ] }, ...groupList];
559
- // Best cross-list similarity to any right item under this anchor
560
- let localBestJ = -1;
561
- let localBestCross = -Infinity;
562
- for (const x of listWithHead) {
563
- const s = S[k][x.j];
564
- if (s > localBestCross) {
565
- localBestCross = s;
566
- localBestJ = x.j;
567
- }
568
- }
569
- // Within-list cohesion to the anchor left word
570
- const withinAA = SAA[k][ai];
571
- const combined = Math.max(localBestCross, withinAA);
572
- if (combined > bestScore) {
573
- bestScore = combined;
574
- bestGroup = ai;
575
- // Prefer the concrete right target when cross-list is stronger;
576
- // otherwise fall back to the group's top right assignment.
577
- bestRightJ =
578
- localBestCross >= withinAA ? localBestJ : headJ;
579
- }
580
- }
581
- // Attach as extra if cohesive enough to some group
582
- if (bestGroup >= 0 &&
583
- bestRightJ >= 0 &&
584
- bestScore >= leftCohesion) {
585
- let byRight = extrasByGroup.get(bestGroup);
586
- if (!byRight) {
587
- byRight = new Map();
588
- extrasByGroup.set(bestGroup, byRight);
589
- }
590
- const arr = byRight.get(bestRightJ) || [];
591
- arr.push({ k, jPref: bestRightJ, score: bestScore });
592
- byRight.set(bestRightJ, arr);
593
- leftUsed.add(k);
594
- }
595
- }
596
- // Build grouped rows with left extras rendered directly under the specific right item
597
- // they most strongly match. This ensures highly related words appear exactly below their
598
- // most similar counterpart on the other side.
599
- const rows = [];
600
- const anchorIndices = Array.from(anchorSet.values());
601
- const orderIndices = preserveLeftOrder
602
- ? anchorIndices.sort((x, y) => x - y)
603
- : anchorIndices.sort((i1, i2) => {
604
- const j1 = anchorHeads.get(i1);
605
- const j2 = anchorHeads.get(i2);
606
- const diff = S[i2][j2] - S[i1][j1];
607
- if (Math.abs(diff) > 1e-9)
608
- return diff;
609
- // Side-invariant tie-breaker using symmetric pair key
610
- const key1 = [
611
- listA[i1].toLowerCase(),
612
- listB[j1].toLowerCase(),
613
- ]
614
- .sort()
615
- .join('|');
616
- const key2 = [
617
- listA[i2].toLowerCase(),
618
- listB[j2].toLowerCase(),
619
- ]
620
- .sort()
621
- .join('|');
622
- return key1.localeCompare(key2);
623
- });
624
- for (const i of orderIndices) {
625
- const groupOrig = assignedToA.get(i) || [];
626
- const headJ = anchorHeads.get(i);
627
- // Build group with head first, then others by score
628
- const others = groupOrig
629
- .filter((x) => x.j !== headJ)
630
- .sort((a, b) => {
631
- const d = b.score - a.score;
632
- if (Math.abs(d) > 1e-9)
633
- return d;
634
- return listB[a.j]
635
- .toLowerCase()
636
- .localeCompare(listB[b.j].toLowerCase());
637
- });
638
- const group = [{ j: headJ, score: S[i][headJ] }, ...others];
639
- // First row uses the anchor head pair
640
- const first = group[0];
641
- rows.push({
642
- left: listA[i],
643
- right: listB[first.j],
644
- score: first.score,
645
- });
646
- // Render left extras that prefer this first right item
647
- const byRight = extrasByGroup.get(i);
648
- const extrasForFirst = (byRight && byRight.get(first.j)) || [];
649
- if (extrasForFirst.length) {
650
- extrasForFirst.sort((e1, e2) => {
651
- const s1 = Math.max(S[e1.k][first.j], SAA[e1.k][i]);
652
- const s2 = Math.max(S[e2.k][first.j], SAA[e2.k][i]);
653
- const d = s2 - s1;
654
- if (Math.abs(d) > 1e-9)
655
- return d;
656
- return listA[e1.k]
657
- .toLowerCase()
658
- .localeCompare(listA[e2.k].toLowerCase());
659
- });
660
- for (const ex of extrasForFirst) {
661
- rows.push({
662
- left: listA[ex.k],
663
- right: '-',
664
- score: Math.max(S[ex.k][first.j], SAA[ex.k][i]),
665
- });
666
- }
667
- }
668
- // Render remaining right items with their own left extras directly beneath
669
- for (let idx = 1; idx < group.length; idx++) {
670
- const g = group[idx];
671
- rows.push({ left: '-', right: listB[g.j], score: g.score });
672
- const extrasForJ = (byRight && byRight.get(g.j)) || [];
673
- if (extrasForJ.length) {
674
- extrasForJ.sort((e1, e2) => {
675
- const s1 = Math.max(S[e1.k][g.j], SAA[e1.k][i]);
676
- const s2 = Math.max(S[e2.k][g.j], SAA[e2.k][i]);
677
- const d = s2 - s1;
678
- if (Math.abs(d) > 1e-9)
679
- return d;
680
- return listA[e1.k]
681
- .toLowerCase()
682
- .localeCompare(listA[e2.k].toLowerCase());
683
- });
684
- for (const ex of extrasForJ) {
685
- rows.push({
686
- left: listA[ex.k],
687
- right: '-',
688
- score: Math.max(S[ex.k][g.j], SAA[ex.k][i]),
689
- });
690
- }
691
- }
692
- }
693
- }
694
- // Append leftover left items (not anchors and not attached as extras)
695
- for (let i = 0; i < aCount; i++) {
696
- if (!anchorSet.has(i) && !leftUsed.has(i)) {
697
- rows.push({ left: listA[i], right: '-', score: 0 });
698
- }
699
- }
700
- // Append unassigned B at the end so they do not disrupt highly related groups
701
- for (const j of leftoverB) {
702
- rows.push({ left: '-', right: listB[j], score: 0 });
703
- }
704
- return rows;
705
- }));
706
- }))), catchError((err) => throwError(() => new Error('Failed to create grouped alignment: ' + err))));
707
- }
708
- // Cosine similarity between two vectors
709
- cosineSimilarity(a, b) {
710
- let dot = 0, na = 0, nb = 0;
711
- for (let i = 0; i < a.length; i++) {
712
- dot += a[i] * b[i];
713
- na += a[i] * a[i];
714
- nb += b[i] * b[i];
715
- }
716
- return dot / (Math.sqrt(na) * Math.sqrt(nb) + 1e-12);
717
- }
718
- /**
719
- * Align words in listA with their most semantically similar words in listB
720
- *
721
- * Algorithm:
722
- * 1. Generate embeddings for all words in both lists using Universal Sentence Encoder
723
- * 2. For each word in listA, compare its embedding with all words in listB
724
- * 3. Find the word in listB with highest cosine similarity score
725
- * 4. Return alignment results with similarity scores
726
- *
727
- * @param listA - Source list of words to align
728
- * @param listB - Target list of words to find matches in
729
- * @returns Observable of alignment results with word, alignedWith, score, and index
730
- */
731
- alignLists(listA, listB) {
732
- return this.loadModel().pipe(
733
- // Step 1: Generate embeddings for both word lists simultaneously
734
- switchMap((model) => forkJoin([
735
- from(model.embed(listA)),
736
- from(model.embed(listB))
737
- ]).pipe(
738
- // Step 2: Convert tensor embeddings to JavaScript arrays
739
- switchMap(([tA, tB]) => {
740
- return forkJoin([from(tA.array()), from(tB.array())]).pipe(map((tensorArrays) => {
741
- const [arrA, arrB] = tensorArrays;
742
- // Clean up TensorFlow tensors to prevent memory leaks
743
- tA.dispose();
744
- tB.dispose();
745
- // For each word in listA, find its best match in listB
746
- const results = listA.map((wordA, i) => {
747
- // Get the embedding vector for current word from listA
748
- const vecA = Float32Array.from(arrA[i]);
749
- // Initialize variables to track the best match
750
- let bestScore = -Infinity;
751
- let bestMatch = null;
752
- let bestIndex = -1;
753
- // Compare current word from listA with all words in listB
754
- for (let j = 0; j < listB.length; j++) {
755
- // Get the embedding vector for current word from listB
756
- const vecB = Float32Array.from(arrB[j]);
757
- // Calculate semantic similarity using cosine similarity
758
- const score = this.cosineSimilarity(vecA, vecB);
759
- // Update best match if this similarity is higher
760
- if (score > bestScore) {
761
- bestScore = score;
762
- bestMatch = listB[j];
763
- bestIndex = j;
764
- }
765
- }
766
- // Return alignment result for this word
767
- return {
768
- word: wordA, // Original word from listA
769
- alignedWith: bestMatch, // Best matching word from listB
770
- score: bestScore, // Similarity score (0-1, higher is better)
771
- index: bestIndex, // Index of best match in listB
772
- };
773
- });
774
- // Return all alignment results
775
- return results;
776
- }));
777
- }))), catchError((err) => throwError(() => new Error('Model not loaded or embedding failed: ' + err))));
778
- }
779
- /**
780
- * Find best matching pairs between two lists above a similarity threshold
781
- *
782
- * @param listA - Source list of words
783
- * @param listB - Target list of words to match against
784
- * @param threshold - Minimum similarity score (0-1, default: 0.15)
785
- * @returns Observable of word pairs with similarity scores
786
- */
787
- findBestMatchingPairs(listA, listB, threshold = 0.15) {
788
- return this.alignLists(listA, listB).pipe(map((alignments) => {
789
- const usedB = new Set();
790
- const pairs = [];
791
- // Sort by similarity score (highest first)
792
- alignments.sort((a, b) => b.score - a.score);
793
- // Debug logging for travel/journey matching
794
- console.log('Embedding Service - All alignments:', alignments.map((alignment) => ({
795
- word: alignment.word,
796
- alignedWith: alignment.alignedWith,
797
- score: alignment.score.toFixed(3),
798
- })));
799
- for (const alignment of alignments) {
800
- if (alignment.score >= threshold && !usedB.has(alignment.index)) {
801
- pairs.push({
802
- wordA: alignment.word,
803
- wordB: alignment.alignedWith,
804
- score: alignment.score,
805
- });
806
- usedB.add(alignment.index);
807
- // Special logging for travel-related matches
808
- if (alignment.word.toLowerCase().includes('travel') ||
809
- alignment.word.toLowerCase().includes('journey') ||
810
- alignment.alignedWith.toLowerCase().includes('travel') ||
811
- alignment.alignedWith.toLowerCase().includes('journey')) {
812
- console.log('🎯 Travel/Journey Match Found:', {
813
- wordA: alignment.word,
814
- wordB: alignment.alignedWith,
815
- score: alignment.score.toFixed(3),
816
- threshold: threshold,
817
- });
818
- }
819
- }
820
- }
821
- return pairs;
822
- }), catchError((err) => throwError(() => new Error('Failed to find best matching pairs: ' + err))));
823
- }
824
- // Calculate similarity between two texts (returns Observable)
825
- calculateSimilarity(primaryText, comparisonText) {
826
- return this.getEmbedding(primaryText).pipe(switchMap((emb1) => this.getEmbedding(comparisonText).pipe(map((emb2) => {
827
- const similarity = this.cosineSimilarity(emb1, emb2);
828
- // Debug logging for travel/journey similarity
829
- if (primaryText.toLowerCase().includes('travel') ||
830
- primaryText.toLowerCase().includes('journey') ||
831
- comparisonText.toLowerCase().includes('travel') ||
832
- comparisonText.toLowerCase().includes('journey')) {
833
- console.log('🔍 Travel/Journey Similarity:', {
834
- primaryText,
835
- comparisonText,
836
- similarity: similarity.toFixed(3),
837
- });
838
- }
839
- return similarity;
840
- }))), catchError((err) => throwError(() => new Error('Failed to calculate similarity: ' + err))));
841
- }
842
- // Test method to check specific word similarities (Observable only)
843
- testWordSimilarity(word1, word2) {
844
- return this.calculateSimilarity(word1, word2).pipe(map((similarity) => {
845
- console.log(`📊 Similarity between "${word1}" and "${word2}": ${(similarity * 100).toFixed(1)}%`);
846
- }), catchError((error) => {
847
- console.error('Error testing word similarity:', error);
848
- return of();
849
- }));
850
- }
851
- // Private methods
852
- // Domain compatibility cap to avoid mismatched category-versus-activity pairings.
853
- // Example: "Fish" (food/animal noun) vs "Bird watching" (activity) should not align.
854
- // Returns a cap in [0,1] representing the maximum allowed similarity for the pair.
855
- // Values below the groupAlignLists() threshold (default 0.25) effectively block alignment.
856
- domainCompatibilityCap(a, b) {
857
- const norm = (s) => s
858
- .toLowerCase()
859
- .replace(/[_-]+/g, ' ')
860
- .replace(/[^a-z\s]/g, ' ')
861
- .replace(/\s+/g, ' ')
862
- .trim();
863
- const A = norm(a);
864
- const B = norm(b);
865
- const watching = /\b[a-z]+\s*watching\b|\bbirdwatching\b/;
866
- const genericActivities = /\b(hiking|reading|painting|photography|gardening|blogging|writing|gaming|chess|puzzles|travel(?:ing|ling)?|programming|coding|language learning|pottery|clay\s*model(?:ing|ling))\b/;
867
- const isActivity = (s) => watching.test(s) || genericActivities.test(s);
868
- const foodAnimal = /\b(fish|sea\s*food|seafood|shell\s*fish|shellfish|clam|clams|mussel|mussels|oyster|oysters|scallop|scallops|shrimp|prawn|prawns|lobster|crab|meat|beef|pork|chicken|egg|eggs)\b/;
869
- const isFoodAnimal = (s) => foodAnimal.test(s);
870
- const aAct = isActivity(A);
871
- const bAct = isActivity(B);
872
- const aFood = isFoodAnimal(A);
873
- const bFood = isFoodAnimal(B);
874
- if ((aAct && bFood) || (bAct && aFood)) {
875
- // Cap below default threshold so these pairs never form groups
876
- return 0.2;
877
- }
878
- return 1.0;
879
- }
880
- // Lightweight lexical similarity booster to catch cases like "AI/ML" ↔ "AI machine learning"
881
- // Uses token normalization + targeted synonym/expansion rules and token Jaccard.
882
- computeLexicalBoost(a, b) {
883
- const expand = (s) => {
884
- let x = s.toLowerCase();
885
- // Normalize separators
886
- x = x.replace(/[_-]+/g, ' ');
887
- // Targeted expansions
888
- x = x.replace(/ai\s*\/\s*ml|ai\s*[- ]?ml/g, 'ai machine learning');
889
- x = x.replace(/\bml\b/g, 'machine learning');
890
- x = x.replace(/web\s*dev(elopment)?/g, 'web development');
891
- // Domain expansions – culinary/food categories (to help cases like Fish ↔ Clams)
892
- x = x.replace(/\bsea\s*food\b/g, 'seafood fish shellfish');
893
- x = x.replace(/\bshell\s*fish\b/g, 'shellfish clams mussels oysters scallops seafood');
894
- x = x.replace(/\bfish(es)?\b/g, 'fish seafood');
895
- x = x.replace(/\bclam(s)?\b/g, 'clams shellfish seafood');
896
- x = x.replace(/\bmussel(s)?\b/g, 'mussels shellfish seafood');
897
- x = x.replace(/\boyster(s)?\b/g, 'oysters shellfish seafood');
898
- x = x.replace(/\bscallop(s)?\b/g, 'scallops shellfish seafood');
899
- x = x.replace(/\bshrimp(s)?\b/g, 'shrimp seafood');
900
- x = x.replace(/\bprawn(s)?\b/g, 'prawns shrimp seafood');
901
- x = x.replace(/\blobster(s)?\b/g, 'lobster seafood');
902
- x = x.replace(/\bcrab(s)?\b/g, 'crab seafood');
903
- x = x.replace(/ui\s*\/\s*ux|ui\s*[- ]?ux/g, 'ui ux design');
904
- return x;
905
- };
906
- const normalize = (s) => {
907
- const x = expand(s)
908
- .replace(/[^a-z0-9\s]/g, ' ')
909
- .replace(/\s+/g, ' ')
910
- .trim();
911
- return x;
912
- };
913
- const A = normalize(a);
914
- const B = normalize(b);
915
- if (!A || !B)
916
- return 0;
917
- // Strong containment boost (e.g., "ai machine learning" contains "machine learning")
918
- if (A.includes(B) || B.includes(A)) {
919
- return Math.max(0.75, Math.min(0.95, Math.max(A.length, B.length) > 12 ? 0.85 : 0.78));
920
- }
921
- const toTokens = (s) => s.split(' ').filter(Boolean);
922
- const setFrom = (arr) => new Set(arr);
923
- const tA = toTokens(A);
924
- const tB = toTokens(B);
925
- if (!tA.length || !tB.length)
926
- return 0;
927
- const sA = setFrom(tA);
928
- const sB = setFrom(tB);
929
- let inter = 0;
930
- for (const t of sA)
931
- if (sB.has(t))
932
- inter++;
933
- const union = sA.size + sB.size - inter;
934
- const jaccard = union > 0 ? inter / union : 0;
935
- // Small extra for sharing big tokens like "learning"/"development" with qualifiers
936
- const longTokenBonus = Math.min(0.1, [...sA].filter((t) => t.length >= 6 && sB.has(t)).length * 0.03);
937
- const score = Math.min(0.95, jaccard + longTokenBonus);
938
- return score;
939
- }
940
- // Lazy load the model only once, as observable
941
- loadModel() {
942
- if (!this.model$) {
943
- this.model$ = defer(() => from(use.load())).pipe(shareReplay(1));
944
- }
945
- return this.model$;
946
- }
947
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: EmbeddingService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
948
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: EmbeddingService, providedIn: 'root' });
949
- }
950
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: EmbeddingService, decorators: [{
951
- type: Injectable,
952
- args: [{
953
- providedIn: 'root',
954
- }]
955
- }] });
956
-
957
215
  class FileConversionService {
958
216
  static MIME_TYPE_JPEG = 'image/jpeg';
959
217
  static MIME_TYPE_PNG = 'image/png';
@@ -963,7 +221,7 @@ class FileConversionService {
963
221
  static FILE_EXTENSION_PNG = '.png';
964
222
  static FILE_EXTENSION_WEBP = '.webp';
965
223
  urlToFile(url, fileName, mimeType) {
966
- return from(fetch(url)).pipe(switchMap$1(response => from(response.blob())), map$1(blob => new File([blob], fileName, { type: mimeType })));
224
+ return from(fetch(url)).pipe(switchMap(response => from(response.blob())), map(blob => new File([blob], fileName, { type: mimeType })));
967
225
  }
968
226
  isDataUrl(str) {
969
227
  return typeof str === 'string' && str.startsWith('data:');
@@ -995,10 +253,10 @@ class FileConversionService {
995
253
  const mime = this.getMimeFromFilename(imageStr);
996
254
  return this.urlToFile(imageStr, fileBaseName, mime);
997
255
  }
998
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: FileConversionService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
999
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: FileConversionService, providedIn: 'root' });
256
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: FileConversionService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
257
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: FileConversionService, providedIn: 'root' });
1000
258
  }
1001
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: FileConversionService, decorators: [{
259
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: FileConversionService, decorators: [{
1002
260
  type: Injectable,
1003
261
  args: [{
1004
262
  providedIn: 'root'
@@ -1008,7 +266,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
1008
266
  class ImageCompressionService {
1009
267
  compressImageFile(file, config, overrides = {}) {
1010
268
  const cfg = { ...config, ...overrides };
1011
- return this.loadImageFromFile(file).pipe(switchMap$1((img) => {
269
+ return this.loadImageFromFile(file).pipe(switchMap((img) => {
1012
270
  let width = img.naturalWidth || img.width;
1013
271
  let height = img.naturalHeight || img.height;
1014
272
  const scale = Math.min(cfg.maxWidth / width, cfg.maxHeight / height, 1);
@@ -1024,7 +282,7 @@ class ImageCompressionService {
1024
282
  };
1025
283
  const process = (w, h, q, attempts) => {
1026
284
  render(w, h);
1027
- return this.canvasToBlob(canvas, cfg.format, q).pipe(switchMap$1((blob) => {
285
+ return this.canvasToBlob(canvas, cfg.format, q).pipe(switchMap((blob) => {
1028
286
  if (blob.size > cfg.maxBytes && attempts < 25) {
1029
287
  if (q > cfg.qualityMin + 0.005) {
1030
288
  return process(w, h, Math.max(cfg.qualityMin, q - cfg.qualityStep), attempts + 1);
@@ -1040,10 +298,10 @@ class ImageCompressionService {
1040
298
  return of(blob);
1041
299
  }));
1042
300
  };
1043
- return process(width, height, cfg.qualityStart, 0).pipe(switchMap$1((finalBlob) => {
301
+ return process(width, height, cfg.qualityStart, 0).pipe(switchMap((finalBlob) => {
1044
302
  const newName = this.renameFileForFormat(file.name, cfg.format);
1045
303
  const compressedFile = new File([finalBlob], newName, { type: cfg.format, lastModified: Date.now() });
1046
- return this.blobToDataURL(finalBlob).pipe(map$1((dataUrl) => ({ file: compressedFile, dataUrl })));
304
+ return this.blobToDataURL(finalBlob).pipe(map((dataUrl) => ({ file: compressedFile, dataUrl })));
1047
305
  }));
1048
306
  }));
1049
307
  }
@@ -1114,10 +372,10 @@ class ImageCompressionService {
1114
372
  return `${base}.webp`;
1115
373
  return `${base}.jpg`;
1116
374
  }
1117
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: ImageCompressionService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1118
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: ImageCompressionService, providedIn: 'root' });
375
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: ImageCompressionService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
376
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: ImageCompressionService, providedIn: 'root' });
1119
377
  }
1120
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: ImageCompressionService, decorators: [{
378
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: ImageCompressionService, decorators: [{
1121
379
  type: Injectable,
1122
380
  args: [{
1123
381
  providedIn: 'root'
@@ -1228,7 +486,7 @@ class OpenAIEmbeddingService {
1228
486
  if (validA.length === 0 && validB.length === 0)
1229
487
  return of({ listA: [], listB: [] });
1230
488
  const key = JSON.stringify({ a: validA, b: validB, v: 'spacer-v35' });
1231
- return from(this.loadSpacerCache()).pipe(switchMap(() => {
489
+ return from(this.loadSpacerCache()).pipe(switchMap$1(() => {
1232
490
  if (this.spacerAlignmentCache.has(key)) {
1233
491
  console.log('Alignment cache hit');
1234
492
  return of(this.spacerAlignmentCache.get(key));
@@ -1240,7 +498,7 @@ class OpenAIEmbeddingService {
1240
498
  .post(url, { listA: validA, listB: validB }, {
1241
499
  headers: new HttpHeaders({ 'Content-Type': 'application/json' }),
1242
500
  })
1243
- .pipe(map((result) => {
501
+ .pipe(map$1((result) => {
1244
502
  const finalResult = { listA: result.listA || [], listB: result.listB || [] };
1245
503
  this.spacerAlignmentCache.set(key, finalResult);
1246
504
  this.persistence.set(CachePersistenceService.STORE_OPENAI_SPACER, key, finalResult);
@@ -1362,7 +620,7 @@ class OpenAIEmbeddingService {
1362
620
  temperature: 0.0, // Zero temperature for maximum determinism
1363
621
  seed: 42 // Fixed seed for reproducibility
1364
622
  };
1365
- return this.http.post(this.API_URL, body, { headers }).pipe(map(response => {
623
+ return this.http.post(this.API_URL, body, { headers }).pipe(map$1(response => {
1366
624
  try {
1367
625
  const content = response.choices[0].message.content;
1368
626
  console.log('OpenAI LLM Output (getAlignedLists):', content);
@@ -1520,7 +778,7 @@ class OpenAIEmbeddingService {
1520
778
  return of([]);
1521
779
  const keyDirect = JSON.stringify({ a: validA, b: validB });
1522
780
  const keyReverse = JSON.stringify({ a: validB, b: validA });
1523
- return from(this.loadAlignmentCache()).pipe(switchMap(() => {
781
+ return from(this.loadAlignmentCache()).pipe(switchMap$1(() => {
1524
782
  if (this.alignmentCache.has(keyDirect)) {
1525
783
  console.log('Alignment cache hit (Direct)');
1526
784
  return of(this.alignmentCache.get(keyDirect).rows);
@@ -1589,7 +847,7 @@ class OpenAIEmbeddingService {
1589
847
  temperature: 0.2
1590
848
  };
1591
849
  console.log('Sending single alignment request to OpenAI...');
1592
- return this.http.post(this.API_URL, body, { headers }).pipe(map(response => {
850
+ return this.http.post(this.API_URL, body, { headers }).pipe(map$1(response => {
1593
851
  try {
1594
852
  const content = response.choices[0].message.content;
1595
853
  console.log('OpenAI LLM Output (groupAlignLists):', content);
@@ -1767,7 +1025,7 @@ class OpenAIEmbeddingService {
1767
1025
  return of(0);
1768
1026
  }
1769
1027
  const simKey = [a, b].sort().join('\0');
1770
- return from(this.loadSimilarityCache()).pipe(switchMap(() => {
1028
+ return from(this.loadSimilarityCache()).pipe(switchMap$1(() => {
1771
1029
  if (this.similarityCache.has(simKey)) {
1772
1030
  return of(this.similarityCache.get(simKey));
1773
1031
  }
@@ -1778,7 +1036,7 @@ class OpenAIEmbeddingService {
1778
1036
  .post(url, { textA: a, textB: b }, {
1779
1037
  headers: new HttpHeaders({ 'Content-Type': 'application/json' }),
1780
1038
  })
1781
- .pipe(map((res) => {
1039
+ .pipe(map$1((res) => {
1782
1040
  const similarity = res.similarity ?? 0;
1783
1041
  this.similarityCache.set(simKey, similarity);
1784
1042
  this.persistence.set(CachePersistenceService.STORE_OPENAI_SIMILARITY, simKey, similarity);
@@ -1797,7 +1055,7 @@ class OpenAIEmbeddingService {
1797
1055
  model: 'text-embedding-3-small',
1798
1056
  input: [a, b]
1799
1057
  };
1800
- return this.http.post(EMBEDDINGS_URL, body, { headers }).pipe(map(res => {
1058
+ return this.http.post(EMBEDDINGS_URL, body, { headers }).pipe(map$1(res => {
1801
1059
  const data = res?.data;
1802
1060
  if (!Array.isArray(data) || data.length < 2) {
1803
1061
  throw new Error('Invalid embeddings response from OpenAI');
@@ -1824,7 +1082,7 @@ class OpenAIEmbeddingService {
1824
1082
  if (!texts || texts.length === 0)
1825
1083
  return of([]);
1826
1084
  const embKey = JSON.stringify(texts);
1827
- return from(this.loadEmbeddingsCache()).pipe(switchMap(() => {
1085
+ return from(this.loadEmbeddingsCache()).pipe(switchMap$1(() => {
1828
1086
  if (this.embeddingsCache.has(embKey)) {
1829
1087
  return of(this.embeddingsCache.get(embKey));
1830
1088
  }
@@ -1835,7 +1093,7 @@ class OpenAIEmbeddingService {
1835
1093
  .post(url, { input: texts }, {
1836
1094
  headers: new HttpHeaders({ 'Content-Type': 'application/json' }),
1837
1095
  })
1838
- .pipe(map((res) => {
1096
+ .pipe(map$1((res) => {
1839
1097
  const result = (res.data || []).map((item) => item.embedding || []);
1840
1098
  this.embeddingsCache.set(embKey, result);
1841
1099
  this.persistence.set(CachePersistenceService.STORE_OPENAI_EMBEDDINGS, embKey, result);
@@ -1854,7 +1112,7 @@ class OpenAIEmbeddingService {
1854
1112
  model: 'text-embedding-3-small',
1855
1113
  input: texts
1856
1114
  };
1857
- return this.http.post(EMBEDDINGS_URL, body, { headers }).pipe(map(res => {
1115
+ return this.http.post(EMBEDDINGS_URL, body, { headers }).pipe(map$1(res => {
1858
1116
  const data = res?.data;
1859
1117
  if (!Array.isArray(data)) {
1860
1118
  throw new Error('Invalid embeddings response from OpenAI');
@@ -1896,10 +1154,10 @@ class OpenAIEmbeddingService {
1896
1154
  this.persistence.clear(CachePersistenceService.STORE_OPENAI_SIMILARITY);
1897
1155
  this.persistence.clear(CachePersistenceService.STORE_OPENAI_EMBEDDINGS);
1898
1156
  }
1899
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: OpenAIEmbeddingService, deps: [{ token: i1.HttpClient }, { token: PROFILE_COMPARISON_API_BASE_URL, optional: true }, { token: CachePersistenceService }], target: i0.ɵɵFactoryTarget.Injectable });
1900
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: OpenAIEmbeddingService, providedIn: 'root' });
1157
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: OpenAIEmbeddingService, deps: [{ token: i1.HttpClient }, { token: PROFILE_COMPARISON_API_BASE_URL, optional: true }, { token: CachePersistenceService }], target: i0.ɵɵFactoryTarget.Injectable });
1158
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: OpenAIEmbeddingService, providedIn: 'root' });
1901
1159
  }
1902
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: OpenAIEmbeddingService, decorators: [{
1160
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: OpenAIEmbeddingService, decorators: [{
1903
1161
  type: Injectable,
1904
1162
  args: [{
1905
1163
  providedIn: 'root'
@@ -1931,10 +1189,10 @@ class ProfileComparisonLibService {
1931
1189
  getVersion() {
1932
1190
  return '0.0.0';
1933
1191
  }
1934
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: ProfileComparisonLibService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1935
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: ProfileComparisonLibService, providedIn: 'root' });
1192
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: ProfileComparisonLibService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1193
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: ProfileComparisonLibService, providedIn: 'root' });
1936
1194
  }
1937
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: ProfileComparisonLibService, decorators: [{
1195
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: ProfileComparisonLibService, decorators: [{
1938
1196
  type: Injectable,
1939
1197
  args: [{
1940
1198
  providedIn: 'root'
@@ -2023,7 +1281,7 @@ class ProfileService {
2023
1281
  const a = (text_1 ?? '').trim();
2024
1282
  const b = (text_2 ?? '').trim();
2025
1283
  const cacheKey = a && b ? [a, b].sort().join('\0') : '';
2026
- return from(this.loadCompareCache()).pipe(switchMap(() => {
1284
+ return from(this.loadCompareCache()).pipe(switchMap$1(() => {
2027
1285
  if (cacheKey && this.compareInterestsCache.has(cacheKey)) {
2028
1286
  return of(this.compareInterestsCache.get(cacheKey));
2029
1287
  }
@@ -2054,7 +1312,7 @@ class ProfileService {
2054
1312
  detectFace(image, creds, options) {
2055
1313
  const rawKey = options?.cacheKey ?? (typeof image === 'string' ? image : undefined);
2056
1314
  const cacheKey = ProfileService.faceCacheKey(rawKey);
2057
- return from(this.loadFaceCache()).pipe(switchMap(() => {
1315
+ return from(this.loadFaceCache()).pipe(switchMap$1(() => {
2058
1316
  if (cacheKey && this.faceCache.has(cacheKey)) {
2059
1317
  console.log('Face detection cache hit');
2060
1318
  return of(this.faceCache.get(cacheKey));
@@ -2126,7 +1384,7 @@ class ProfileService {
2126
1384
  }
2127
1385
  // Propagate other errors (or second failure) to the caller
2128
1386
  return throwError(() => error);
2129
- }))), map$1((raw) => {
1387
+ }))), map((raw) => {
2130
1388
  // Face++ sometimes returns HTTP 200 with an error payload containing `error_message`
2131
1389
  // e.g. "AUTHENTICATION_ERROR: api_key and api_secret does not match." or
2132
1390
  // "AUTHORIZATION_ERROR:<reason>" or "CONCURRENCY_LIMIT_EXCEEDED".
@@ -2188,10 +1446,10 @@ class ProfileService {
2188
1446
  this.compareInterestsCache.clear();
2189
1447
  this.persistence.clear(CachePersistenceService.STORE_PROFILE_COMPARE);
2190
1448
  }
2191
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: ProfileService, deps: [{ token: i1.HttpClient }, { token: PROFILE_COMPARISON_API_BASE_URL, optional: true }, { token: CachePersistenceService }], target: i0.ɵɵFactoryTarget.Injectable });
2192
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: ProfileService, providedIn: 'root' });
1449
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: ProfileService, deps: [{ token: i1.HttpClient }, { token: PROFILE_COMPARISON_API_BASE_URL, optional: true }, { token: CachePersistenceService }], target: i0.ɵɵFactoryTarget.Injectable });
1450
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: ProfileService, providedIn: 'root' });
2193
1451
  }
2194
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: ProfileService, decorators: [{
1452
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: ProfileService, decorators: [{
2195
1453
  type: Injectable,
2196
1454
  args: [{ providedIn: 'root' }]
2197
1455
  }], ctorParameters: () => [{ type: i1.HttpClient }, { type: undefined, decorators: [{
@@ -2206,27 +1464,34 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
2206
1464
  * The lib does not call third-party APIs; the backend does.
2207
1465
  */
2208
1466
  class ProfileComparisonBackendService {
2209
- http;
2210
- apiBaseUrl;
2211
- getVerboseLogging;
2212
- constructor(http, apiBaseUrl, getVerboseLogging) {
2213
- this.http = http;
2214
- this.apiBaseUrl = apiBaseUrl;
2215
- this.getVerboseLogging = getVerboseLogging;
1467
+ http = inject(HttpClient);
1468
+ apiBaseUrl = inject(PROFILE_COMPARISON_API_BASE_URL, { optional: true });
1469
+ getVerboseLogging = inject(PROFILE_COMPARISON_VERBOSE_LOGGING, { optional: true });
1470
+ constructor() { }
1471
+ static extractErrorMessage(err) {
1472
+ if (err == null || typeof err !== 'object')
1473
+ return 'Unknown error';
1474
+ const e = err;
1475
+ return e.error?.message || e.message || 'Unknown error';
2216
1476
  }
2217
1477
  log(...args) {
2218
1478
  if (this.getVerboseLogging?.() === true) {
2219
1479
  console.log('[ProfileComparisonBackend]', ...args);
2220
1480
  }
2221
1481
  }
2222
- getBaseUrl() {
1482
+ /** Returns the legacy token URL (for backward compat fallback). */
1483
+ getLegacyTokenUrl() {
2223
1484
  const u = this.apiBaseUrl;
2224
1485
  if (u == null)
2225
1486
  return null;
2226
1487
  return typeof u === 'function' ? u() : u;
2227
1488
  }
2228
- getComparison(config) {
2229
- const base = this.getBaseUrl();
1489
+ /** @deprecated Use getLegacyTokenUrl(). Kept for backward compat. */
1490
+ getBaseUrl() {
1491
+ return this.getLegacyTokenUrl();
1492
+ }
1493
+ getComparison(config, baseUrl) {
1494
+ const base = baseUrl ?? this.getLegacyTokenUrl();
2230
1495
  this.log('getComparison', 'baseUrl', base ?? '(null)');
2231
1496
  if (!base || !base.trim()) {
2232
1497
  this.log('getComparison', 'returning error: backend not configured');
@@ -2236,29 +1501,17 @@ class ProfileComparisonBackendService {
2236
1501
  this.log('getComparison', 'POST', url);
2237
1502
  return this.http.post(url, { config }).pipe(tap$1(() => this.log('getComparison', 'POST success')), catchError((err) => {
2238
1503
  this.log('getComparison', 'POST error', err);
2239
- const msg = err?.error?.message ?? err?.message ?? err?.statusText ?? String(err);
2240
- const status = err?.status ?? err?.statusCode;
2241
- console.error('[ProfileComparisonBackend] getComparison failed', status, msg);
1504
+ console.error('[ProfileComparisonBackend] getComparison failed', err?.status, ProfileComparisonBackendService.extractErrorMessage(err));
2242
1505
  return throwError(() => err);
2243
1506
  }));
2244
1507
  }
2245
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: ProfileComparisonBackendService, deps: [{ token: i1.HttpClient }, { token: PROFILE_COMPARISON_API_BASE_URL, optional: true }, { token: PROFILE_COMPARISON_VERBOSE_LOGGING, optional: true }], target: i0.ɵɵFactoryTarget.Injectable });
2246
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: ProfileComparisonBackendService, providedIn: 'root' });
1508
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: ProfileComparisonBackendService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1509
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: ProfileComparisonBackendService, providedIn: 'root' });
2247
1510
  }
2248
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: ProfileComparisonBackendService, decorators: [{
1511
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: ProfileComparisonBackendService, decorators: [{
2249
1512
  type: Injectable,
2250
1513
  args: [{ providedIn: 'root' }]
2251
- }], ctorParameters: () => [{ type: i1.HttpClient }, { type: undefined, decorators: [{
2252
- type: Optional
2253
- }, {
2254
- type: Inject,
2255
- args: [PROFILE_COMPARISON_API_BASE_URL]
2256
- }] }, { type: undefined, decorators: [{
2257
- type: Optional
2258
- }, {
2259
- type: Inject,
2260
- args: [PROFILE_COMPARISON_VERBOSE_LOGGING]
2261
- }] }] });
1514
+ }], ctorParameters: () => [] });
2262
1515
 
2263
1516
  class ProfileComparisonLibComponent {
2264
1517
  backendService;
@@ -2281,6 +1534,7 @@ class ProfileComparisonLibComponent {
2281
1534
  static DEFAULT_MAX_HEIGHT = 1600;
2282
1535
  static SPACER_SMALL = '-';
2283
1536
  static SPACER_LARGE = '----';
1537
+ static PLACEHOLDER = '--';
2284
1538
  static CONSOLIDATION_SEPARATOR_X = '×';
2285
1539
  static CONSOLIDATION_SEPARATOR_SLASH = '/';
2286
1540
  static FACEPP_RATE_LIMIT_DELAY_MS = 5000;
@@ -2305,8 +1559,12 @@ class ProfileComparisonLibComponent {
2305
1559
  person3Interests: [],
2306
1560
  user1Image: '',
2307
1561
  user2Image: '',
1562
+ edgeShading: true,
1563
+ shadingColor: 'rgba(44, 40, 47, 0.7)'
2308
1564
  };
2309
- fadeAllEdges = false;
1565
+ backendMode = BackendMode.Real;
1566
+ backendUrl = null;
1567
+ fadeAllEdges = true;
2310
1568
  matrixDataChange = new EventEmitter();
2311
1569
  rawLLMOutputChange = new EventEmitter();
2312
1570
  viewProfileClick = new EventEmitter();
@@ -2350,6 +1608,8 @@ class ProfileComparisonLibComponent {
2350
1608
  alignedPerson2Interests = [];
2351
1609
  leftProfileClicked = false;
2352
1610
  rightProfileClicked = false;
1611
+ leftShapeAnimating = false;
1612
+ rightShapeAnimating = false;
2353
1613
  sortedA = [];
2354
1614
  sortedB = [];
2355
1615
  sortedC = [];
@@ -2358,6 +1618,8 @@ class ProfileComparisonLibComponent {
2358
1618
  matrixData = null;
2359
1619
  /** When false, backend URL is not provided — show "Configure backend". */
2360
1620
  backendConfigured = false;
1621
+ /** The resolved backend URL computed from inputs and legacy token. */
1622
+ resolvedBackendUrl = null;
2361
1623
  compressionConfig = {
2362
1624
  maxWidth: ProfileComparisonLibComponent.DEFAULT_MAX_WIDTH,
2363
1625
  maxHeight: ProfileComparisonLibComponent.DEFAULT_MAX_HEIGHT,
@@ -2375,15 +1637,11 @@ class ProfileComparisonLibComponent {
2375
1637
  fetchRequestId = 0;
2376
1638
  leftContainer;
2377
1639
  rightContainer;
2378
- profileFlex;
2379
- profileImgLeft;
2380
- profileImgRight;
1640
+ leftFrame;
1641
+ rightFrame;
1642
+ leftBgFrame;
1643
+ rightBgFrame;
2381
1644
  shapeContainer;
2382
- shapeBg;
2383
- shapeBg1;
2384
- shapeBg2;
2385
- shapeTextLeft;
2386
- shapeTextRight;
2387
1645
  shapeTextCenter;
2388
1646
  constructor(backendService, renderer, fileConversionService, imageCompressionService, cdr, getVerboseLogging) {
2389
1647
  this.backendService = backendService;
@@ -2411,9 +1669,9 @@ class ProfileComparisonLibComponent {
2411
1669
  });
2412
1670
  this.baseConfig = JSON.parse(JSON.stringify(this.config));
2413
1671
  this.updateConfigProperties();
2414
- const base = this.backendService.getBaseUrl();
2415
- this.backendConfigured = !!(base && base.trim());
2416
- this.log('ngOnInit', 'backendConfigured', this.backendConfigured, 'base', base ?? '(null)');
1672
+ this.resolvedBackendUrl = resolveBackendUrl(this.backendUrl, this.backendMode, this.backendService.getLegacyTokenUrl());
1673
+ this.backendConfigured = !!this.resolvedBackendUrl;
1674
+ this.log('ngOnInit', 'backendConfigured', this.backendConfigured, 'resolvedUrl', this.resolvedBackendUrl ?? '(null)');
2417
1675
  if (!this.backendConfigured) {
2418
1676
  this.log('ngOnInit', 'exiting early: backend not configured');
2419
1677
  this.user1Transform = 'translateY(0px)';
@@ -2441,9 +1699,21 @@ class ProfileComparisonLibComponent {
2441
1699
  this.waitForImagesAndInitDrag();
2442
1700
  }
2443
1701
  ngOnChanges(changes) {
1702
+ if (changes['backendMode'] || changes['backendUrl']) {
1703
+ this.resolvedBackendUrl = resolveBackendUrl(this.backendUrl, this.backendMode, this.backendService.getLegacyTokenUrl());
1704
+ this.backendConfigured = !!this.resolvedBackendUrl;
1705
+ this.log('ngOnChanges', 'backend inputs changed', 'resolvedUrl', this.resolvedBackendUrl ?? '(null)');
1706
+ }
1707
+ if (changes['fadeAllEdges']) {
1708
+ this.config.edgeShading = this.fadeAllEdges;
1709
+ }
2444
1710
  if (changes['config']) {
2445
1711
  const first = changes['config'].firstChange;
2446
1712
  this.log('ngOnChanges', 'config changed', 'firstChange', first, 'backendConfigured', this.backendConfigured);
1713
+ // Sync fadeAllEdges if it was explicitly changed via Input but not in config
1714
+ if (this.config.edgeShading === undefined) {
1715
+ this.config.edgeShading = this.fadeAllEdges;
1716
+ }
2447
1717
  this.updateConfigProperties();
2448
1718
  if (!this.backendConfigured) {
2449
1719
  this.log('ngOnChanges', 'skipping fetch: backend not configured');
@@ -2473,7 +1743,87 @@ class ProfileComparisonLibComponent {
2473
1743
  if (!interest || !this.centerItem?.length)
2474
1744
  return false;
2475
1745
  const n = String(interest).trim().toLowerCase();
2476
- return this.centerItem.some((c) => String(c).trim().toLowerCase() === n);
1746
+ // Placeholders are NEVER 'in center' - they stay on their respective sides
1747
+ if (this.isPlaceholder(n))
1748
+ return false;
1749
+ return this.centerItem.some((c) => {
1750
+ if (!c || this.isPlaceholder(c))
1751
+ return false;
1752
+ const nc = String(c).trim().toLowerCase();
1753
+ // Handle consolidated items (e.g. "Football × Soccer" or "Football / Soccer")
1754
+ return nc === n || nc.split(' × ').includes(n) || nc.split(' / ').includes(n);
1755
+ });
1756
+ }
1757
+ /**
1758
+ * Returns true if the value is a placeholder string consisting only of one or more dashes.
1759
+ * Handles variations like "-", "---", "----", etc.
1760
+ */
1761
+ /**
1762
+ * Returns true if we should show a dash on the side instead of the text.
1763
+ * This happens if:
1764
+ * 1. The interest is a placeholder (----).
1765
+ * 2. The interest exactly matches the center label at that index (to avoid redundancy).
1766
+ */
1767
+ shouldShowDash(interest, index) {
1768
+ if (!interest || this.isPlaceholder(interest))
1769
+ return true;
1770
+ if (!this.centerItem || !this.centerItem[index])
1771
+ return false;
1772
+ const n = String(interest).trim().toLowerCase();
1773
+ const c = String(this.centerItem[index]).trim().toLowerCase();
1774
+ if (this.isPlaceholder(c))
1775
+ return false;
1776
+ // Show dash if the side interest is exactly represented in the center
1777
+ return n === c || c.split(' × ').includes(n) || c.split(' / ').includes(n);
1778
+ }
1779
+ /**
1780
+ * Returns true if the center label at the current index is a repeat of the one above it.
1781
+ * This is used to "collapse" repeating categories into dashes.
1782
+ */
1783
+ isCenterRedundant(index) {
1784
+ if (index === 0 || !this.centerItem || !this.centerItem[index])
1785
+ return false;
1786
+ const current = String(this.centerItem[index]).trim().toLowerCase();
1787
+ const previous = String(this.centerItem[index - 1]).trim().toLowerCase();
1788
+ if (this.isPlaceholder(current))
1789
+ return false;
1790
+ return current === previous;
1791
+ }
1792
+ /**
1793
+ * Reorders the aligned results so that items with identical center labels follow each other.
1794
+ */
1795
+ regroupByCenterLabel() {
1796
+ if (!this.centerItem?.length)
1797
+ return;
1798
+ // We need to keep all three lists in sync while sorting
1799
+ const combined = this.centerItem.map((label, idx) => ({
1800
+ label,
1801
+ p1: this.alignedPerson1Interests[idx] || ProfileComparisonLibComponent.PLACEHOLDER,
1802
+ p2: this.alignedPerson2Interests[idx] || ProfileComparisonLibComponent.PLACEHOLDER
1803
+ }));
1804
+ // Sort by label, keeping placeholders (-) at the bottom
1805
+ combined.sort((a, b) => {
1806
+ const aPlace = this.isPlaceholder(a.label);
1807
+ const bPlace = this.isPlaceholder(b.label);
1808
+ if (aPlace && !bPlace)
1809
+ return 1;
1810
+ if (!aPlace && bPlace)
1811
+ return -1;
1812
+ return String(a.label).localeCompare(String(b.label));
1813
+ });
1814
+ // Write back to the arrays
1815
+ this.centerItem = combined.map(c => c.label);
1816
+ this.alignedPerson1Interests = combined.map(c => c.p1);
1817
+ this.alignedPerson2Interests = combined.map(c => c.p2);
1818
+ }
1819
+ isPlaceholder(val) {
1820
+ if (typeof val !== 'string')
1821
+ return false;
1822
+ const trimmed = val.trim();
1823
+ if (!trimmed)
1824
+ return false;
1825
+ // Standard dash characters include hyphen (-), en-dash (–), em-dash (—)
1826
+ return /^[-–—]+$/.test(trimmed);
2477
1827
  }
2478
1828
  fetchComparison() {
2479
1829
  this.log('fetchComparison', 'start');
@@ -2502,7 +1852,7 @@ class ProfileComparisonLibComponent {
2502
1852
  user1ImageLen: (configToSend.user1Image || '').length,
2503
1853
  user2ImageLen: (configToSend.user2Image || '').length,
2504
1854
  });
2505
- this.computeSub = this.backendService.getComparison(configToSend).subscribe({
1855
+ this.computeSub = this.backendService.getComparison(configToSend, this.resolvedBackendUrl).subscribe({
2506
1856
  next: (payload) => {
2507
1857
  if (requestId !== this.fetchRequestId) {
2508
1858
  this.log('fetchComparison', 'ignoring stale success', { requestId, current: this.fetchRequestId });
@@ -2513,22 +1863,13 @@ class ProfileComparisonLibComponent {
2513
1863
  this.backendError = null;
2514
1864
  const a1 = payload.alignedPerson1Interests ?? [];
2515
1865
  const a2 = payload.alignedPerson2Interests ?? [];
2516
- const sameLists = a1.length === a2.length && a1.every((v, i) => String(v).trim().toLowerCase() === String(a2[i]).trim().toLowerCase());
2517
- if (sameLists && a1.length > 0) {
2518
- this.log('fetchComparison', 'backend returned identical aligned lists; using raw lists to keep sides distinct', { len: a1.length });
2519
- this.alignedPerson1Interests = [];
2520
- this.alignedPerson2Interests = [];
2521
- this.displayPerson1Interests = this.person1Interests;
2522
- this.displayPerson2Interests = this.person2Interests;
2523
- this.centerItem = payload.centerItem ?? [];
2524
- }
2525
- else {
2526
- this.alignedPerson1Interests = a1;
2527
- this.alignedPerson2Interests = a2;
2528
- this.centerItem = payload.centerItem ?? [];
2529
- this.displayPerson1Interests = a1.length ? a1 : this.person1Interests;
2530
- this.displayPerson2Interests = a2.length ? a2 : this.person2Interests;
2531
- }
1866
+ this.alignedPerson1Interests = a1;
1867
+ this.alignedPerson2Interests = a2;
1868
+ this.centerItem = payload.centerItem ?? [];
1869
+ // Regroup items so redundant center labels flow together
1870
+ this.regroupByCenterLabel();
1871
+ this.displayPerson1Interests = this.alignedPerson1Interests;
1872
+ this.displayPerson2Interests = this.alignedPerson2Interests;
2532
1873
  this.matrixData = payload.matrixData ?? null;
2533
1874
  if (this.matrixData)
2534
1875
  this.matrixDataChange.emit(this.matrixData);
@@ -2594,6 +1935,8 @@ class ProfileComparisonLibComponent {
2594
1935
  person3Interests: list(safe.person3Interests, base.person3Interests),
2595
1936
  user1Image: str(safe.user1Image, base.user1Image),
2596
1937
  user2Image: str(safe.user2Image, base.user2Image),
1938
+ edgeShading: safe.edgeShading ?? base.edgeShading ?? true,
1939
+ shadingColor: safe.shadingColor ?? base.shadingColor ?? 'rgba(44, 40, 47, 0.7)',
2597
1940
  };
2598
1941
  }
2599
1942
  onUserImageLoad(which, event) {
@@ -2721,7 +2064,7 @@ class ProfileComparisonLibComponent {
2721
2064
  }
2722
2065
  getProfileContainerSize(which) {
2723
2066
  try {
2724
- const el = (which === 1 ? this.profileImgLeft : this.profileImgRight)?.nativeElement || null;
2067
+ const el = (which === 1 ? this.leftFrame : this.rightFrame)?.nativeElement || null;
2725
2068
  if (!el)
2726
2069
  return { width: 0, height: 0 };
2727
2070
  const rect = el.getBoundingClientRect();
@@ -2732,170 +2075,149 @@ class ProfileComparisonLibComponent {
2732
2075
  }
2733
2076
  }
2734
2077
  getOverlapSizeCssPx() {
2735
- try {
2736
- const root = this.profileFlex?.nativeElement || null;
2737
- const fallback = 40;
2738
- if (!root)
2739
- return fallback;
2740
- const val = getComputedStyle(root).getPropertyValue('--overlap-size').trim();
2741
- const parsed = parseFloat(val);
2742
- return isNaN(parsed) ? fallback : parsed;
2743
- }
2744
- catch {
2745
- return 40;
2746
- }
2078
+ return 40;
2747
2079
  }
2748
2080
  setOverlapSizeCssPx(px) {
2749
- try {
2750
- const root = this.profileFlex?.nativeElement || null;
2751
- if (!root)
2752
- return;
2753
- const clamped = Math.max(8, Math.min(80, Math.round(px)));
2754
- root.style.setProperty('--overlap-size', `${clamped}px`);
2755
- }
2756
- catch { }
2081
+ // deprecated
2757
2082
  }
2758
2083
  waitForImagesAndInitDrag() {
2759
- const shapeBg1 = this.shapeBg1?.nativeElement;
2760
- const shapeBg2 = this.shapeBg2?.nativeElement;
2761
- if (!shapeBg1 || !shapeBg2) {
2084
+ const leftFrame = this.leftFrame?.nativeElement;
2085
+ const rightFrame = this.rightFrame?.nativeElement;
2086
+ if (!leftFrame || !rightFrame) {
2762
2087
  setTimeout(() => this.waitForImagesAndInitDrag(), 100);
2763
2088
  return;
2764
2089
  }
2765
- const checkImagesLoaded = () => {
2766
- // For SVG elements, check if they're rendered instead of naturalWidth
2767
- const img1Loaded = shapeBg1 && shapeBg1.getBoundingClientRect().width > 0;
2768
- const img2Loaded = shapeBg2 && shapeBg2.getBoundingClientRect().width > 0;
2769
- if (img1Loaded && img2Loaded) {
2770
- setTimeout(() => this.initDrag(), 50);
2771
- }
2772
- else {
2773
- setTimeout(checkImagesLoaded, 100);
2774
- }
2775
- };
2776
- checkImagesLoaded();
2090
+ setTimeout(() => this.initDrag(), 100);
2777
2091
  }
2778
2092
  initDrag() {
2779
- const shapeBg1 = this.shapeBg1?.nativeElement;
2780
- const shapeBg2 = this.shapeBg2?.nativeElement;
2781
- const shapeTextLeft = this.shapeTextLeft?.nativeElement || null;
2782
- const shapeTextRight = this.shapeTextRight?.nativeElement || null;
2783
- if (!shapeBg1 || !shapeBg2 || !shapeTextLeft || !shapeTextRight)
2093
+ const leftFrame = this.leftFrame?.nativeElement;
2094
+ const rightFrame = this.rightFrame?.nativeElement;
2095
+ const leftBgFrame = this.leftBgFrame?.nativeElement;
2096
+ const rightBgFrame = this.rightBgFrame?.nativeElement;
2097
+ const shapeTextCenter = this.shapeTextCenter?.nativeElement;
2098
+ if (!leftFrame || !rightFrame)
2784
2099
  return;
2785
- // Cast to HTMLElement to access style properties conveniently, as SVGElement style handling can be similar in this context
2786
- const shapeBg1El = shapeBg1;
2787
- const shapeBg2El = shapeBg2;
2788
- shapeBg1El.style.height = ProfileComparisonLibComponent.SHAPE_BG_HEIGHT;
2789
- // shapeBg1El.style.objectFit = ProfileComparisonLibComponent.SHAPE_BG_OBJECT_FIT;
2790
- // shapeBg1El.style.objectPosition = ProfileComparisonLibComponent.SHAPE_BG1_OBJECT_POSITION;
2791
- shapeBg2El.style.height = ProfileComparisonLibComponent.SHAPE_BG_HEIGHT;
2792
- // shapeBg2El.style.objectFit = ProfileComparisonLibComponent.SHAPE_BG_OBJECT_FIT;
2793
- // shapeBg2El.style.objectPosition = ProfileComparisonLibComponent.SHAPE_BG2_OBJECT_POSITION;
2794
- const originalWidth1 = shapeBg1.getBoundingClientRect().width;
2795
- const originalWidth2 = shapeBg2.getBoundingClientRect().width;
2796
- const minWidth = ProfileComparisonLibComponent.DRAG_MIN_WIDTH;
2797
- const maxWidth = ProfileComparisonLibComponent.DRAG_MAX_WIDTH;
2798
2100
  let isDragging = false;
2799
2101
  let dragStartX = 0;
2800
- let initialWidth1 = 0;
2801
- let initialWidth2 = 0;
2802
- let activeImg = null;
2803
- // Use ViewChild for center text
2804
- const shapeTextCenter = this.shapeTextCenter?.nativeElement || null;
2102
+ let activeFrame = null;
2103
+ const initialTranslateXLeft = -24; // starting CSS value percent
2104
+ const initialTranslateXRight = 24;
2805
2105
  const onMouseMove = (e) => {
2806
- if (!isDragging || !activeImg)
2106
+ if (!isDragging || !activeFrame)
2807
2107
  return;
2808
2108
  const deltaX = e.clientX - dragStartX;
2809
- const containerWidth = this.shapeContainer?.nativeElement?.clientWidth || 0;
2810
- let newWidth1 = initialWidth1;
2811
- let newWidth2 = initialWidth2;
2812
- if (activeImg === shapeBg1) {
2813
- newWidth1 = initialWidth1 + deltaX;
2814
- newWidth2 = initialWidth2 - deltaX;
2109
+ const containerWidth = leftFrame.parentElement?.clientWidth || 360;
2110
+ const deltaPercent = (deltaX / containerWidth) * 100;
2111
+ let newLeftTranslate = initialTranslateXLeft;
2112
+ let newRightTranslate = initialTranslateXRight;
2113
+ if (activeFrame === leftFrame) {
2114
+ // Dragging left frame rightwards (deltaX > 0) increases its translateX
2115
+ newLeftTranslate = Math.max(-50, Math.min(0, initialTranslateXLeft + deltaPercent));
2116
+ // Counter-balance right frame moving outward
2117
+ newRightTranslate = Math.max(0, Math.min(50, initialTranslateXRight - deltaPercent));
2815
2118
  }
2816
- else if (activeImg === shapeBg2) {
2817
- newWidth1 = initialWidth1 - deltaX;
2818
- newWidth2 = initialWidth2 + deltaX;
2119
+ else {
2120
+ // Dragging right frame leftwards (deltaX < 0) decreases its translateX
2121
+ newRightTranslate = Math.max(0, Math.min(50, initialTranslateXRight + deltaPercent));
2122
+ // Counter-balance left frame moving outward
2123
+ newLeftTranslate = Math.max(-50, Math.min(0, initialTranslateXLeft - deltaPercent));
2819
2124
  }
2820
- newWidth1 = Math.max(minWidth, Math.min(maxWidth, newWidth1));
2821
- newWidth2 = Math.max(minWidth, Math.min(maxWidth, newWidth2));
2822
- shapeBg1El.style.width = newWidth1 + 'px';
2823
- shapeBg1El.style.height = ProfileComparisonLibComponent.SHAPE_BG_HEIGHT;
2824
- shapeBg2El.style.width = newWidth2 + 'px';
2825
- shapeBg2El.style.height = ProfileComparisonLibComponent.SHAPE_BG_HEIGHT;
2826
- const widthChange1 = newWidth1 - originalWidth1;
2827
- const widthChange2 = newWidth2 - originalWidth2;
2828
- const dragDirection = deltaX > 0 ? 1 : -1;
2829
- const dragDistance = Math.abs(deltaX);
2830
- const maxDragDistance = ProfileComparisonLibComponent.MAX_DRAG_DISTANCE;
2831
- const centerMoveDistance = Math.min(dragDistance, maxDragDistance) * dragDirection * ProfileComparisonLibComponent.CENTER_MOVE_MULTIPLIER;
2832
- shapeTextLeft.style.transform = `translateX(${widthChange1 * ProfileComparisonLibComponent.TEXT_MOVE_MULTIPLIER}px)`;
2833
- shapeTextRight.style.transform = `translateX(${-widthChange2 * ProfileComparisonLibComponent.TEXT_MOVE_MULTIPLIER}px)`;
2125
+ leftFrame.style.transform = `translateX(${newLeftTranslate}%) scale(0.86)`;
2126
+ rightFrame.style.transform = `translateX(${newRightTranslate}%) scale(0.86)`;
2127
+ if (leftBgFrame)
2128
+ leftBgFrame.style.transform = `translateX(${newLeftTranslate}%) scale(0.86)`;
2129
+ if (rightBgFrame)
2130
+ rightBgFrame.style.transform = `translateX(${newRightTranslate}%) scale(0.86)`;
2834
2131
  if (shapeTextCenter) {
2835
- shapeTextCenter.style.transform = `translateX(${centerMoveDistance * ProfileComparisonLibComponent.TEXT_MOVE_MULTIPLIER}px)`;
2132
+ // Just fade out center text slightly when pulling apart
2133
+ const pullDistanceFactor = Math.abs(newLeftTranslate - initialTranslateXLeft) / 24;
2134
+ shapeTextCenter.style.opacity = `${Math.max(0, 1 - pullDistanceFactor * 1.5)}`;
2836
2135
  }
2837
2136
  };
2838
- const resetWidths = () => {
2137
+ const resetDrag = () => {
2839
2138
  isDragging = false;
2840
- activeImg = null;
2841
- shapeBg1El.style.transition = ProfileComparisonLibComponent.TRANSITION_WIDTH;
2842
- shapeBg2El.style.transition = ProfileComparisonLibComponent.TRANSITION_WIDTH;
2843
- shapeTextLeft.style.transition = ProfileComparisonLibComponent.TRANSITION_TRANSFORM;
2844
- shapeTextRight.style.transition = ProfileComparisonLibComponent.TRANSITION_TRANSFORM;
2845
- if (shapeTextCenter) {
2846
- shapeTextCenter.style.transition = ProfileComparisonLibComponent.TRANSITION_TRANSFORM;
2847
- }
2848
- shapeBg1El.style.width = originalWidth1 + 'px';
2849
- shapeBg2El.style.width = originalWidth2 + 'px';
2850
- shapeTextLeft.style.transform = ProfileComparisonLibComponent.TRANSFORM_RESET;
2851
- shapeTextRight.style.transform = ProfileComparisonLibComponent.TRANSFORM_RESET;
2852
- if (shapeTextCenter) {
2853
- shapeTextCenter.style.transform = ProfileComparisonLibComponent.TRANSFORM_RESET;
2854
- }
2139
+ activeFrame = null;
2140
+ leftFrame.style.transition = 'transform 0.3s ease-out';
2141
+ rightFrame.style.transition = 'transform 0.3s ease-out';
2142
+ if (leftBgFrame)
2143
+ leftBgFrame.style.transition = 'transform 0.3s ease-out';
2144
+ if (rightBgFrame)
2145
+ rightBgFrame.style.transition = 'transform 0.3s ease-out';
2146
+ if (shapeTextCenter)
2147
+ shapeTextCenter.style.transition = 'opacity 0.3s ease-out';
2148
+ leftFrame.style.transform = `translateX(-24%) scale(0.86)`;
2149
+ rightFrame.style.transform = `translateX(24%) scale(0.86)`;
2150
+ if (leftBgFrame)
2151
+ leftBgFrame.style.transform = `translateX(-24%) scale(0.86)`;
2152
+ if (rightBgFrame)
2153
+ rightBgFrame.style.transform = `translateX(24%) scale(0.86)`;
2154
+ if (shapeTextCenter)
2155
+ shapeTextCenter.style.opacity = '1';
2855
2156
  document.removeEventListener('mousemove', onMouseMove);
2856
- document.removeEventListener('mouseup', resetWidths);
2857
- document.removeEventListener('mouseleave', resetWidths);
2157
+ document.removeEventListener('mouseup', resetDrag);
2158
+ document.removeEventListener('mouseleave', resetDrag);
2858
2159
  setTimeout(() => {
2859
- shapeBg1El.style.transition = '';
2860
- shapeBg2El.style.transition = '';
2861
- shapeTextLeft.style.transition = '';
2862
- shapeTextRight.style.transition = '';
2863
- if (shapeTextCenter) {
2160
+ leftFrame.style.transition = '';
2161
+ rightFrame.style.transition = '';
2162
+ if (leftBgFrame)
2163
+ leftBgFrame.style.transition = '';
2164
+ if (rightBgFrame)
2165
+ rightBgFrame.style.transition = '';
2166
+ if (shapeTextCenter)
2864
2167
  shapeTextCenter.style.transition = '';
2865
- }
2866
- }, ProfileComparisonLibComponent.TRANSITION_RESET_DELAY_MS);
2168
+ }, 300);
2867
2169
  };
2868
- [shapeBg1, shapeBg2].forEach((img) => {
2869
- img.addEventListener('mousedown', (e) => {
2170
+ [leftFrame, rightFrame].forEach((frame) => {
2171
+ // Ensure cursor visually indicates grab on the svg images inside the frame
2172
+ const svgs = frame.querySelectorAll('svg');
2173
+ svgs.forEach(s => s.style.cursor = 'grab');
2174
+ frame.addEventListener('mousedown', (e) => {
2175
+ // Prevent default text selection during drag
2870
2176
  e.preventDefault();
2871
2177
  isDragging = true;
2872
- activeImg = img;
2178
+ activeFrame = frame;
2873
2179
  dragStartX = e.clientX;
2874
- initialWidth1 = shapeBg1.getBoundingClientRect().width;
2875
- initialWidth2 = shapeBg2.getBoundingClientRect().width;
2876
2180
  document.addEventListener('mousemove', onMouseMove);
2877
- document.addEventListener('mouseup', resetWidths);
2878
- document.addEventListener('mouseleave', resetWidths);
2181
+ document.addEventListener('mouseup', resetDrag);
2182
+ document.addEventListener('mouseleave', resetDrag);
2879
2183
  });
2880
2184
  });
2881
2185
  }
2882
2186
  onViewProfile(side) {
2187
+ if (side === 'left') {
2188
+ this.leftProfileClicked = true;
2189
+ setTimeout(() => this.leftProfileClicked = false, 650);
2190
+ }
2191
+ else {
2192
+ this.rightProfileClicked = true;
2193
+ setTimeout(() => this.rightProfileClicked = false, 650);
2194
+ }
2883
2195
  this.viewProfileClick.emit({ side });
2884
2196
  }
2197
+ onShapeClick(side) {
2198
+ if (side === 'left') {
2199
+ this.leftShapeAnimating = true;
2200
+ setTimeout(() => this.leftShapeAnimating = false, 550);
2201
+ }
2202
+ else {
2203
+ this.rightShapeAnimating = true;
2204
+ setTimeout(() => this.rightShapeAnimating = false, 550);
2205
+ }
2206
+ }
2885
2207
  compressConfigImagesIfNeeded() {
2886
2208
  const tasks = [];
2887
2209
  if (this.user1Image) {
2888
2210
  tasks.push(this.compressImageStringToDataUrl(this.user1Image, 'ConfigUser1').pipe(tap$1((dataUrl) => { if (dataUrl)
2889
- this.user1Image = dataUrl; }), map$1(() => void 0)));
2211
+ this.user1Image = dataUrl; }), map(() => void 0)));
2890
2212
  }
2891
2213
  if (this.user2Image) {
2892
2214
  tasks.push(this.compressImageStringToDataUrl(this.user2Image, 'ConfigUser2').pipe(tap$1((dataUrl) => { if (dataUrl)
2893
- this.user2Image = dataUrl; }), map$1(() => void 0)));
2215
+ this.user2Image = dataUrl; }), map(() => void 0)));
2894
2216
  }
2895
- return tasks.length > 0 ? forkJoin(tasks).pipe(map$1(() => void 0)) : of(void 0);
2217
+ return tasks.length > 0 ? forkJoin(tasks).pipe(map(() => void 0)) : of(void 0);
2896
2218
  }
2897
2219
  compressImageStringToDataUrl(imageStr, baseName) {
2898
- return this.fileConversionService.getFileForImageString(imageStr, `${baseName}.jpg`).pipe(switchMap$1(file => this.imageCompressionService.compressImageFile(file, this.compressionConfig)), map$1(res => res.dataUrl), catchError$1(() => of(null)));
2220
+ return this.fileConversionService.getFileForImageString(imageStr, `${baseName}.jpg`).pipe(switchMap(file => this.imageCompressionService.compressImageFile(file, this.compressionConfig)), map(res => res.dataUrl), catchError$1(() => of(null)));
2899
2221
  }
2900
2222
  getEyeCoordinatesFromBBox(bbox) {
2901
2223
  const eyeY = bbox.y + bbox.height * 0.38;
@@ -2979,8 +2301,8 @@ class ProfileComparisonLibComponent {
2979
2301
  const user2FaceCoords = this.getFaceCoordinatesFromBBox(face2);
2980
2302
  const user1Eyes = this.getEyeCoordinatesFromFace(face1);
2981
2303
  const user2Eyes = this.getEyeCoordinatesFromFace(face2);
2982
- const leftContainerEl = this.profileImgLeft?.nativeElement || null;
2983
- const rightContainerEl = this.profileImgRight?.nativeElement || null;
2304
+ const leftContainerEl = this.leftFrame?.nativeElement || null;
2305
+ const rightContainerEl = this.rightFrame?.nativeElement || null;
2984
2306
  const containerWidth1 = leftContainerEl?.clientWidth || 160;
2985
2307
  const containerHeight1 = leftContainerEl?.clientHeight || 550;
2986
2308
  const containerWidth2 = rightContainerEl?.clientWidth || 160;
@@ -3017,12 +2339,12 @@ class ProfileComparisonLibComponent {
3017
2339
  isValidFaceData(face) {
3018
2340
  return !!face && typeof face.x === 'number' && face.width > 0 && face.height > 0;
3019
2341
  }
3020
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: ProfileComparisonLibComponent, deps: [{ token: ProfileComparisonBackendService }, { token: i0.Renderer2 }, { token: FileConversionService }, { token: ImageCompressionService }, { token: i0.ChangeDetectorRef }, { token: PROFILE_COMPARISON_VERBOSE_LOGGING, optional: true }], target: i0.ɵɵFactoryTarget.Component });
3021
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.16", type: ProfileComparisonLibComponent, isStandalone: false, selector: "lib-profile-comparison", inputs: { config: "config", fadeAllEdges: "fadeAllEdges" }, 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 class=\"backend-error-message\" *ngIf=\"backendError\">{{ backendError }}</div>\n <div #profileFlex class=\"profile-flex\" [class.fade-all]=\"fadeAllEdges\">\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.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.object-position]=\"user2ObjectPosition\"\n (load)=\"onUserImageLoad(2, $event)\"\n />\n <!-- [style.object-fit]=\"'cover'\" -->\n </div>\n </div>\n\n <div #shapeContainer class=\"shape\">\n <div #shapeBg class=\"shape-bg\" [class.fade-all]=\"fadeAllEdges\">\n <svg width=\"250\" height=\"350\" viewBox=\"0 0 250 350\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" preserveAspectRatio=\"xMaxYMid slice\" style=\"height: 350px;\" class=\"shape-bg1\" #shapeBg1>\n<g filter=\"url(#filter0_i_4_4137)\">\n<path d=\"M215.177 44.4924L-18.823 19.3104C-25.3264 18.6106 -31 23.7064 -31 30.2473V283.169C-31 289.914 -24.9854 295.07 -18.3196 294.04L215.68 257.869C221.043 257.04 225 252.407 225 246.981V55.4497C225 49.8302 220.764 45.0937 215.177 44.4924Z\" fill=\"#00CFE6\" fill-opacity=\"0.07\"/>\n</g>\n<path d=\"M215.177 44.4924L-18.823 19.3104C-25.3264 18.6106 -31 23.7064 -31 30.2473V283.169C-31 289.914 -24.9854 295.07 -18.3196 294.04L215.68 257.869C221.043 257.04 225 252.407 225 246.981V55.4497C225 49.8302 220.764 45.0937 215.177 44.4924Z\" fill=\"url(#paint0_radial_4_4137)\" fill-opacity=\"0.07\" stroke=\"url(#paint1_linear_4_4137)\" stroke-width=\"2\" stroke-miterlimit=\"3.99393\" stroke-linejoin=\"round\"/>\n<g filter=\"url(#filter1_ddddd_4_4137)\">\n<path d=\"M219.081 43.0755L-18.9193 17.2045C-24.8351 16.5614 -30 21.1953 -30 27.1459V287.318C-30 293.455 -24.5217 298.145 -18.4573 297.198L219.543 260.038C224.411 259.278 228 255.075 228 250.148V53.0303C228 47.9257 224.155 43.6271 219.081 43.0755Z\" stroke=\"#61C2AB\" stroke-opacity=\"0.3\" stroke-linejoin=\"round\" shape-rendering=\"crispEdges\"/>\n</g>\n<defs>\n<filter id=\"filter0_i_4_4137\" x=\"-31\" y=\"19.2461\" width=\"256\" height=\"274.925\" filterUnits=\"userSpaceOnUse\" color-interpolation-filters=\"sRGB\">\n<feFlood flood-opacity=\"0\" result=\"BackgroundImageFix\"/>\n<feBlend mode=\"normal\" in=\"SourceGraphic\" in2=\"BackgroundImageFix\" result=\"shape\"/>\n<feColorMatrix in=\"SourceAlpha\" type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0\" result=\"hardAlpha\"/>\n<feOffset/>\n<feGaussianBlur stdDeviation=\"7.95\"/>\n<feComposite in2=\"hardAlpha\" operator=\"arithmetic\" k2=\"-1\" k3=\"1\"/>\n<feColorMatrix type=\"matrix\" values=\"0 0 0 0 0.516667 0 0 0 0 1 0 0 0 1 0 0 0 1 0\"/>\n<feBlend mode=\"normal\" in2=\"shape\" result=\"effect1_innerShadow_4_4137\"/>\n</filter>\n<filter id=\"filter1_ddddd_4_4137\" x=\"-46.2\" y=\"0.944715\" width=\"290.4\" height=\"312.575\" filterUnits=\"userSpaceOnUse\" color-interpolation-filters=\"sRGB\">\n<feFlood flood-opacity=\"0\" result=\"BackgroundImageFix\"/>\n<feColorMatrix in=\"SourceAlpha\" type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0\" result=\"hardAlpha\"/>\n<feOffset/>\n<feGaussianBlur stdDeviation=\"7.95\"/>\n<feComposite in2=\"hardAlpha\" operator=\"out\"/>\n<feColorMatrix type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0\"/>\n<feBlend mode=\"normal\" in2=\"BackgroundImageFix\" result=\"effect1_dropShadow_4_4137\"/>\n<feColorMatrix in=\"SourceAlpha\" type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0\" result=\"hardAlpha\"/>\n<feOffset/>\n<feGaussianBlur stdDeviation=\"7.85\"/>\n<feComposite in2=\"hardAlpha\" operator=\"out\"/>\n<feColorMatrix type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0\"/>\n<feBlend mode=\"normal\" in2=\"effect1_dropShadow_4_4137\" result=\"effect2_dropShadow_4_4137\"/>\n<feColorMatrix in=\"SourceAlpha\" type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0\" result=\"hardAlpha\"/>\n<feOffset/>\n<feGaussianBlur stdDeviation=\"7.85\"/>\n<feComposite in2=\"hardAlpha\" operator=\"out\"/>\n<feColorMatrix type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0\"/>\n<feBlend mode=\"normal\" in2=\"effect2_dropShadow_4_4137\" result=\"effect3_dropShadow_4_4137\"/>\n<feColorMatrix in=\"SourceAlpha\" type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0\" result=\"hardAlpha\"/>\n<feOffset/>\n<feGaussianBlur stdDeviation=\"7.85\"/>\n<feComposite in2=\"hardAlpha\" operator=\"out\"/>\n<feColorMatrix type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0\"/>\n<feBlend mode=\"normal\" in2=\"effect3_dropShadow_4_4137\" result=\"effect4_dropShadow_4_4137\"/>\n<feColorMatrix in=\"SourceAlpha\" type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0\" result=\"hardAlpha\"/>\n<feOffset/>\n<feGaussianBlur stdDeviation=\"7.85\"/>\n<feComposite in2=\"hardAlpha\" operator=\"out\"/>\n<feColorMatrix type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0\"/>\n<feBlend mode=\"normal\" in2=\"effect4_dropShadow_4_4137\" result=\"effect5_dropShadow_4_4137\"/>\n<feBlend mode=\"normal\" in=\"SourceGraphic\" in2=\"effect5_dropShadow_4_4137\" result=\"shape\"/>\n</filter>\n<radialGradient id=\"paint0_radial_4_4137\" cx=\"0\" cy=\"0\" r=\"1\" gradientTransform=\"matrix(65.1034 -81.3964 74.955 49.415 97 157)\" gradientUnits=\"userSpaceOnUse\">\n<stop stop-color=\"#0051E6\" stop-opacity=\"0\"/>\n<stop offset=\"0.615385\" stop-color=\"#0051E6\" stop-opacity=\"0\"/>\n<stop offset=\"1\" stop-color=\"white\"/>\n</radialGradient>\n<linearGradient id=\"paint1_linear_4_4137\" x1=\"97\" y1=\"18\" x2=\"97\" y2=\"296\" gradientUnits=\"userSpaceOnUse\">\n<stop offset=\"0.380829\" stop-color=\"#52FFEB\"/>\n<stop offset=\"1\" stop-color=\"#319990\"/>\n</linearGradient>\n</defs>\n</svg>\n <svg width=\"250\" height=\"350\" viewBox=\"0 0 250 350\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" preserveAspectRatio=\"xMinYMid slice\" style=\"height: 350px;\" class=\"shape-bg2\" #shapeBg2>\n<g filter=\"url(#filter0_i_4_4141)\">\n<path d=\"M29.5864 44.9454L263.826 19.8065C270.329 19.1087 276 24.2041 276 30.7437V283.174C276 289.917 269.988 295.073 263.324 294.046L29.0843 257.937C23.7196 257.11 19.7602 252.476 19.7602 247.048V55.9032C19.7602 50.2824 23.9977 45.5452 29.5864 44.9454Z\" fill=\"#7B00E6\" fill-opacity=\"0.07\"/>\n</g>\n<path d=\"M29.5864 44.9454L263.826 19.8065C270.329 19.1087 276 24.2041 276 30.7437V283.174C276 289.917 269.988 295.073 263.324 294.046L29.0843 257.937C23.7196 257.11 19.7602 252.476 19.7602 247.048V55.9032C19.7602 50.2824 23.9977 45.5452 29.5864 44.9454Z\" fill=\"url(#paint0_radial_4_4141)\" fill-opacity=\"0.07\" stroke=\"url(#paint1_linear_4_4141)\" stroke-width=\"2\" stroke-miterlimit=\"3.99393\" stroke-linejoin=\"round\"/>\n<g filter=\"url(#filter1_ddddd_4_4141)\">\n<path d=\"M25.9212 43.077L264.37 17.2022C270.285 16.5603 275.449 21.194 275.449 27.1438V287.321C275.449 293.457 269.972 298.146 263.909 297.201L25.46 260.036C20.5905 259.277 17 255.074 17 250.146V53.032C17 47.9267 20.8457 43.6277 25.9212 43.077Z\" stroke=\"#C37DFF\" stroke-opacity=\"0.3\" stroke-linejoin=\"round\" shape-rendering=\"crispEdges\"/>\n</g>\n<defs>\n<filter id=\"filter0_i_4_4141\" x=\"19.7602\" y=\"19.7425\" width=\"256.24\" height=\"274.434\" filterUnits=\"userSpaceOnUse\" color-interpolation-filters=\"sRGB\">\n<feFlood flood-opacity=\"0\" result=\"BackgroundImageFix\"/>\n<feBlend mode=\"normal\" in=\"SourceGraphic\" in2=\"BackgroundImageFix\" result=\"shape\"/>\n<feColorMatrix in=\"SourceAlpha\" type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0\" result=\"hardAlpha\"/>\n<feOffset/>\n<feGaussianBlur stdDeviation=\"7.95\"/>\n<feComposite in2=\"hardAlpha\" operator=\"arithmetic\" k2=\"-1\" k3=\"1\"/>\n<feColorMatrix type=\"matrix\" values=\"0 0 0 0 0.482353 0 0 0 0 0 0 0 0 0 0.901961 0 0 0 1 0\"/>\n<feBlend mode=\"normal\" in2=\"shape\" result=\"effect1_innerShadow_4_4141\"/>\n</filter>\n<filter id=\"filter1_ddddd_4_4141\" x=\"0.8\" y=\"0.942639\" width=\"290.849\" height=\"312.58\" filterUnits=\"userSpaceOnUse\" color-interpolation-filters=\"sRGB\">\n<feFlood flood-opacity=\"0\" result=\"BackgroundImageFix\"/>\n<feColorMatrix in=\"SourceAlpha\" type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0\" result=\"hardAlpha\"/>\n<feOffset/>\n<feGaussianBlur stdDeviation=\"7.85\"/>\n<feComposite in2=\"hardAlpha\" operator=\"out\"/>\n<feColorMatrix type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0\"/>\n<feBlend mode=\"normal\" in2=\"BackgroundImageFix\" result=\"effect1_dropShadow_4_4141\"/>\n<feColorMatrix in=\"SourceAlpha\" type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0\" result=\"hardAlpha\"/>\n<feOffset/>\n<feGaussianBlur stdDeviation=\"7.85\"/>\n<feComposite in2=\"hardAlpha\" operator=\"out\"/>\n<feColorMatrix type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0\"/>\n<feBlend mode=\"normal\" in2=\"effect1_dropShadow_4_4141\" result=\"effect2_dropShadow_4_4141\"/>\n<feColorMatrix in=\"SourceAlpha\" type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0\" result=\"hardAlpha\"/>\n<feOffset/>\n<feGaussianBlur stdDeviation=\"7.85\"/>\n<feComposite in2=\"hardAlpha\" operator=\"out\"/>\n<feColorMatrix type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0\"/>\n<feBlend mode=\"normal\" in2=\"effect2_dropShadow_4_4141\" result=\"effect3_dropShadow_4_4141\"/>\n<feColorMatrix in=\"SourceAlpha\" type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0\" result=\"hardAlpha\"/>\n<feOffset/>\n<feGaussianBlur stdDeviation=\"7.85\"/>\n<feComposite in2=\"hardAlpha\" operator=\"out\"/>\n<feColorMatrix type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0\"/>\n<feBlend mode=\"normal\" in2=\"effect3_dropShadow_4_4141\" result=\"effect4_dropShadow_4_4141\"/>\n<feColorMatrix in=\"SourceAlpha\" type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0\" result=\"hardAlpha\"/>\n<feOffset/>\n<feGaussianBlur stdDeviation=\"7.85\"/>\n<feComposite in2=\"hardAlpha\" operator=\"out\"/>\n<feColorMatrix type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0\"/>\n<feBlend mode=\"normal\" in2=\"effect4_dropShadow_4_4141\" result=\"effect5_dropShadow_4_4137\"/>\n<feBlend mode=\"normal\" in=\"SourceGraphic\" in2=\"effect5_dropShadow_4_4137\" result=\"shape\"/>\n</filter>\n<radialGradient id=\"paint0_radial_4_4137\" cx=\"0\" cy=\"0\" r=\"1\" gradientTransform=\"matrix(65.1034 -81.3964 74.955 49.415 97 157)\" gradientUnits=\"userSpaceOnUse\">\n<stop stop-color=\"#0051E6\" stop-opacity=\"0\"/>\n<stop offset=\"0.615385\" stop-color=\"#0051E6\" stop-opacity=\"0\"/>\n<stop offset=\"1\" stop-color=\"white\"/>\n</radialGradient>\n<linearGradient id=\"paint1_linear_4_4137\" x1=\"97\" y1=\"18\" x2=\"97\" y2=\"296\" gradientUnits=\"userSpaceOnUse\">\n<stop offset=\"0.380829\" stop-color=\"#52FFEB\"/>\n<stop offset=\"1\" stop-color=\"#319990\"/>\n</linearGradient>\n</defs>\n</svg>\n </div>\n\n <div class=\"shape-text\">\n <div #shapeTextLeft class=\"shape-text-left\">\n <div class=\"scroll-container\" #leftContainer>\n <ng-container\n *ngFor=\"\n let interest of alignedPerson1Interests.length > 0\n ? alignedPerson1Interests\n : displayPerson1Interests.length > 0\n ? displayPerson1Interests\n : person1Interests;\n let i = index\n \"\n >\n <p\n class=\"shape-p\"\n *ngIf=\"!isInCenter(interest)\"\n [ngClass]=\"{'dash-item': interest === '-', 'spacer-item': interest === '----', 'aligned-item': interest !== '-' && interest !== '----'}\"\n >\n {{ interest === \"-\" ? \"\u2014\" : interest }}\n </p>\n </ng-container>\n </div>\n <h2 class=\"shape-h2\" (click)=\"onViewProfile('left')\">View Profile</h2>\n </div>\n\n <div #shapeTextCenter class=\"shape-text-center\">\n <ng-container *ngFor=\"let item of centerItem\">\n <p class=\"shape-p-center\">{{ item }}</p>\n </ng-container>\n </div>\n <div #shapeTextRight class=\"shape-text-right\">\n <div class=\"scroll-container\" #rightContainer>\n <ng-container\n *ngFor=\"\n let interest of alignedPerson2Interests.length > 0\n ? alignedPerson2Interests\n : displayPerson2Interests.length > 0\n ? displayPerson2Interests\n : person2Interests;\n let i = index\n \"\n >\n <p\n class=\"shape-p-right\"\n *ngIf=\"!isInCenter(interest)\"\n [ngClass]=\"{'dash-item': interest === '-', 'spacer-item': interest === '----', 'aligned-item': interest !== '-' && interest !== '----'}\"\n >\n {{ interest === \"-\" ? \"\u2014\" : interest }}\n </p>\n </ng-container>\n </div>\n <h2 class=\"shape-h2-right\" (click)=\"onViewProfile('right')\">View Profile</h2>\n </div>\n </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: [".profile-img.left{-webkit-mask-image:linear-gradient(to right,rgb(0,0,0) 0%,rgb(0,0,0) calc(100% - var(--overlap-size, 40px)),rgba(0,0,0,0) 100%);-webkit-mask-size:100% 100%;-webkit-mask-repeat:no-repeat;mask-image:linear-gradient(to right,#fff 0% calc(100% - var(--overlap-size, 40px)),#fff0);mask-size:100% 100%;mask-repeat:no-repeat;margin-right:calc(-.5 * var(--overlap-size, 40px))}.profile-img.right{-webkit-mask-image:linear-gradient(to left,rgb(0,0,0) 0%,rgb(0,0,0) calc(100% - var(--overlap-size, 40px)),rgba(0,0,0,0) 100%);-webkit-mask-size:100% 100%;-webkit-mask-repeat:no-repeat;mask-image:linear-gradient(to left,#fff 0% calc(100% - var(--overlap-size, 40px)),#fff0);mask-size:100% 100%;mask-repeat:no-repeat;margin-left:calc(-.5 * var(--overlap-size, 40px))}.profile-img.fade-all{-webkit-mask-image:linear-gradient(to bottom,transparent 75px,black 125px,black 425px,transparent 475px),linear-gradient(to right,transparent,black 15%,black 85%,transparent);-webkit-mask-composite:source-in;mask-image:linear-gradient(to bottom,transparent 75px,black 125px,black 425px,transparent 475px),linear-gradient(to right,transparent,black 15%,black 85%,transparent);mask-composite:intersect}.profile-img.fade-all.left{-webkit-mask-image:linear-gradient(to bottom,transparent 75px,black 125px,black 425px,transparent 475px),linear-gradient(to right,transparent 0%,black 15%,black calc(100% - var(--overlap-size, 40px)),transparent 100%);mask-image:linear-gradient(to bottom,transparent 75px,black 125px,black 425px,transparent 475px),linear-gradient(to right,transparent 0%,black 15%,black calc(100% - var(--overlap-size, 40px)),transparent 100%)}.profile-img.fade-all.right{-webkit-mask-image:linear-gradient(to bottom,transparent 75px,black 125px,black 425px,transparent 475px),linear-gradient(to left,transparent 0%,black 15%,black calc(100% - var(--overlap-size, 40px)),transparent 100%);mask-image:linear-gradient(to bottom,transparent 75px,black 125px,black 425px,transparent 475px),linear-gradient(to left,transparent 0%,black 15%,black calc(100% - var(--overlap-size, 40px)),transparent 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:320px;max-width:100%;margin:0 auto;overflow:hidden;position:relative}.profile-flex{display:flex;width:100%;height:400px;overflow:hidden;background:linear-gradient(135deg,#3a3a3a,#2a2a2a);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 15%,black 85%,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 15%,black 85%,transparent 100%),linear-gradient(to bottom,transparent 0%,black 15%,black 85%,transparent 100%);mask-composite:intersect}.shape-bg1,.shape-bg2{position:absolute;max-width:250px;opacity:.5;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{width:180px;height:550px;position:relative;flex-shrink:0;background:linear-gradient(135deg,#3a3a3a,#2a2a2a);margin:0;padding:0;left:0;top:-75px;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:.6;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}.shape{position:absolute;top:40px;width:100%}.shape-bg{position:relative}.shape-text{position:absolute;top:0;left:0;right:0;width:100%;height:100%;z-index:1;padding:0 1.25rem;box-sizing:border-box;pointer-events:none}.shape-text-left{margin-top:40px;text-align:left;position:absolute;top:0;bottom:0;right:calc(50% + 75px);z-index:2;pointer-events:auto;display:flex;flex-direction:column;align-items:flex-end}.shape-text-left .scroll-container{width:100%}.shape-text-center{position:absolute;left:0;right:0;margin:0 auto;width:fit-content;font-family:Gilroy,sans-serif;font-weight:700;font-size:.75rem;line-height:100%;letter-spacing:0%;color:#fff;text-align:center;display:flex;justify-content:center;align-items:center;flex-direction:column;text-shadow:2px 2px 4px rgba(0,0,0,.3);z-index:10;transition:transform .3s ease;pointer-events:none;top:50%;transform:translateY(-50%);padding:0 .625rem;margin-top:3.8125rem}.shape-text-right{margin-top:40px;text-align:right;position:absolute;top:0;bottom:0;left:calc(50% + 70px);z-index:2;pointer-events:auto;display:flex;flex-direction:column;align-items:flex-start}.shape-text-right .scroll-container{width:100%}.shape-p{font-family:Gilroy,sans-serif;font-weight:400;font-size:.75rem;line-height:100%;letter-spacing:0%;color:#fff;padding-bottom:.125rem;text-align:right;text-shadow:2px 2px 4px rgba(0,0,0,.3);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;direction:rtl;-webkit-user-select:none;user-select:none;margin:0}.shape-p-center{font-family:Gilroy,sans-serif;font-weight:700;font-size:.75rem;line-height:100%;letter-spacing:0%;color:#fff;margin:0;text-align:center;display:flex;justify-content:center;align-items:center;text-shadow:2px 2px 4px rgba(0,0,0,.3)}.shape-p-right{font-family:Gilroy,sans-serif;font-weight:400;font-size:.75rem;line-height:100%;letter-spacing:0%;color:#fff;padding-bottom:.125rem;text-align:left;text-shadow:2px 2px 4px rgba(0,0,0,.3);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin:0;-webkit-user-select:none;user-select:none}.dash-item{color:#ffffff4d!important;font-size:.625rem!important;height:.625rem}.spacer-item{height:auto!important;color:#fffc!important;font-weight:700;font-size:.75rem!important;letter-spacing:2px}.shape-h2{font-family:Calistoga,serif;font-weight:400;font-size:.625rem;letter-spacing:0%;color:#ffffff80;text-shadow:2px 2px 4px rgba(0,0,0,.3);cursor:pointer;transition:all .3s ease;border:none;outline:none;white-space:nowrap;pointer-events:auto;z-index:100;position:relative;margin-left:.5rem}.shape-h2:hover{opacity:.8;transform:translateY(-2px)}.shape-h2-right{font-family:Calistoga,serif;font-weight:400;font-size:.625rem;letter-spacing:0%;color:#ffffff80;text-shadow:2px 2px 4px rgba(0,0,0,.3);cursor:pointer;transition:all .3s ease;border:none;outline:none;white-space:nowrap;pointer-events:auto;z-index:100;position:relative;margin-right:3.75rem}.shape-h2-right:hover{opacity:.8;transform:translateY(-2px)}.scroll-when-long{display:inline-block;max-width:4.5625rem;white-space:nowrap;overflow-x:auto;overflow-y:hidden;-webkit-overflow-scrolling:touch}.scroll-when-long::-webkit-scrollbar{height:2px}.scroll-when-long::-webkit-scrollbar-track{background:#eee;border-radius:.625rem}.scroll-when-long::-webkit-scrollbar-thumb{background:#888;border-radius:.625rem}.scroll-when-long::-webkit-scrollbar-thumb:hover{background:#555}.shape-text p{cursor:default;-webkit-user-select:none;user-select:none}.shape-text p:active{cursor:default}#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:fixed;top:25%;left:50%;transform:translate(-50%,-50%);background:transparent;color:#fff;padding:20px 30px;border-radius:10px;text-align:center;z-index:1000;display:flex;flex-direction:column;align-items:center;gap:8px}.loading-spinner{width:40px;height:40px;border:4px solid rgba(255,255,255,.3);border-top:4px solid #667eea;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.loading-indicator p{margin:0;font-size:16px;font-weight:500}.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}\n"], dependencies: [{ kind: "directive", type: i2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] });
2342
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: ProfileComparisonLibComponent, deps: [{ token: ProfileComparisonBackendService }, { token: i0.Renderer2 }, { token: FileConversionService }, { token: ImageCompressionService }, { token: i0.ChangeDetectorRef }, { token: PROFILE_COMPARISON_VERBOSE_LOGGING, optional: true }], target: i0.ɵɵFactoryTarget.Component });
2343
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.19", type: ProfileComparisonLibComponent, isStandalone: false, selector: "lib-profile-comparison", inputs: { config: "config", backendMode: "backendMode", backendUrl: "backendUrl", fadeAllEdges: "fadeAllEdges" }, 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: "leftFrame", first: true, predicate: ["leftFrame"], descendants: true }, { propertyName: "rightFrame", first: true, predicate: ["rightFrame"], descendants: true }, { propertyName: "leftBgFrame", first: true, predicate: ["leftBgFrame"], descendants: true }, { propertyName: "rightBgFrame", first: true, predicate: ["rightBgFrame"], descendants: true }, { propertyName: "shapeContainer", first: true, predicate: ["shapeContainer"], 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 profile-comparison\" *ngIf=\"backendConfigured\"\n [style.--edge-shading-color]=\"config.shadingColor || 'rgba(44, 40, 47, 0.0)'\"\n [style.--edge-shading-width]=\"'28px'\"\n [style.--edge-shading-display]=\"config.edgeShading !== false ? 'block' : 'none'\"\n [style.--edge-mask-left]=\"config.edgeShading !== false ? 'linear-gradient(to right, transparent 0%, rgba(0,0,0,0.4) 14px, black 20px)' : 'none'\"\n [style.--edge-mask-right]=\"config.edgeShading !== false ? 'linear-gradient(to left, transparent 0%, rgba(0,0,0,0.4) 14px, black 20px)' : 'none'\">\n <div class=\"backend-error-message\" *ngIf=\"backendError\">{{ backendError }}</div>\n\n <div class=\"profile-comparison__inner\" #shapeContainer>\n <!-- Backgrounds layer: structurally below everything else to fix stacking -->\n <div class=\"profile-comparison__bg-layer\">\n <div class=\"profile-comparison__frame profile-comparison__frame--left bg-frame\"\n [class.is-animating-nudge]=\"leftShapeAnimating\"\n #leftBgFrame>\n <div\n class=\"profile-comparison__bg profile-comparison__bg--left\"\n [ngStyle]=\"{ 'background-image': 'url(' + user1Image + ')', 'transform': user1Transform, 'object-position': user1ObjectPosition }\"\n ></div>\n </div>\n <div class=\"profile-comparison__frame profile-comparison__frame--right bg-frame\"\n [class.is-animating-nudge]=\"rightShapeAnimating\"\n #rightBgFrame>\n <div\n class=\"profile-comparison__bg profile-comparison__bg--right\"\n [ngStyle]=\"{ 'background-image': 'url(' + user2Image + ')', 'transform': user2Transform, 'object-position': user2ObjectPosition }\"\n ></div>\n </div>\n </div>\n\n <!-- Active interactive shapes and content layer -->\n <div class=\"profile-comparison__frame profile-comparison__frame--left\"\n [class.is-animating-nudge]=\"leftShapeAnimating\"\n (click)=\"onShapeClick('left')\"\n #leftFrame>\n <svg\n class=\"profile-comparison__frame-svg profile-comparison__frame-svg--glass\"\n width=\"256\"\n height=\"275\"\n viewBox=\"0 0 256 275\"\n preserveAspectRatio=\"none\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n aria-hidden=\"true\"\n >\n <g filter=\"url(#filter-glass-left)\">\n <path\n d=\"M246.177 26.4924 L0 0 M0 278 L246.68 239.869 C252.043 239.04 256 234.407 256 228.981 V37.4497 C256 31.8302 251.764 27.0937 246.177 26.4924\"\n stroke=\"#61C2AB\"\n stroke-opacity=\"0.3\"\n stroke-width=\"0.809707\"\n stroke-linejoin=\"round\"\n />\n </g>\n <defs>\n <filter id=\"filter-glass-left\" x=\"-20\" y=\"-20\" width=\"300\" height=\"320\" filterUnits=\"userSpaceOnUse\" color-interpolation-filters=\"sRGB\">\n <feFlood flood-opacity=\"0\" result=\"BackgroundImageFix\"/>\n <feColorMatrix in=\"SourceAlpha\" type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0\" result=\"hardAlpha\"/>\n <feOffset/>\n <feGaussianBlur stdDeviation=\"6.3562\"/>\n <feComposite in2=\"hardAlpha\" operator=\"out\"/>\n <feColorMatrix type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0\"/>\n <feBlend mode=\"normal\" in2=\"BackgroundImageFix\" result=\"effect1\"/>\n <feGaussianBlur in=\"effect1\" stdDeviation=\"6.3562\" result=\"effect2\"/>\n <feGaussianBlur in=\"effect2\" stdDeviation=\"6.3562\" result=\"effect3\"/>\n <feBlend mode=\"normal\" in=\"SourceGraphic\" in2=\"effect3\" result=\"shape\"/>\n </filter>\n </defs>\n </svg>\n <svg\n class=\"profile-comparison__frame-svg profile-comparison__frame-svg--middle\"\n width=\"256\"\n height=\"275\"\n viewBox=\"0 0 256 275\"\n preserveAspectRatio=\"none\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n aria-hidden=\"true\"\n >\n <g filter=\"url(#filter0_i_2126_3746)\">\n <path\n d=\"M245.538 25.2463 L-0.638672 -1.2459 V276.7542 L246.042 238.623 C251.404 237.794 255.361 233.161 255.361 227.735 V36.2037 C255.361 30.5841 251.126 25.8476 245.538 25.2463 Z\"\n fill=\"#00CFE6\"\n fill-opacity=\"0.07\"\n />\n </g>\n <defs>\n <filter id=\"filter0_i_2126_3746\" x=\"-0.638672\" y=\"-2\" width=\"256\" height=\"279\" filterUnits=\"userSpaceOnUse\" color-interpolation-filters=\"sRGB\">\n <feFlood flood-opacity=\"0\" result=\"BackgroundImageFix\" />\n <feBlend mode=\"normal\" in=\"SourceGraphic\" in2=\"BackgroundImageFix\" result=\"shape\" />\n <feColorMatrix in=\"SourceAlpha\" type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0\" result=\"hardAlpha\" />\n <feOffset />\n <feGaussianBlur stdDeviation=\"7.95\" />\n <feComposite in2=\"hardAlpha\" operator=\"arithmetic\" k2=\"-1\" k3=\"1\" />\n <feColorMatrix type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0.516667 0 0 0 0 1 0 0 0 1 0\" />\n <feBlend mode=\"normal\" in2=\"shape\" result=\"effect1_innerShadow_2126_3746\" />\n </filter>\n </defs>\n </svg>\n <svg\n class=\"profile-comparison__frame-svg profile-comparison__frame-svg--left\"\n viewBox=\"0 0 256 278\"\n preserveAspectRatio=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n aria-hidden=\"true\"\n >\n <defs>\n <radialGradient id=\"profile-left-fill\" cx=\"0\" cy=\"0\" r=\"1\" gradientTransform=\"matrix(65.1034 -81.3964 74.955 49.415 128 139)\" gradientUnits=\"userSpaceOnUse\">\n <stop stop-color=\"#0051E6\" stop-opacity=\"0\" />\n <stop offset=\"0.5\" stop-color=\"#0051E6\" stop-opacity=\"0.035\" />\n <stop offset=\"1\" stop-color=\"white\" />\n </radialGradient>\n <linearGradient id=\"profile-left-stroke\" x1=\"128\" y1=\"0\" x2=\"128\" y2=\"278\" gradientUnits=\"userSpaceOnUse\">\n <stop offset=\"0\" stop-color=\"#52FFEB\" />\n <stop offset=\"0.5\" stop-color=\"#41CFA1\" />\n <stop offset=\"1\" stop-color=\"#319990\" />\n </linearGradient>\n </defs>\n <!-- Fill Layer -->\n <path\n d=\"M246.177 26.4924 L0 0 V278 L246.68 239.869 C252.043 239.04 256 234.407 256 228.981 V37.4497 C256 31.8302 251.764 27.0937 246.177 26.4924 Z\"\n fill=\"url(#profile-left-fill)\"\n fill-opacity=\"0.07\"\n />\n <!-- Stroke Layer (Left outer border skipped) -->\n <path\n d=\"M246.177 26.4924 L0 0 M0 278 L246.68 239.869 C252.043 239.04 256 234.407 256 228.981 V37.4497 C256 31.8302 251.764 27.0937 246.177 26.4924\"\n fill=\"none\"\n stroke=\"url(#profile-left-stroke)\"\n stroke-width=\"2\"\n stroke-miterlimit=\"3.99393\"\n stroke-linejoin=\"round\"\n />\n <!-- Added explicit embedded SVG text anchoring tightly inside the graphical shape bounds instead of pure flexbox logic padding. -->\n <g\n style=\"cursor: pointer !important;\"\n (click)=\"onViewProfile('left'); $event.stopPropagation()\"\n (mousedown)=\"$event.stopPropagation()\"\n >\n <rect x=\"0\" y=\"240\" width=\"100\" height=\"40\" fill=\"transparent\" pointer-events=\"auto\" />\n <text\n x=\"15\" y=\"260\"\n fill=\"rgba(255, 255, 255, 0.60)\"\n font-family=\"'Calistoga', serif\"\n font-size=\"9\"\n text-anchor=\"start\"\n [class.is-animating-bob]=\"leftProfileClicked\"\n style=\"text-shadow: 0 2px 4px rgba(0,0,0,0.8);\"\n pointer-events=\"none\"\n >View Profile</text>\n </g>\n </svg>\n <div class=\"profile-comparison__content profile-comparison__content--left\">\n <div class=\"profile-comparison__list profile-comparison__list--left\">\n <ng-container\n *ngFor=\"\n let interest of alignedPerson1Interests.length > 0\n ? alignedPerson1Interests\n : displayPerson1Interests.length > 0\n ? displayPerson1Interests\n : person1Interests;\n let i = index\n \"\n >\n <div class=\"profile-comparison__item profile-comparison__item--left\">\n <lib-marquee\n class=\"profile-comparison__text\"\n *ngIf=\"!shouldShowDash(interest, i)\"\n [title]=\"interest\"\n [onlyMarqueeOnHover]=\"false\"\n style=\"--marquee-font-size: 15px; --marquee-font-family: 'Gilroy', sans-serif; --marquee-font-color: white; --marquee-font-weight: 100; --marquee-animation-duration: 8s;\"\n ></lib-marquee>\n <svg *ngIf=\"shouldShowDash(interest, i)\" class=\"profile-comparison__dash\" width=\"30\" height=\"4\" viewBox=\"0 0 30 4\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n <path d=\"M2 2H28\" stroke=\"white\" stroke-width=\"4\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n </svg>\n </div>\n </ng-container>\n </div>\n </div>\n </div>\n\n <div class=\"profile-comparison__frame profile-comparison__frame--right\"\n [class.is-animating-nudge]=\"rightShapeAnimating\"\n (click)=\"onShapeClick('right')\"\n #rightFrame>\n <svg\n class=\"profile-comparison__frame-svg profile-comparison__frame-svg--glass\"\n width=\"257\"\n height=\"275\"\n viewBox=\"0 0 257 275\"\n preserveAspectRatio=\"none\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n aria-hidden=\"true\"\n >\n <g filter=\"url(#filter-glass-right)\">\n <path\n d=\"M256.24 277.5 L243.564 275.546 L9.32411 239.437 C3.95944 238.61 0 233.976 0 228.548 V37.4032 C0 31.7824 4.2375 27.0452 9.8262 26.4454 L244.066 1.30651 L256.24 0\"\n stroke=\"#AB4AFF\"\n stroke-opacity=\"0.3\"\n stroke-width=\"0.809707\"\n stroke-linejoin=\"round\"\n />\n </g>\n <defs>\n <filter id=\"filter-glass-right\" x=\"-20\" y=\"-20\" width=\"300\" height=\"320\" filterUnits=\"userSpaceOnUse\" color-interpolation-filters=\"sRGB\">\n <feFlood flood-opacity=\"0\" result=\"BackgroundImageFix\"/>\n <feColorMatrix in=\"SourceAlpha\" type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0\" result=\"hardAlpha\"/>\n <feOffset/>\n <feGaussianBlur stdDeviation=\"6.3562\"/>\n <feComposite in2=\"hardAlpha\" operator=\"out\"/>\n <feColorMatrix type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0\"/>\n <feBlend mode=\"normal\" in2=\"BackgroundImageFix\" result=\"effect1\"/>\n <feGaussianBlur in=\"effect1\" stdDeviation=\"6.3562\" result=\"effect2\"/>\n <feGaussianBlur in=\"effect2\" stdDeviation=\"6.3562\" result=\"effect3\"/>\n <feBlend mode=\"normal\" in=\"SourceGraphic\" in2=\"effect3\" result=\"shape\"/>\n </filter>\n </defs>\n </svg>\n <svg\n class=\"profile-comparison__frame-svg profile-comparison__frame-svg--middle\"\n width=\"257\"\n height=\"275\"\n viewBox=\"0 0 257 275\"\n preserveAspectRatio=\"none\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n aria-hidden=\"true\"\n >\n <g filter=\"url(#filter0_i_2126_3750)\">\n <path\n d=\"M9.82611 25.203 L244.066 0.0640762 L256.24 -1.2424 V276.258 L243.564 274.304 L9.32402 238.195 C3.95935 237.368 -9.15527e-05 232.733 -9.15527e-05 227.305 V36.1607 C-9.15527e-05 30.5399 4.23741 25.8028 9.82611 25.203 Z\"\n fill=\"#7B00E6\"\n fill-opacity=\"0.07\"\n />\n </g>\n <defs>\n <filter id=\"filter0_i_2126_3750\" x=\"0\" y=\"-2\" width=\"256.24\" height=\"279\" filterUnits=\"userSpaceOnUse\" color-interpolation-filters=\"sRGB\">\n <feFlood flood-opacity=\"0\" result=\"BackgroundImageFix\" />\n <feBlend mode=\"normal\" in=\"SourceGraphic\" in2=\"BackgroundImageFix\" result=\"shape\" />\n <feColorMatrix in=\"SourceAlpha\" type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0\" result=\"hardAlpha\" />\n <feOffset />\n <feGaussianBlur stdDeviation=\"7.95\" />\n <feComposite in2=\"hardAlpha\" operator=\"arithmetic\" k2=\"-1\" k3=\"1\" />\n <feColorMatrix type=\"matrix\" values=\"0 0 0 0 0.482353 0 0 0 0 0 0 0 0 0 0.901961 0 0 0 1 0\" />\n <feBlend mode=\"normal\" in2=\"shape\" result=\"effect1_innerShadow_2126_3750\" />\n </filter>\n </defs>\n </svg>\n <svg\n class=\"profile-comparison__frame-svg profile-comparison__frame-svg--right\"\n viewBox=\"0 0 257 278\"\n preserveAspectRatio=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n aria-hidden=\"true\"\n >\n <defs>\n <radialGradient id=\"profile-right-fill\" cx=\"0\" cy=\"0\" r=\"1\" gradientTransform=\"matrix(-65.1644 -81.25 -75.0252 49.3261 128.12 138.75)\" gradientUnits=\"userSpaceOnUse\">\n <stop stop-color=\"#0051E6\" stop-opacity=\"0\" />\n <stop offset=\"0.5\" stop-color=\"#0051E6\" stop-opacity=\"0.035\" />\n <stop offset=\"1\" stop-color=\"white\" />\n </radialGradient>\n <linearGradient id=\"profile-right-stroke\" x1=\"128.12\" y1=\"277.5\" x2=\"128.12\" y2=\"8.50002\" gradientUnits=\"userSpaceOnUse\">\n <stop offset=\"0\" stop-color=\"#662C99\" />\n <stop offset=\"0.5\" stop-color=\"#893BD3\" />\n <stop offset=\"1\" stop-color=\"#AB4AFF\" />\n </linearGradient>\n </defs>\n <!-- Fill Layer -->\n <path\n d=\"M9.8262 26.4454 L244.066 1.30651 L256.24 0 V277.5 L243.564 275.546 L9.32411 239.437 C3.95944 238.61 0 233.976 0 228.548 V37.4032 C0 31.7824 4.2375 27.0452 9.8262 26.4454 Z\"\n fill=\"url(#profile-right-fill)\"\n fill-opacity=\"0.07\"\n />\n <!-- Stroke Layer (Right outer border skipped) -->\n <path\n d=\"M256.24 277.5 L243.564 275.546 L9.32411 239.437 C3.95944 238.61 0 233.976 0 228.548 V37.4032 C0 31.7824 4.2375 27.0452 9.8262 26.4454 L244.066 1.30651 L256.24 0\"\n fill=\"none\"\n stroke=\"url(#profile-right-stroke)\"\n stroke-width=\"2\"\n stroke-miterlimit=\"3.99393\"\n stroke-linejoin=\"round\"\n />\n <g\n style=\"cursor: pointer !important;\"\n (click)=\"onViewProfile('right'); $event.stopPropagation()\"\n (mousedown)=\"$event.stopPropagation()\"\n >\n <rect x=\"156\" y=\"240\" width=\"100\" height=\"40\" fill=\"transparent\" pointer-events=\"auto\" />\n <text\n x=\"241\" y=\"260\"\n fill=\"rgba(255, 255, 255, 0.60)\"\n font-family=\"'Calistoga', serif\"\n font-size=\"9\"\n text-anchor=\"end\"\n [class.is-animating-bob]=\"rightProfileClicked\"\n style=\"text-shadow: 0 2px 4px rgba(0,0,0,0.8);\"\n pointer-events=\"auto\"\n >View Profile</text>\n </g>\n </svg>\n <div class=\"profile-comparison__content profile-comparison__content--right\">\n <div class=\"profile-comparison__list profile-comparison__list--right\">\n <ng-container\n *ngFor=\"\n let interest of alignedPerson2Interests.length > 0\n ? alignedPerson2Interests\n : displayPerson2Interests.length > 0\n ? displayPerson2Interests\n : person2Interests;\n let i = index\n \"\n >\n <div class=\"profile-comparison__item profile-comparison__item--right\">\n <lib-marquee\n class=\"profile-comparison__text\"\n *ngIf=\"!shouldShowDash(interest, i)\"\n [title]=\"interest\"\n [onlyMarqueeOnHover]=\"false\"\n style=\"--marquee-font-size: 15px; --marquee-font-family: 'Gilroy', sans-serif; --marquee-font-color: white; --marquee-font-weight: 100; --marquee-animation-duration: 8s;\"\n ></lib-marquee>\n <svg *ngIf=\"shouldShowDash(interest, i)\" class=\"profile-comparison__dash\" width=\"30\" height=\"4\" viewBox=\"0 0 30 4\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n <path d=\"M2 2H28\" stroke=\"white\" stroke-width=\"4\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n </svg>\n </div>\n </ng-container>\n </div>\n </div>\n </div>\n\n <!-- Center text section -->\n <div class=\"profile-comparison__center\" #shapeTextCenter>\n <div class=\"profile-comparison__center-inner\">\n <ng-container *ngFor=\"let item of centerItem; let idx = index\">\n <div class=\"profile-comparison__center-item\">\n <!-- Show the label for the first occurrence -->\n <lib-marquee\n *ngIf=\"!isPlaceholder(item) && !isCenterRedundant(idx)\"\n class=\"profile-comparison__center-text\"\n [title]=\"item\"\n [onlyMarqueeOnHover]=\"false\"\n style=\"--marquee-font-size: 15px; --marquee-font-family: 'Gilroy', sans-serif; --marquee-font-color: white; --marquee-font-weight: 700; --marquee-animation-duration: 8s;\"\n ></lib-marquee>\n\n <!-- Show a dash for redundant/collapsed categories -->\n <svg *ngIf=\"isCenterRedundant(idx)\" class=\"profile-comparison__dash\" width=\"30\" height=\"4\" viewBox=\"0 0 30 4\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n <path d=\"M2 2H28\" stroke=\"white\" stroke-width=\"4\" stroke-opacity=\"0.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n </svg>\n\n <!-- Spacers/Placeholders result in empty divs (no dash) by default -->\n </div>\n </ng-container>\n </div>\n </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: ["@keyframes textDropIn{0%{opacity:0;transform:translateY(-8px)}to{opacity:1;transform:translateY(0)}}@keyframes nudgeLeft{0%,to{transform:translate(-24%) scale(.86)}50%{transform:translate(-26%) scale(.86)}}@keyframes nudgeRight{0%,to{transform:translate(24%) scale(.86)}50%{transform:translate(26%) scale(.86)}}.profile-img.left{-webkit-mask-image:linear-gradient(to right,rgb(0,0,0) 0%,rgb(0,0,0) calc(100% - var(--overlap-size, 40px)),rgba(0,0,0,0) 100%);-webkit-mask-size:100% 100%;-webkit-mask-repeat:no-repeat;mask-image:linear-gradient(to right,#fff 0% calc(100% - var(--overlap-size, 40px)),#fff0);mask-size:100% 100%;mask-repeat:no-repeat;margin-right:calc(-.5 * var(--overlap-size, 40px))}.profile-img.right{-webkit-mask-image:linear-gradient(to left,rgb(0,0,0) 0%,rgb(0,0,0) calc(100% - var(--overlap-size, 40px)),rgba(0,0,0,0) 100%);-webkit-mask-size:100% 100%;-webkit-mask-repeat:no-repeat;mask-image:linear-gradient(to left,#fff 0% calc(100% - var(--overlap-size, 40px)),#fff0);mask-size:100% 100%;mask-repeat:no-repeat;margin-left:calc(-.5 * var(--overlap-size, 40px))}.profile-img.fade-all{-webkit-mask-image:linear-gradient(to bottom,transparent 0%,black 10%,black 90%,transparent 100%),linear-gradient(to right,transparent,black 15%,black 85%,transparent);-webkit-mask-composite:source-in;mask-image:linear-gradient(to bottom,transparent 0%,black 10%,black 90%,transparent 100%),linear-gradient(to right,transparent,black 15%,black 85%,transparent);mask-composite:intersect}.profile-img.fade-all.left{-webkit-mask-image:linear-gradient(to bottom,transparent 0%,black 10%,black 90%,transparent 100%),linear-gradient(to right,transparent 0%,black 15%,black calc(100% - var(--overlap-size, 40px)),transparent 100%);mask-image:linear-gradient(to bottom,transparent 0%,black 10%,black 90%,transparent 100%),linear-gradient(to right,transparent 0%,black 15%,black calc(100% - var(--overlap-size, 40px)),transparent 100%)}.profile-img.fade-all.right{-webkit-mask-image:linear-gradient(to bottom,transparent 0%,black 10%,black 90%,transparent 100%),linear-gradient(to left,transparent 0%,black 15%,black calc(100% - var(--overlap-size, 40px)),transparent 100%);mask-image:linear-gradient(to bottom,transparent 0%,black 10%,black 90%,transparent 100%),linear-gradient(to left,transparent 0%,black 15%,black calc(100% - var(--overlap-size, 40px)),transparent 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%;min-width:300px;max-width:600px;margin:0 auto;overflow:visible;position:relative}.profile-flex{display:flex;width:100%;height:414px;align-items:center;overflow:hidden;background:linear-gradient(135deg,#1a1a1a,#0a0a0a);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 15%,black 85%,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 15%,black 85%,transparent 100%),linear-gradient(to bottom,transparent 0%,black 15%,black 85%,transparent 100%);mask-composite:intersect}.shape-bg{position:relative;pointer-events:auto;width:100%;height:100%}.shape-bg1,.shape-bg2{position:absolute;width:75%;max-width:none;height:350px;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;overflow:visible!important}.shape-bg svg:active{cursor:grabbing}.profile-img{width:50%;height:350px;position:relative;flex-shrink:0;background:linear-gradient(135deg,#1a1a1a,#0a0a0a);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 30%;opacity:.75;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}.shape{position:absolute;top:32px;width:100%;left:0;z-index:2;pointer-events:none}.shape-bg{position:relative;pointer-events:auto}.shape-text{position:absolute;top:0;left:0;right:0;width:100%;height:100%;z-index:1;padding:0 1.25rem;box-sizing:border-box;pointer-events:none}.shape-text-left{margin-top:40px;text-align:left;position:absolute;top:0;bottom:0;right:calc(50% + 75px);z-index:2;pointer-events:auto;display:flex;flex-direction:column;align-items:flex-end}.shape-text-left .scroll-container{width:100%}.shape-text-center{position:absolute;left:0;right:0;margin:0 auto;width:fit-content;font-family:Gilroy,sans-serif;font-weight:700;font-size:.75rem;line-height:100%;letter-spacing:0%;color:#fff;text-align:center;display:flex;justify-content:center;align-items:center;flex-direction:column;text-shadow:2px 2px 4px rgba(0,0,0,.3);z-index:10;transition:transform .3s ease;pointer-events:none;top:50%;transform:translateY(-50%);padding:0 .625rem;margin-top:3.8125rem}.shape-text-right{margin-top:40px;text-align:right;position:absolute;top:0;bottom:0;left:calc(50% + 70px);z-index:2;pointer-events:auto;display:flex;flex-direction:column;align-items:flex-start}.shape-text-right .scroll-container{width:100%}.shape-p{font-family:Gilroy,sans-serif;font-weight:400;font-size:.75rem;line-height:100%;letter-spacing:0%;color:#fff;padding-bottom:.125rem;text-align:right;text-shadow:2px 2px 4px rgba(0,0,0,.3);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;direction:rtl;-webkit-user-select:none;user-select:none;margin:0}.shape-p-center{font-family:Gilroy,sans-serif;font-weight:700;font-size:.75rem;line-height:100%;letter-spacing:0%;color:#fff;margin:0;text-align:center;display:flex;justify-content:center;align-items:center;text-shadow:2px 2px 4px rgba(0,0,0,.3)}.shape-p-right{font-family:Gilroy,sans-serif;font-weight:400;font-size:.75rem;line-height:100%;letter-spacing:0%;color:#fff;padding-bottom:.125rem;text-align:left;text-shadow:2px 2px 4px rgba(0,0,0,.3);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin:0;-webkit-user-select:none;user-select:none}.dash-item{color:#ffffff4d!important;font-size:.625rem!important;height:.625rem}.spacer-item{height:auto!important;color:#fffc!important;font-weight:700;font-size:.75rem!important;letter-spacing:2px}.shape-h2{font-family:Calistoga,serif;font-weight:400;font-size:.625rem;letter-spacing:0%;color:#fff;text-shadow:2px 2px 4px rgba(0,0,0,.3);cursor:pointer;transition:all .3s ease;border:none;outline:none;white-space:nowrap;pointer-events:auto;z-index:100;position:relative;margin-left:.5rem}.shape-h2:hover{opacity:.8;transform:translateY(-2px)}.shape-h2-right{font-family:Calistoga,serif;font-weight:400;font-size:.625rem;letter-spacing:0%;color:#fff;text-shadow:2px 2px 4px rgba(0,0,0,.3);cursor:pointer;transition:all .3s ease;border:none;outline:none;white-space:nowrap;pointer-events:auto;z-index:100;position:relative;margin-right:3.75rem}.shape-h2-right:hover{opacity:.8;transform:translateY(-2px)}.scroll-when-long{display:inline-block;max-width:4.5625rem;white-space:nowrap;overflow-x:auto;overflow-y:hidden;-webkit-overflow-scrolling:touch}.scroll-when-long::-webkit-scrollbar{height:2px}.scroll-when-long::-webkit-scrollbar-track{background:#eee;border-radius:.625rem}.scroll-when-long::-webkit-scrollbar-thumb{background:#888;border-radius:.625rem}.scroll-when-long::-webkit-scrollbar-thumb:hover{background:#555}.shape-text p{cursor:default;-webkit-user-select:none;user-select:none}.shape-text p:active{cursor:default}:host{display:block;background-color:#2c282f}.profile-comparison{width:100%;padding:0;box-sizing:border-box;display:flex;justify-content:center;align-items:center;min-height:100vh;background-color:#2c282f}.profile-comparison__inner{position:relative;width:100%;max-width:360px;display:grid;grid-template-columns:1fr;grid-template-rows:auto;background:transparent}.profile-comparison__bg-layer{grid-area:1/1;position:relative;width:100%;height:100%;pointer-events:none;display:grid;grid-template-columns:1fr;grid-template-rows:1fr}.profile-comparison__frame{grid-area:1/1;position:relative;pointer-events:none}.profile-comparison__frame.bg-frame{width:100%;height:100%;overflow:hidden;-webkit-mask-composite:source-in;mask-composite:intersect}.profile-comparison__frame.bg-frame.profile-comparison__frame--left{clip-path:polygon(0% 0%,96.2% 9.5%,100% 13.5%,100% 82.4%,96.4% 86.3%,0% 100%);-webkit-mask-image:var(--v-mask),linear-gradient(to right,black 40%,transparent 100%);mask-image:var(--v-mask),linear-gradient(to right,black 40%,transparent 100%)}.profile-comparison__frame.bg-frame.profile-comparison__frame--right{clip-path:polygon(100% 0%,100% 100%,94.7% 99.3%,3.6% 86.3%,0% 82.4%,0% 13.5%,3.8% 9.5%,95.3% .5%);-webkit-mask-image:var(--v-mask),linear-gradient(to left,black 40%,transparent 100%);mask-image:var(--v-mask),linear-gradient(to left,black 40%,transparent 100%)}.profile-comparison__frame--left{transform:translate(-24%) scale(.86)}.profile-comparison__frame--left.is-animating-nudge{animation:nudgeLeft .5s ease-out}.profile-comparison__frame--left .profile-comparison__frame-svg{-webkit-mask-image:var(--edge-mask-left, linear-gradient(to right, transparent 0%, rgba(0, 0, 0, .4) 14px, black var(--edge-shading-width, 28px)));mask-image:var(--edge-mask-left, linear-gradient(to right, transparent 0%, rgba(0, 0, 0, .4) 14px, black var(--edge-shading-width, 28px)))}.profile-comparison__frame--left:after{content:\"\";position:absolute;inset:0;pointer-events:none;background:linear-gradient(to right,var(--edge-shading-color, rgba(44, 40, 47, .7)) 0%,rgba(44,40,47,.3) 14px,transparent var(--edge-shading-width, 28px));z-index:5;display:var(--edge-shading-display, block)}.profile-comparison__frame--left .profile-comparison__frame-svg--glass{transform:scale(1.02) translate(1px);transform-origin:center}.profile-comparison__frame--right{transform:translate(24%) scale(.86)}.profile-comparison__frame--right.is-animating-nudge{animation:nudgeRight .5s ease-out}.profile-comparison__frame--right .profile-comparison__frame-svg{-webkit-mask-image:var(--edge-mask-right, linear-gradient(to left, transparent 0%, rgba(0, 0, 0, .4) 14px, black var(--edge-shading-width, 28px)));mask-image:var(--edge-mask-right, linear-gradient(to left, transparent, black var(--edge-shading-width, 28px)))}.profile-comparison__frame--right:after{content:\"\";position:absolute;inset:0;pointer-events:none;background:linear-gradient(to left,var(--edge-shading-color, rgba(44, 40, 47, .7)) 0%,rgba(44,40,47,.3) 14px,transparent var(--edge-shading-width, 28px));z-index:5;display:var(--edge-shading-display, block)}.profile-comparison__frame--right .profile-comparison__frame-svg--glass{transform:scale(1.02) translate(-1px);transform-origin:center}.profile-comparison__frame--right .profile-comparison__frame-svg--bottom{width:113.2%}.profile-comparison__frame-svg{position:absolute;inset:0;width:100%;height:100%;pointer-events:auto;overflow:visible}.profile-comparison__frame-svg--glass{z-index:1}.profile-comparison__frame-svg--middle{z-index:2}.profile-comparison__frame-svg--left,.profile-comparison__frame-svg--right{z-index:3}.profile-comparison__frame-svg--bottom{width:113.7%;height:112.6%;inset:51% auto auto 50%;transform:translate(-50%,-51%)}.profile-comparison__bg{position:absolute;inset:0;background-size:360px auto;background-repeat:no-repeat;filter:blur(2px);pointer-events:auto;transition:filter .6s ease;--vignette-mask: linear-gradient(to bottom, transparent, black 10%, black 90%, transparent), linear-gradient(to right, transparent, black 10%, black 90%, transparent)}.profile-comparison__bg--left{background-position:left center;margin-right:20%;-webkit-mask-image:var(--vignette-mask),linear-gradient(to right,black 50%,transparent 100%);mask-image:var(--vignette-mask),linear-gradient(to right,black 50%,transparent 100%);-webkit-mask-composite:source-in;mask-composite:intersect}.profile-comparison__bg--right{background-position:right center;margin-left:20%;-webkit-mask-image:var(--vignette-mask),linear-gradient(to left,black 50%,transparent 100%);mask-image:var(--vignette-mask),linear-gradient(to left,black 50%,transparent 100%);-webkit-mask-composite:source-in;mask-composite:intersect}@keyframes viewProfileBob{0%{transform:translateY(0)}50%{transform:translateY(-3px)}to{transform:translateY(0)}}.profile-comparison svg text{transition:opacity .3s ease}.profile-comparison svg text.is-animating-bob{animation:viewProfileBob .6s ease-in-out;fill:#fff!important}.profile-comparison__content{position:relative;z-index:20;padding:50px 12% 75px;min-height:100%;display:flex;flex-direction:column;justify-content:flex-start;color:#ecf0f1;font-family:Gilroy,serif;pointer-events:none}.profile-comparison__content--left{align-items:flex-end;text-align:right;padding-right:30%}.profile-comparison__content--right{align-items:flex-start;text-align:left;padding-left:30%}.profile-comparison__list{display:flex;flex-direction:column;gap:10px;width:100%;pointer-events:auto}.profile-comparison__list--left{align-items:flex-end;text-align:right;padding-right:30%}.profile-comparison__list--right{align-items:flex-start;text-align:left;padding-left:30%}.profile-comparison__item{display:flex;align-items:center;height:22px;width:170px;flex-shrink:0;animation:textDropIn .4s ease-out backwards}.profile-comparison__item--left{justify-content:flex-end}.profile-comparison__item--left .profile-comparison__dash{margin-right:12px;flex-shrink:0}.profile-comparison__item--right{justify-content:flex-start}.profile-comparison__item--right .profile-comparison__dash{margin-left:12px;flex-shrink:0}.profile-comparison__text{font-family:Gilroy,sans-serif;font-size:15px;font-weight:100!important;color:#fff;width:100%;display:block}.profile-comparison__text ::ng-deep *{font-weight:400!important}.profile-comparison__dash{display:block}.profile-comparison__item-text{display:none}.profile-comparison__center{grid-area:1/1;display:flex;align-items:flex-start;justify-content:center;z-index:15;pointer-events:none;transform:scale(.86)}.profile-comparison__center-inner{display:flex;flex-direction:column;align-items:center;text-align:center;justify-content:flex-start;padding:50px 0 75px;gap:10px;width:100%}.profile-comparison__center-item{height:22px;display:flex;align-items:center;justify-content:center;width:140px;flex-shrink:0;animation:textDropIn .4s ease-out backwards}.profile-comparison__center-text{font-family:Gilroy,sans-serif;font-size:15px;font-weight:700;color:#fff;width:100%;display:block}.profile-comparison__center-text--empty{color:#ecf0f180}@media(max-width:480px){.profile-comparison{padding:12px}.profile-comparison__inner{max-width:300px}.profile-comparison__name{font-size:11px}.profile-comparison__list{font-size:9px}.profile-comparison__center-text{font-size:10px}}.loading-indicator{position:fixed;top:25%;left:50%;transform:translate(-50%,-50%);background:transparent;color:#fff;padding:20px 30px;border-radius:10px;text-align:center;z-index:1000;display:flex;flex-direction:column;align-items:center;gap:8px}.loading-spinner{width:40px;height:40px;border:4px solid rgba(255,255,255,.3);border-top:4px solid #667eea;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.loading-indicator p{margin:0;font-size:16px;font-weight:500}.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}\n"], dependencies: [{ kind: "directive", type: i2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i2.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "component", type: i3.MarqueeComponent, selector: "lib-marquee", inputs: ["title", "searchText", "padding", "onlyMarqueeOnHover"], outputs: ["marquee"] }] });
3022
2344
  }
3023
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: ProfileComparisonLibComponent, decorators: [{
2345
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: ProfileComparisonLibComponent, decorators: [{
3024
2346
  type: Component,
3025
- 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 class=\"backend-error-message\" *ngIf=\"backendError\">{{ backendError }}</div>\n <div #profileFlex class=\"profile-flex\" [class.fade-all]=\"fadeAllEdges\">\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.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.object-position]=\"user2ObjectPosition\"\n (load)=\"onUserImageLoad(2, $event)\"\n />\n <!-- [style.object-fit]=\"'cover'\" -->\n </div>\n </div>\n\n <div #shapeContainer class=\"shape\">\n <div #shapeBg class=\"shape-bg\" [class.fade-all]=\"fadeAllEdges\">\n <svg width=\"250\" height=\"350\" viewBox=\"0 0 250 350\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" preserveAspectRatio=\"xMaxYMid slice\" style=\"height: 350px;\" class=\"shape-bg1\" #shapeBg1>\n<g filter=\"url(#filter0_i_4_4137)\">\n<path d=\"M215.177 44.4924L-18.823 19.3104C-25.3264 18.6106 -31 23.7064 -31 30.2473V283.169C-31 289.914 -24.9854 295.07 -18.3196 294.04L215.68 257.869C221.043 257.04 225 252.407 225 246.981V55.4497C225 49.8302 220.764 45.0937 215.177 44.4924Z\" fill=\"#00CFE6\" fill-opacity=\"0.07\"/>\n</g>\n<path d=\"M215.177 44.4924L-18.823 19.3104C-25.3264 18.6106 -31 23.7064 -31 30.2473V283.169C-31 289.914 -24.9854 295.07 -18.3196 294.04L215.68 257.869C221.043 257.04 225 252.407 225 246.981V55.4497C225 49.8302 220.764 45.0937 215.177 44.4924Z\" fill=\"url(#paint0_radial_4_4137)\" fill-opacity=\"0.07\" stroke=\"url(#paint1_linear_4_4137)\" stroke-width=\"2\" stroke-miterlimit=\"3.99393\" stroke-linejoin=\"round\"/>\n<g filter=\"url(#filter1_ddddd_4_4137)\">\n<path d=\"M219.081 43.0755L-18.9193 17.2045C-24.8351 16.5614 -30 21.1953 -30 27.1459V287.318C-30 293.455 -24.5217 298.145 -18.4573 297.198L219.543 260.038C224.411 259.278 228 255.075 228 250.148V53.0303C228 47.9257 224.155 43.6271 219.081 43.0755Z\" stroke=\"#61C2AB\" stroke-opacity=\"0.3\" stroke-linejoin=\"round\" shape-rendering=\"crispEdges\"/>\n</g>\n<defs>\n<filter id=\"filter0_i_4_4137\" x=\"-31\" y=\"19.2461\" width=\"256\" height=\"274.925\" filterUnits=\"userSpaceOnUse\" color-interpolation-filters=\"sRGB\">\n<feFlood flood-opacity=\"0\" result=\"BackgroundImageFix\"/>\n<feBlend mode=\"normal\" in=\"SourceGraphic\" in2=\"BackgroundImageFix\" result=\"shape\"/>\n<feColorMatrix in=\"SourceAlpha\" type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0\" result=\"hardAlpha\"/>\n<feOffset/>\n<feGaussianBlur stdDeviation=\"7.95\"/>\n<feComposite in2=\"hardAlpha\" operator=\"arithmetic\" k2=\"-1\" k3=\"1\"/>\n<feColorMatrix type=\"matrix\" values=\"0 0 0 0 0.516667 0 0 0 0 1 0 0 0 1 0 0 0 1 0\"/>\n<feBlend mode=\"normal\" in2=\"shape\" result=\"effect1_innerShadow_4_4137\"/>\n</filter>\n<filter id=\"filter1_ddddd_4_4137\" x=\"-46.2\" y=\"0.944715\" width=\"290.4\" height=\"312.575\" filterUnits=\"userSpaceOnUse\" color-interpolation-filters=\"sRGB\">\n<feFlood flood-opacity=\"0\" result=\"BackgroundImageFix\"/>\n<feColorMatrix in=\"SourceAlpha\" type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0\" result=\"hardAlpha\"/>\n<feOffset/>\n<feGaussianBlur stdDeviation=\"7.95\"/>\n<feComposite in2=\"hardAlpha\" operator=\"out\"/>\n<feColorMatrix type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0\"/>\n<feBlend mode=\"normal\" in2=\"BackgroundImageFix\" result=\"effect1_dropShadow_4_4137\"/>\n<feColorMatrix in=\"SourceAlpha\" type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0\" result=\"hardAlpha\"/>\n<feOffset/>\n<feGaussianBlur stdDeviation=\"7.85\"/>\n<feComposite in2=\"hardAlpha\" operator=\"out\"/>\n<feColorMatrix type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0\"/>\n<feBlend mode=\"normal\" in2=\"effect1_dropShadow_4_4137\" result=\"effect2_dropShadow_4_4137\"/>\n<feColorMatrix in=\"SourceAlpha\" type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0\" result=\"hardAlpha\"/>\n<feOffset/>\n<feGaussianBlur stdDeviation=\"7.85\"/>\n<feComposite in2=\"hardAlpha\" operator=\"out\"/>\n<feColorMatrix type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0\"/>\n<feBlend mode=\"normal\" in2=\"effect2_dropShadow_4_4137\" result=\"effect3_dropShadow_4_4137\"/>\n<feColorMatrix in=\"SourceAlpha\" type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0\" result=\"hardAlpha\"/>\n<feOffset/>\n<feGaussianBlur stdDeviation=\"7.85\"/>\n<feComposite in2=\"hardAlpha\" operator=\"out\"/>\n<feColorMatrix type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0\"/>\n<feBlend mode=\"normal\" in2=\"effect3_dropShadow_4_4137\" result=\"effect4_dropShadow_4_4137\"/>\n<feColorMatrix in=\"SourceAlpha\" type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0\" result=\"hardAlpha\"/>\n<feOffset/>\n<feGaussianBlur stdDeviation=\"7.85\"/>\n<feComposite in2=\"hardAlpha\" operator=\"out\"/>\n<feColorMatrix type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0\"/>\n<feBlend mode=\"normal\" in2=\"effect4_dropShadow_4_4137\" result=\"effect5_dropShadow_4_4137\"/>\n<feBlend mode=\"normal\" in=\"SourceGraphic\" in2=\"effect5_dropShadow_4_4137\" result=\"shape\"/>\n</filter>\n<radialGradient id=\"paint0_radial_4_4137\" cx=\"0\" cy=\"0\" r=\"1\" gradientTransform=\"matrix(65.1034 -81.3964 74.955 49.415 97 157)\" gradientUnits=\"userSpaceOnUse\">\n<stop stop-color=\"#0051E6\" stop-opacity=\"0\"/>\n<stop offset=\"0.615385\" stop-color=\"#0051E6\" stop-opacity=\"0\"/>\n<stop offset=\"1\" stop-color=\"white\"/>\n</radialGradient>\n<linearGradient id=\"paint1_linear_4_4137\" x1=\"97\" y1=\"18\" x2=\"97\" y2=\"296\" gradientUnits=\"userSpaceOnUse\">\n<stop offset=\"0.380829\" stop-color=\"#52FFEB\"/>\n<stop offset=\"1\" stop-color=\"#319990\"/>\n</linearGradient>\n</defs>\n</svg>\n <svg width=\"250\" height=\"350\" viewBox=\"0 0 250 350\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" preserveAspectRatio=\"xMinYMid slice\" style=\"height: 350px;\" class=\"shape-bg2\" #shapeBg2>\n<g filter=\"url(#filter0_i_4_4141)\">\n<path d=\"M29.5864 44.9454L263.826 19.8065C270.329 19.1087 276 24.2041 276 30.7437V283.174C276 289.917 269.988 295.073 263.324 294.046L29.0843 257.937C23.7196 257.11 19.7602 252.476 19.7602 247.048V55.9032C19.7602 50.2824 23.9977 45.5452 29.5864 44.9454Z\" fill=\"#7B00E6\" fill-opacity=\"0.07\"/>\n</g>\n<path d=\"M29.5864 44.9454L263.826 19.8065C270.329 19.1087 276 24.2041 276 30.7437V283.174C276 289.917 269.988 295.073 263.324 294.046L29.0843 257.937C23.7196 257.11 19.7602 252.476 19.7602 247.048V55.9032C19.7602 50.2824 23.9977 45.5452 29.5864 44.9454Z\" fill=\"url(#paint0_radial_4_4141)\" fill-opacity=\"0.07\" stroke=\"url(#paint1_linear_4_4141)\" stroke-width=\"2\" stroke-miterlimit=\"3.99393\" stroke-linejoin=\"round\"/>\n<g filter=\"url(#filter1_ddddd_4_4141)\">\n<path d=\"M25.9212 43.077L264.37 17.2022C270.285 16.5603 275.449 21.194 275.449 27.1438V287.321C275.449 293.457 269.972 298.146 263.909 297.201L25.46 260.036C20.5905 259.277 17 255.074 17 250.146V53.032C17 47.9267 20.8457 43.6277 25.9212 43.077Z\" stroke=\"#C37DFF\" stroke-opacity=\"0.3\" stroke-linejoin=\"round\" shape-rendering=\"crispEdges\"/>\n</g>\n<defs>\n<filter id=\"filter0_i_4_4141\" x=\"19.7602\" y=\"19.7425\" width=\"256.24\" height=\"274.434\" filterUnits=\"userSpaceOnUse\" color-interpolation-filters=\"sRGB\">\n<feFlood flood-opacity=\"0\" result=\"BackgroundImageFix\"/>\n<feBlend mode=\"normal\" in=\"SourceGraphic\" in2=\"BackgroundImageFix\" result=\"shape\"/>\n<feColorMatrix in=\"SourceAlpha\" type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0\" result=\"hardAlpha\"/>\n<feOffset/>\n<feGaussianBlur stdDeviation=\"7.95\"/>\n<feComposite in2=\"hardAlpha\" operator=\"arithmetic\" k2=\"-1\" k3=\"1\"/>\n<feColorMatrix type=\"matrix\" values=\"0 0 0 0 0.482353 0 0 0 0 0 0 0 0 0 0.901961 0 0 0 1 0\"/>\n<feBlend mode=\"normal\" in2=\"shape\" result=\"effect1_innerShadow_4_4141\"/>\n</filter>\n<filter id=\"filter1_ddddd_4_4141\" x=\"0.8\" y=\"0.942639\" width=\"290.849\" height=\"312.58\" filterUnits=\"userSpaceOnUse\" color-interpolation-filters=\"sRGB\">\n<feFlood flood-opacity=\"0\" result=\"BackgroundImageFix\"/>\n<feColorMatrix in=\"SourceAlpha\" type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0\" result=\"hardAlpha\"/>\n<feOffset/>\n<feGaussianBlur stdDeviation=\"7.85\"/>\n<feComposite in2=\"hardAlpha\" operator=\"out\"/>\n<feColorMatrix type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0\"/>\n<feBlend mode=\"normal\" in2=\"BackgroundImageFix\" result=\"effect1_dropShadow_4_4141\"/>\n<feColorMatrix in=\"SourceAlpha\" type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0\" result=\"hardAlpha\"/>\n<feOffset/>\n<feGaussianBlur stdDeviation=\"7.85\"/>\n<feComposite in2=\"hardAlpha\" operator=\"out\"/>\n<feColorMatrix type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0\"/>\n<feBlend mode=\"normal\" in2=\"effect1_dropShadow_4_4141\" result=\"effect2_dropShadow_4_4141\"/>\n<feColorMatrix in=\"SourceAlpha\" type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0\" result=\"hardAlpha\"/>\n<feOffset/>\n<feGaussianBlur stdDeviation=\"7.85\"/>\n<feComposite in2=\"hardAlpha\" operator=\"out\"/>\n<feColorMatrix type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0\"/>\n<feBlend mode=\"normal\" in2=\"effect2_dropShadow_4_4141\" result=\"effect3_dropShadow_4_4141\"/>\n<feColorMatrix in=\"SourceAlpha\" type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0\" result=\"hardAlpha\"/>\n<feOffset/>\n<feGaussianBlur stdDeviation=\"7.85\"/>\n<feComposite in2=\"hardAlpha\" operator=\"out\"/>\n<feColorMatrix type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0\"/>\n<feBlend mode=\"normal\" in2=\"effect3_dropShadow_4_4141\" result=\"effect4_dropShadow_4_4141\"/>\n<feColorMatrix in=\"SourceAlpha\" type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0\" result=\"hardAlpha\"/>\n<feOffset/>\n<feGaussianBlur stdDeviation=\"7.85\"/>\n<feComposite in2=\"hardAlpha\" operator=\"out\"/>\n<feColorMatrix type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0\"/>\n<feBlend mode=\"normal\" in2=\"effect4_dropShadow_4_4141\" result=\"effect5_dropShadow_4_4137\"/>\n<feBlend mode=\"normal\" in=\"SourceGraphic\" in2=\"effect5_dropShadow_4_4137\" result=\"shape\"/>\n</filter>\n<radialGradient id=\"paint0_radial_4_4137\" cx=\"0\" cy=\"0\" r=\"1\" gradientTransform=\"matrix(65.1034 -81.3964 74.955 49.415 97 157)\" gradientUnits=\"userSpaceOnUse\">\n<stop stop-color=\"#0051E6\" stop-opacity=\"0\"/>\n<stop offset=\"0.615385\" stop-color=\"#0051E6\" stop-opacity=\"0\"/>\n<stop offset=\"1\" stop-color=\"white\"/>\n</radialGradient>\n<linearGradient id=\"paint1_linear_4_4137\" x1=\"97\" y1=\"18\" x2=\"97\" y2=\"296\" gradientUnits=\"userSpaceOnUse\">\n<stop offset=\"0.380829\" stop-color=\"#52FFEB\"/>\n<stop offset=\"1\" stop-color=\"#319990\"/>\n</linearGradient>\n</defs>\n</svg>\n </div>\n\n <div class=\"shape-text\">\n <div #shapeTextLeft class=\"shape-text-left\">\n <div class=\"scroll-container\" #leftContainer>\n <ng-container\n *ngFor=\"\n let interest of alignedPerson1Interests.length > 0\n ? alignedPerson1Interests\n : displayPerson1Interests.length > 0\n ? displayPerson1Interests\n : person1Interests;\n let i = index\n \"\n >\n <p\n class=\"shape-p\"\n *ngIf=\"!isInCenter(interest)\"\n [ngClass]=\"{'dash-item': interest === '-', 'spacer-item': interest === '----', 'aligned-item': interest !== '-' && interest !== '----'}\"\n >\n {{ interest === \"-\" ? \"\u2014\" : interest }}\n </p>\n </ng-container>\n </div>\n <h2 class=\"shape-h2\" (click)=\"onViewProfile('left')\">View Profile</h2>\n </div>\n\n <div #shapeTextCenter class=\"shape-text-center\">\n <ng-container *ngFor=\"let item of centerItem\">\n <p class=\"shape-p-center\">{{ item }}</p>\n </ng-container>\n </div>\n <div #shapeTextRight class=\"shape-text-right\">\n <div class=\"scroll-container\" #rightContainer>\n <ng-container\n *ngFor=\"\n let interest of alignedPerson2Interests.length > 0\n ? alignedPerson2Interests\n : displayPerson2Interests.length > 0\n ? displayPerson2Interests\n : person2Interests;\n let i = index\n \"\n >\n <p\n class=\"shape-p-right\"\n *ngIf=\"!isInCenter(interest)\"\n [ngClass]=\"{'dash-item': interest === '-', 'spacer-item': interest === '----', 'aligned-item': interest !== '-' && interest !== '----'}\"\n >\n {{ interest === \"-\" ? \"\u2014\" : interest }}\n </p>\n </ng-container>\n </div>\n <h2 class=\"shape-h2-right\" (click)=\"onViewProfile('right')\">View Profile</h2>\n </div>\n </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: [".profile-img.left{-webkit-mask-image:linear-gradient(to right,rgb(0,0,0) 0%,rgb(0,0,0) calc(100% - var(--overlap-size, 40px)),rgba(0,0,0,0) 100%);-webkit-mask-size:100% 100%;-webkit-mask-repeat:no-repeat;mask-image:linear-gradient(to right,#fff 0% calc(100% - var(--overlap-size, 40px)),#fff0);mask-size:100% 100%;mask-repeat:no-repeat;margin-right:calc(-.5 * var(--overlap-size, 40px))}.profile-img.right{-webkit-mask-image:linear-gradient(to left,rgb(0,0,0) 0%,rgb(0,0,0) calc(100% - var(--overlap-size, 40px)),rgba(0,0,0,0) 100%);-webkit-mask-size:100% 100%;-webkit-mask-repeat:no-repeat;mask-image:linear-gradient(to left,#fff 0% calc(100% - var(--overlap-size, 40px)),#fff0);mask-size:100% 100%;mask-repeat:no-repeat;margin-left:calc(-.5 * var(--overlap-size, 40px))}.profile-img.fade-all{-webkit-mask-image:linear-gradient(to bottom,transparent 75px,black 125px,black 425px,transparent 475px),linear-gradient(to right,transparent,black 15%,black 85%,transparent);-webkit-mask-composite:source-in;mask-image:linear-gradient(to bottom,transparent 75px,black 125px,black 425px,transparent 475px),linear-gradient(to right,transparent,black 15%,black 85%,transparent);mask-composite:intersect}.profile-img.fade-all.left{-webkit-mask-image:linear-gradient(to bottom,transparent 75px,black 125px,black 425px,transparent 475px),linear-gradient(to right,transparent 0%,black 15%,black calc(100% - var(--overlap-size, 40px)),transparent 100%);mask-image:linear-gradient(to bottom,transparent 75px,black 125px,black 425px,transparent 475px),linear-gradient(to right,transparent 0%,black 15%,black calc(100% - var(--overlap-size, 40px)),transparent 100%)}.profile-img.fade-all.right{-webkit-mask-image:linear-gradient(to bottom,transparent 75px,black 125px,black 425px,transparent 475px),linear-gradient(to left,transparent 0%,black 15%,black calc(100% - var(--overlap-size, 40px)),transparent 100%);mask-image:linear-gradient(to bottom,transparent 75px,black 125px,black 425px,transparent 475px),linear-gradient(to left,transparent 0%,black 15%,black calc(100% - var(--overlap-size, 40px)),transparent 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:320px;max-width:100%;margin:0 auto;overflow:hidden;position:relative}.profile-flex{display:flex;width:100%;height:400px;overflow:hidden;background:linear-gradient(135deg,#3a3a3a,#2a2a2a);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 15%,black 85%,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 15%,black 85%,transparent 100%),linear-gradient(to bottom,transparent 0%,black 15%,black 85%,transparent 100%);mask-composite:intersect}.shape-bg1,.shape-bg2{position:absolute;max-width:250px;opacity:.5;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{width:180px;height:550px;position:relative;flex-shrink:0;background:linear-gradient(135deg,#3a3a3a,#2a2a2a);margin:0;padding:0;left:0;top:-75px;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:.6;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}.shape{position:absolute;top:40px;width:100%}.shape-bg{position:relative}.shape-text{position:absolute;top:0;left:0;right:0;width:100%;height:100%;z-index:1;padding:0 1.25rem;box-sizing:border-box;pointer-events:none}.shape-text-left{margin-top:40px;text-align:left;position:absolute;top:0;bottom:0;right:calc(50% + 75px);z-index:2;pointer-events:auto;display:flex;flex-direction:column;align-items:flex-end}.shape-text-left .scroll-container{width:100%}.shape-text-center{position:absolute;left:0;right:0;margin:0 auto;width:fit-content;font-family:Gilroy,sans-serif;font-weight:700;font-size:.75rem;line-height:100%;letter-spacing:0%;color:#fff;text-align:center;display:flex;justify-content:center;align-items:center;flex-direction:column;text-shadow:2px 2px 4px rgba(0,0,0,.3);z-index:10;transition:transform .3s ease;pointer-events:none;top:50%;transform:translateY(-50%);padding:0 .625rem;margin-top:3.8125rem}.shape-text-right{margin-top:40px;text-align:right;position:absolute;top:0;bottom:0;left:calc(50% + 70px);z-index:2;pointer-events:auto;display:flex;flex-direction:column;align-items:flex-start}.shape-text-right .scroll-container{width:100%}.shape-p{font-family:Gilroy,sans-serif;font-weight:400;font-size:.75rem;line-height:100%;letter-spacing:0%;color:#fff;padding-bottom:.125rem;text-align:right;text-shadow:2px 2px 4px rgba(0,0,0,.3);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;direction:rtl;-webkit-user-select:none;user-select:none;margin:0}.shape-p-center{font-family:Gilroy,sans-serif;font-weight:700;font-size:.75rem;line-height:100%;letter-spacing:0%;color:#fff;margin:0;text-align:center;display:flex;justify-content:center;align-items:center;text-shadow:2px 2px 4px rgba(0,0,0,.3)}.shape-p-right{font-family:Gilroy,sans-serif;font-weight:400;font-size:.75rem;line-height:100%;letter-spacing:0%;color:#fff;padding-bottom:.125rem;text-align:left;text-shadow:2px 2px 4px rgba(0,0,0,.3);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin:0;-webkit-user-select:none;user-select:none}.dash-item{color:#ffffff4d!important;font-size:.625rem!important;height:.625rem}.spacer-item{height:auto!important;color:#fffc!important;font-weight:700;font-size:.75rem!important;letter-spacing:2px}.shape-h2{font-family:Calistoga,serif;font-weight:400;font-size:.625rem;letter-spacing:0%;color:#ffffff80;text-shadow:2px 2px 4px rgba(0,0,0,.3);cursor:pointer;transition:all .3s ease;border:none;outline:none;white-space:nowrap;pointer-events:auto;z-index:100;position:relative;margin-left:.5rem}.shape-h2:hover{opacity:.8;transform:translateY(-2px)}.shape-h2-right{font-family:Calistoga,serif;font-weight:400;font-size:.625rem;letter-spacing:0%;color:#ffffff80;text-shadow:2px 2px 4px rgba(0,0,0,.3);cursor:pointer;transition:all .3s ease;border:none;outline:none;white-space:nowrap;pointer-events:auto;z-index:100;position:relative;margin-right:3.75rem}.shape-h2-right:hover{opacity:.8;transform:translateY(-2px)}.scroll-when-long{display:inline-block;max-width:4.5625rem;white-space:nowrap;overflow-x:auto;overflow-y:hidden;-webkit-overflow-scrolling:touch}.scroll-when-long::-webkit-scrollbar{height:2px}.scroll-when-long::-webkit-scrollbar-track{background:#eee;border-radius:.625rem}.scroll-when-long::-webkit-scrollbar-thumb{background:#888;border-radius:.625rem}.scroll-when-long::-webkit-scrollbar-thumb:hover{background:#555}.shape-text p{cursor:default;-webkit-user-select:none;user-select:none}.shape-text p:active{cursor:default}#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:fixed;top:25%;left:50%;transform:translate(-50%,-50%);background:transparent;color:#fff;padding:20px 30px;border-radius:10px;text-align:center;z-index:1000;display:flex;flex-direction:column;align-items:center;gap:8px}.loading-spinner{width:40px;height:40px;border:4px solid rgba(255,255,255,.3);border-top:4px solid #667eea;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.loading-indicator p{margin:0;font-size:16px;font-weight:500}.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}\n"] }]
2347
+ 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 profile-comparison\" *ngIf=\"backendConfigured\"\n [style.--edge-shading-color]=\"config.shadingColor || 'rgba(44, 40, 47, 0.0)'\"\n [style.--edge-shading-width]=\"'28px'\"\n [style.--edge-shading-display]=\"config.edgeShading !== false ? 'block' : 'none'\"\n [style.--edge-mask-left]=\"config.edgeShading !== false ? 'linear-gradient(to right, transparent 0%, rgba(0,0,0,0.4) 14px, black 20px)' : 'none'\"\n [style.--edge-mask-right]=\"config.edgeShading !== false ? 'linear-gradient(to left, transparent 0%, rgba(0,0,0,0.4) 14px, black 20px)' : 'none'\">\n <div class=\"backend-error-message\" *ngIf=\"backendError\">{{ backendError }}</div>\n\n <div class=\"profile-comparison__inner\" #shapeContainer>\n <!-- Backgrounds layer: structurally below everything else to fix stacking -->\n <div class=\"profile-comparison__bg-layer\">\n <div class=\"profile-comparison__frame profile-comparison__frame--left bg-frame\"\n [class.is-animating-nudge]=\"leftShapeAnimating\"\n #leftBgFrame>\n <div\n class=\"profile-comparison__bg profile-comparison__bg--left\"\n [ngStyle]=\"{ 'background-image': 'url(' + user1Image + ')', 'transform': user1Transform, 'object-position': user1ObjectPosition }\"\n ></div>\n </div>\n <div class=\"profile-comparison__frame profile-comparison__frame--right bg-frame\"\n [class.is-animating-nudge]=\"rightShapeAnimating\"\n #rightBgFrame>\n <div\n class=\"profile-comparison__bg profile-comparison__bg--right\"\n [ngStyle]=\"{ 'background-image': 'url(' + user2Image + ')', 'transform': user2Transform, 'object-position': user2ObjectPosition }\"\n ></div>\n </div>\n </div>\n\n <!-- Active interactive shapes and content layer -->\n <div class=\"profile-comparison__frame profile-comparison__frame--left\"\n [class.is-animating-nudge]=\"leftShapeAnimating\"\n (click)=\"onShapeClick('left')\"\n #leftFrame>\n <svg\n class=\"profile-comparison__frame-svg profile-comparison__frame-svg--glass\"\n width=\"256\"\n height=\"275\"\n viewBox=\"0 0 256 275\"\n preserveAspectRatio=\"none\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n aria-hidden=\"true\"\n >\n <g filter=\"url(#filter-glass-left)\">\n <path\n d=\"M246.177 26.4924 L0 0 M0 278 L246.68 239.869 C252.043 239.04 256 234.407 256 228.981 V37.4497 C256 31.8302 251.764 27.0937 246.177 26.4924\"\n stroke=\"#61C2AB\"\n stroke-opacity=\"0.3\"\n stroke-width=\"0.809707\"\n stroke-linejoin=\"round\"\n />\n </g>\n <defs>\n <filter id=\"filter-glass-left\" x=\"-20\" y=\"-20\" width=\"300\" height=\"320\" filterUnits=\"userSpaceOnUse\" color-interpolation-filters=\"sRGB\">\n <feFlood flood-opacity=\"0\" result=\"BackgroundImageFix\"/>\n <feColorMatrix in=\"SourceAlpha\" type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0\" result=\"hardAlpha\"/>\n <feOffset/>\n <feGaussianBlur stdDeviation=\"6.3562\"/>\n <feComposite in2=\"hardAlpha\" operator=\"out\"/>\n <feColorMatrix type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0\"/>\n <feBlend mode=\"normal\" in2=\"BackgroundImageFix\" result=\"effect1\"/>\n <feGaussianBlur in=\"effect1\" stdDeviation=\"6.3562\" result=\"effect2\"/>\n <feGaussianBlur in=\"effect2\" stdDeviation=\"6.3562\" result=\"effect3\"/>\n <feBlend mode=\"normal\" in=\"SourceGraphic\" in2=\"effect3\" result=\"shape\"/>\n </filter>\n </defs>\n </svg>\n <svg\n class=\"profile-comparison__frame-svg profile-comparison__frame-svg--middle\"\n width=\"256\"\n height=\"275\"\n viewBox=\"0 0 256 275\"\n preserveAspectRatio=\"none\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n aria-hidden=\"true\"\n >\n <g filter=\"url(#filter0_i_2126_3746)\">\n <path\n d=\"M245.538 25.2463 L-0.638672 -1.2459 V276.7542 L246.042 238.623 C251.404 237.794 255.361 233.161 255.361 227.735 V36.2037 C255.361 30.5841 251.126 25.8476 245.538 25.2463 Z\"\n fill=\"#00CFE6\"\n fill-opacity=\"0.07\"\n />\n </g>\n <defs>\n <filter id=\"filter0_i_2126_3746\" x=\"-0.638672\" y=\"-2\" width=\"256\" height=\"279\" filterUnits=\"userSpaceOnUse\" color-interpolation-filters=\"sRGB\">\n <feFlood flood-opacity=\"0\" result=\"BackgroundImageFix\" />\n <feBlend mode=\"normal\" in=\"SourceGraphic\" in2=\"BackgroundImageFix\" result=\"shape\" />\n <feColorMatrix in=\"SourceAlpha\" type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0\" result=\"hardAlpha\" />\n <feOffset />\n <feGaussianBlur stdDeviation=\"7.95\" />\n <feComposite in2=\"hardAlpha\" operator=\"arithmetic\" k2=\"-1\" k3=\"1\" />\n <feColorMatrix type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0.516667 0 0 0 0 1 0 0 0 1 0\" />\n <feBlend mode=\"normal\" in2=\"shape\" result=\"effect1_innerShadow_2126_3746\" />\n </filter>\n </defs>\n </svg>\n <svg\n class=\"profile-comparison__frame-svg profile-comparison__frame-svg--left\"\n viewBox=\"0 0 256 278\"\n preserveAspectRatio=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n aria-hidden=\"true\"\n >\n <defs>\n <radialGradient id=\"profile-left-fill\" cx=\"0\" cy=\"0\" r=\"1\" gradientTransform=\"matrix(65.1034 -81.3964 74.955 49.415 128 139)\" gradientUnits=\"userSpaceOnUse\">\n <stop stop-color=\"#0051E6\" stop-opacity=\"0\" />\n <stop offset=\"0.5\" stop-color=\"#0051E6\" stop-opacity=\"0.035\" />\n <stop offset=\"1\" stop-color=\"white\" />\n </radialGradient>\n <linearGradient id=\"profile-left-stroke\" x1=\"128\" y1=\"0\" x2=\"128\" y2=\"278\" gradientUnits=\"userSpaceOnUse\">\n <stop offset=\"0\" stop-color=\"#52FFEB\" />\n <stop offset=\"0.5\" stop-color=\"#41CFA1\" />\n <stop offset=\"1\" stop-color=\"#319990\" />\n </linearGradient>\n </defs>\n <!-- Fill Layer -->\n <path\n d=\"M246.177 26.4924 L0 0 V278 L246.68 239.869 C252.043 239.04 256 234.407 256 228.981 V37.4497 C256 31.8302 251.764 27.0937 246.177 26.4924 Z\"\n fill=\"url(#profile-left-fill)\"\n fill-opacity=\"0.07\"\n />\n <!-- Stroke Layer (Left outer border skipped) -->\n <path\n d=\"M246.177 26.4924 L0 0 M0 278 L246.68 239.869 C252.043 239.04 256 234.407 256 228.981 V37.4497 C256 31.8302 251.764 27.0937 246.177 26.4924\"\n fill=\"none\"\n stroke=\"url(#profile-left-stroke)\"\n stroke-width=\"2\"\n stroke-miterlimit=\"3.99393\"\n stroke-linejoin=\"round\"\n />\n <!-- Added explicit embedded SVG text anchoring tightly inside the graphical shape bounds instead of pure flexbox logic padding. -->\n <g\n style=\"cursor: pointer !important;\"\n (click)=\"onViewProfile('left'); $event.stopPropagation()\"\n (mousedown)=\"$event.stopPropagation()\"\n >\n <rect x=\"0\" y=\"240\" width=\"100\" height=\"40\" fill=\"transparent\" pointer-events=\"auto\" />\n <text\n x=\"15\" y=\"260\"\n fill=\"rgba(255, 255, 255, 0.60)\"\n font-family=\"'Calistoga', serif\"\n font-size=\"9\"\n text-anchor=\"start\"\n [class.is-animating-bob]=\"leftProfileClicked\"\n style=\"text-shadow: 0 2px 4px rgba(0,0,0,0.8);\"\n pointer-events=\"none\"\n >View Profile</text>\n </g>\n </svg>\n <div class=\"profile-comparison__content profile-comparison__content--left\">\n <div class=\"profile-comparison__list profile-comparison__list--left\">\n <ng-container\n *ngFor=\"\n let interest of alignedPerson1Interests.length > 0\n ? alignedPerson1Interests\n : displayPerson1Interests.length > 0\n ? displayPerson1Interests\n : person1Interests;\n let i = index\n \"\n >\n <div class=\"profile-comparison__item profile-comparison__item--left\">\n <lib-marquee\n class=\"profile-comparison__text\"\n *ngIf=\"!shouldShowDash(interest, i)\"\n [title]=\"interest\"\n [onlyMarqueeOnHover]=\"false\"\n style=\"--marquee-font-size: 15px; --marquee-font-family: 'Gilroy', sans-serif; --marquee-font-color: white; --marquee-font-weight: 100; --marquee-animation-duration: 8s;\"\n ></lib-marquee>\n <svg *ngIf=\"shouldShowDash(interest, i)\" class=\"profile-comparison__dash\" width=\"30\" height=\"4\" viewBox=\"0 0 30 4\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n <path d=\"M2 2H28\" stroke=\"white\" stroke-width=\"4\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n </svg>\n </div>\n </ng-container>\n </div>\n </div>\n </div>\n\n <div class=\"profile-comparison__frame profile-comparison__frame--right\"\n [class.is-animating-nudge]=\"rightShapeAnimating\"\n (click)=\"onShapeClick('right')\"\n #rightFrame>\n <svg\n class=\"profile-comparison__frame-svg profile-comparison__frame-svg--glass\"\n width=\"257\"\n height=\"275\"\n viewBox=\"0 0 257 275\"\n preserveAspectRatio=\"none\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n aria-hidden=\"true\"\n >\n <g filter=\"url(#filter-glass-right)\">\n <path\n d=\"M256.24 277.5 L243.564 275.546 L9.32411 239.437 C3.95944 238.61 0 233.976 0 228.548 V37.4032 C0 31.7824 4.2375 27.0452 9.8262 26.4454 L244.066 1.30651 L256.24 0\"\n stroke=\"#AB4AFF\"\n stroke-opacity=\"0.3\"\n stroke-width=\"0.809707\"\n stroke-linejoin=\"round\"\n />\n </g>\n <defs>\n <filter id=\"filter-glass-right\" x=\"-20\" y=\"-20\" width=\"300\" height=\"320\" filterUnits=\"userSpaceOnUse\" color-interpolation-filters=\"sRGB\">\n <feFlood flood-opacity=\"0\" result=\"BackgroundImageFix\"/>\n <feColorMatrix in=\"SourceAlpha\" type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0\" result=\"hardAlpha\"/>\n <feOffset/>\n <feGaussianBlur stdDeviation=\"6.3562\"/>\n <feComposite in2=\"hardAlpha\" operator=\"out\"/>\n <feColorMatrix type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0\"/>\n <feBlend mode=\"normal\" in2=\"BackgroundImageFix\" result=\"effect1\"/>\n <feGaussianBlur in=\"effect1\" stdDeviation=\"6.3562\" result=\"effect2\"/>\n <feGaussianBlur in=\"effect2\" stdDeviation=\"6.3562\" result=\"effect3\"/>\n <feBlend mode=\"normal\" in=\"SourceGraphic\" in2=\"effect3\" result=\"shape\"/>\n </filter>\n </defs>\n </svg>\n <svg\n class=\"profile-comparison__frame-svg profile-comparison__frame-svg--middle\"\n width=\"257\"\n height=\"275\"\n viewBox=\"0 0 257 275\"\n preserveAspectRatio=\"none\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n aria-hidden=\"true\"\n >\n <g filter=\"url(#filter0_i_2126_3750)\">\n <path\n d=\"M9.82611 25.203 L244.066 0.0640762 L256.24 -1.2424 V276.258 L243.564 274.304 L9.32402 238.195 C3.95935 237.368 -9.15527e-05 232.733 -9.15527e-05 227.305 V36.1607 C-9.15527e-05 30.5399 4.23741 25.8028 9.82611 25.203 Z\"\n fill=\"#7B00E6\"\n fill-opacity=\"0.07\"\n />\n </g>\n <defs>\n <filter id=\"filter0_i_2126_3750\" x=\"0\" y=\"-2\" width=\"256.24\" height=\"279\" filterUnits=\"userSpaceOnUse\" color-interpolation-filters=\"sRGB\">\n <feFlood flood-opacity=\"0\" result=\"BackgroundImageFix\" />\n <feBlend mode=\"normal\" in=\"SourceGraphic\" in2=\"BackgroundImageFix\" result=\"shape\" />\n <feColorMatrix in=\"SourceAlpha\" type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0\" result=\"hardAlpha\" />\n <feOffset />\n <feGaussianBlur stdDeviation=\"7.95\" />\n <feComposite in2=\"hardAlpha\" operator=\"arithmetic\" k2=\"-1\" k3=\"1\" />\n <feColorMatrix type=\"matrix\" values=\"0 0 0 0 0.482353 0 0 0 0 0 0 0 0 0 0.901961 0 0 0 1 0\" />\n <feBlend mode=\"normal\" in2=\"shape\" result=\"effect1_innerShadow_2126_3750\" />\n </filter>\n </defs>\n </svg>\n <svg\n class=\"profile-comparison__frame-svg profile-comparison__frame-svg--right\"\n viewBox=\"0 0 257 278\"\n preserveAspectRatio=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n aria-hidden=\"true\"\n >\n <defs>\n <radialGradient id=\"profile-right-fill\" cx=\"0\" cy=\"0\" r=\"1\" gradientTransform=\"matrix(-65.1644 -81.25 -75.0252 49.3261 128.12 138.75)\" gradientUnits=\"userSpaceOnUse\">\n <stop stop-color=\"#0051E6\" stop-opacity=\"0\" />\n <stop offset=\"0.5\" stop-color=\"#0051E6\" stop-opacity=\"0.035\" />\n <stop offset=\"1\" stop-color=\"white\" />\n </radialGradient>\n <linearGradient id=\"profile-right-stroke\" x1=\"128.12\" y1=\"277.5\" x2=\"128.12\" y2=\"8.50002\" gradientUnits=\"userSpaceOnUse\">\n <stop offset=\"0\" stop-color=\"#662C99\" />\n <stop offset=\"0.5\" stop-color=\"#893BD3\" />\n <stop offset=\"1\" stop-color=\"#AB4AFF\" />\n </linearGradient>\n </defs>\n <!-- Fill Layer -->\n <path\n d=\"M9.8262 26.4454 L244.066 1.30651 L256.24 0 V277.5 L243.564 275.546 L9.32411 239.437 C3.95944 238.61 0 233.976 0 228.548 V37.4032 C0 31.7824 4.2375 27.0452 9.8262 26.4454 Z\"\n fill=\"url(#profile-right-fill)\"\n fill-opacity=\"0.07\"\n />\n <!-- Stroke Layer (Right outer border skipped) -->\n <path\n d=\"M256.24 277.5 L243.564 275.546 L9.32411 239.437 C3.95944 238.61 0 233.976 0 228.548 V37.4032 C0 31.7824 4.2375 27.0452 9.8262 26.4454 L244.066 1.30651 L256.24 0\"\n fill=\"none\"\n stroke=\"url(#profile-right-stroke)\"\n stroke-width=\"2\"\n stroke-miterlimit=\"3.99393\"\n stroke-linejoin=\"round\"\n />\n <g\n style=\"cursor: pointer !important;\"\n (click)=\"onViewProfile('right'); $event.stopPropagation()\"\n (mousedown)=\"$event.stopPropagation()\"\n >\n <rect x=\"156\" y=\"240\" width=\"100\" height=\"40\" fill=\"transparent\" pointer-events=\"auto\" />\n <text\n x=\"241\" y=\"260\"\n fill=\"rgba(255, 255, 255, 0.60)\"\n font-family=\"'Calistoga', serif\"\n font-size=\"9\"\n text-anchor=\"end\"\n [class.is-animating-bob]=\"rightProfileClicked\"\n style=\"text-shadow: 0 2px 4px rgba(0,0,0,0.8);\"\n pointer-events=\"auto\"\n >View Profile</text>\n </g>\n </svg>\n <div class=\"profile-comparison__content profile-comparison__content--right\">\n <div class=\"profile-comparison__list profile-comparison__list--right\">\n <ng-container\n *ngFor=\"\n let interest of alignedPerson2Interests.length > 0\n ? alignedPerson2Interests\n : displayPerson2Interests.length > 0\n ? displayPerson2Interests\n : person2Interests;\n let i = index\n \"\n >\n <div class=\"profile-comparison__item profile-comparison__item--right\">\n <lib-marquee\n class=\"profile-comparison__text\"\n *ngIf=\"!shouldShowDash(interest, i)\"\n [title]=\"interest\"\n [onlyMarqueeOnHover]=\"false\"\n style=\"--marquee-font-size: 15px; --marquee-font-family: 'Gilroy', sans-serif; --marquee-font-color: white; --marquee-font-weight: 100; --marquee-animation-duration: 8s;\"\n ></lib-marquee>\n <svg *ngIf=\"shouldShowDash(interest, i)\" class=\"profile-comparison__dash\" width=\"30\" height=\"4\" viewBox=\"0 0 30 4\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n <path d=\"M2 2H28\" stroke=\"white\" stroke-width=\"4\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n </svg>\n </div>\n </ng-container>\n </div>\n </div>\n </div>\n\n <!-- Center text section -->\n <div class=\"profile-comparison__center\" #shapeTextCenter>\n <div class=\"profile-comparison__center-inner\">\n <ng-container *ngFor=\"let item of centerItem; let idx = index\">\n <div class=\"profile-comparison__center-item\">\n <!-- Show the label for the first occurrence -->\n <lib-marquee\n *ngIf=\"!isPlaceholder(item) && !isCenterRedundant(idx)\"\n class=\"profile-comparison__center-text\"\n [title]=\"item\"\n [onlyMarqueeOnHover]=\"false\"\n style=\"--marquee-font-size: 15px; --marquee-font-family: 'Gilroy', sans-serif; --marquee-font-color: white; --marquee-font-weight: 700; --marquee-animation-duration: 8s;\"\n ></lib-marquee>\n\n <!-- Show a dash for redundant/collapsed categories -->\n <svg *ngIf=\"isCenterRedundant(idx)\" class=\"profile-comparison__dash\" width=\"30\" height=\"4\" viewBox=\"0 0 30 4\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n <path d=\"M2 2H28\" stroke=\"white\" stroke-width=\"4\" stroke-opacity=\"0.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n </svg>\n\n <!-- Spacers/Placeholders result in empty divs (no dash) by default -->\n </div>\n </ng-container>\n </div>\n </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: ["@keyframes textDropIn{0%{opacity:0;transform:translateY(-8px)}to{opacity:1;transform:translateY(0)}}@keyframes nudgeLeft{0%,to{transform:translate(-24%) scale(.86)}50%{transform:translate(-26%) scale(.86)}}@keyframes nudgeRight{0%,to{transform:translate(24%) scale(.86)}50%{transform:translate(26%) scale(.86)}}.profile-img.left{-webkit-mask-image:linear-gradient(to right,rgb(0,0,0) 0%,rgb(0,0,0) calc(100% - var(--overlap-size, 40px)),rgba(0,0,0,0) 100%);-webkit-mask-size:100% 100%;-webkit-mask-repeat:no-repeat;mask-image:linear-gradient(to right,#fff 0% calc(100% - var(--overlap-size, 40px)),#fff0);mask-size:100% 100%;mask-repeat:no-repeat;margin-right:calc(-.5 * var(--overlap-size, 40px))}.profile-img.right{-webkit-mask-image:linear-gradient(to left,rgb(0,0,0) 0%,rgb(0,0,0) calc(100% - var(--overlap-size, 40px)),rgba(0,0,0,0) 100%);-webkit-mask-size:100% 100%;-webkit-mask-repeat:no-repeat;mask-image:linear-gradient(to left,#fff 0% calc(100% - var(--overlap-size, 40px)),#fff0);mask-size:100% 100%;mask-repeat:no-repeat;margin-left:calc(-.5 * var(--overlap-size, 40px))}.profile-img.fade-all{-webkit-mask-image:linear-gradient(to bottom,transparent 0%,black 10%,black 90%,transparent 100%),linear-gradient(to right,transparent,black 15%,black 85%,transparent);-webkit-mask-composite:source-in;mask-image:linear-gradient(to bottom,transparent 0%,black 10%,black 90%,transparent 100%),linear-gradient(to right,transparent,black 15%,black 85%,transparent);mask-composite:intersect}.profile-img.fade-all.left{-webkit-mask-image:linear-gradient(to bottom,transparent 0%,black 10%,black 90%,transparent 100%),linear-gradient(to right,transparent 0%,black 15%,black calc(100% - var(--overlap-size, 40px)),transparent 100%);mask-image:linear-gradient(to bottom,transparent 0%,black 10%,black 90%,transparent 100%),linear-gradient(to right,transparent 0%,black 15%,black calc(100% - var(--overlap-size, 40px)),transparent 100%)}.profile-img.fade-all.right{-webkit-mask-image:linear-gradient(to bottom,transparent 0%,black 10%,black 90%,transparent 100%),linear-gradient(to left,transparent 0%,black 15%,black calc(100% - var(--overlap-size, 40px)),transparent 100%);mask-image:linear-gradient(to bottom,transparent 0%,black 10%,black 90%,transparent 100%),linear-gradient(to left,transparent 0%,black 15%,black calc(100% - var(--overlap-size, 40px)),transparent 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%;min-width:300px;max-width:600px;margin:0 auto;overflow:visible;position:relative}.profile-flex{display:flex;width:100%;height:414px;align-items:center;overflow:hidden;background:linear-gradient(135deg,#1a1a1a,#0a0a0a);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 15%,black 85%,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 15%,black 85%,transparent 100%),linear-gradient(to bottom,transparent 0%,black 15%,black 85%,transparent 100%);mask-composite:intersect}.shape-bg{position:relative;pointer-events:auto;width:100%;height:100%}.shape-bg1,.shape-bg2{position:absolute;width:75%;max-width:none;height:350px;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;overflow:visible!important}.shape-bg svg:active{cursor:grabbing}.profile-img{width:50%;height:350px;position:relative;flex-shrink:0;background:linear-gradient(135deg,#1a1a1a,#0a0a0a);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 30%;opacity:.75;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}.shape{position:absolute;top:32px;width:100%;left:0;z-index:2;pointer-events:none}.shape-bg{position:relative;pointer-events:auto}.shape-text{position:absolute;top:0;left:0;right:0;width:100%;height:100%;z-index:1;padding:0 1.25rem;box-sizing:border-box;pointer-events:none}.shape-text-left{margin-top:40px;text-align:left;position:absolute;top:0;bottom:0;right:calc(50% + 75px);z-index:2;pointer-events:auto;display:flex;flex-direction:column;align-items:flex-end}.shape-text-left .scroll-container{width:100%}.shape-text-center{position:absolute;left:0;right:0;margin:0 auto;width:fit-content;font-family:Gilroy,sans-serif;font-weight:700;font-size:.75rem;line-height:100%;letter-spacing:0%;color:#fff;text-align:center;display:flex;justify-content:center;align-items:center;flex-direction:column;text-shadow:2px 2px 4px rgba(0,0,0,.3);z-index:10;transition:transform .3s ease;pointer-events:none;top:50%;transform:translateY(-50%);padding:0 .625rem;margin-top:3.8125rem}.shape-text-right{margin-top:40px;text-align:right;position:absolute;top:0;bottom:0;left:calc(50% + 70px);z-index:2;pointer-events:auto;display:flex;flex-direction:column;align-items:flex-start}.shape-text-right .scroll-container{width:100%}.shape-p{font-family:Gilroy,sans-serif;font-weight:400;font-size:.75rem;line-height:100%;letter-spacing:0%;color:#fff;padding-bottom:.125rem;text-align:right;text-shadow:2px 2px 4px rgba(0,0,0,.3);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;direction:rtl;-webkit-user-select:none;user-select:none;margin:0}.shape-p-center{font-family:Gilroy,sans-serif;font-weight:700;font-size:.75rem;line-height:100%;letter-spacing:0%;color:#fff;margin:0;text-align:center;display:flex;justify-content:center;align-items:center;text-shadow:2px 2px 4px rgba(0,0,0,.3)}.shape-p-right{font-family:Gilroy,sans-serif;font-weight:400;font-size:.75rem;line-height:100%;letter-spacing:0%;color:#fff;padding-bottom:.125rem;text-align:left;text-shadow:2px 2px 4px rgba(0,0,0,.3);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin:0;-webkit-user-select:none;user-select:none}.dash-item{color:#ffffff4d!important;font-size:.625rem!important;height:.625rem}.spacer-item{height:auto!important;color:#fffc!important;font-weight:700;font-size:.75rem!important;letter-spacing:2px}.shape-h2{font-family:Calistoga,serif;font-weight:400;font-size:.625rem;letter-spacing:0%;color:#fff;text-shadow:2px 2px 4px rgba(0,0,0,.3);cursor:pointer;transition:all .3s ease;border:none;outline:none;white-space:nowrap;pointer-events:auto;z-index:100;position:relative;margin-left:.5rem}.shape-h2:hover{opacity:.8;transform:translateY(-2px)}.shape-h2-right{font-family:Calistoga,serif;font-weight:400;font-size:.625rem;letter-spacing:0%;color:#fff;text-shadow:2px 2px 4px rgba(0,0,0,.3);cursor:pointer;transition:all .3s ease;border:none;outline:none;white-space:nowrap;pointer-events:auto;z-index:100;position:relative;margin-right:3.75rem}.shape-h2-right:hover{opacity:.8;transform:translateY(-2px)}.scroll-when-long{display:inline-block;max-width:4.5625rem;white-space:nowrap;overflow-x:auto;overflow-y:hidden;-webkit-overflow-scrolling:touch}.scroll-when-long::-webkit-scrollbar{height:2px}.scroll-when-long::-webkit-scrollbar-track{background:#eee;border-radius:.625rem}.scroll-when-long::-webkit-scrollbar-thumb{background:#888;border-radius:.625rem}.scroll-when-long::-webkit-scrollbar-thumb:hover{background:#555}.shape-text p{cursor:default;-webkit-user-select:none;user-select:none}.shape-text p:active{cursor:default}:host{display:block;background-color:#2c282f}.profile-comparison{width:100%;padding:0;box-sizing:border-box;display:flex;justify-content:center;align-items:center;min-height:100vh;background-color:#2c282f}.profile-comparison__inner{position:relative;width:100%;max-width:360px;display:grid;grid-template-columns:1fr;grid-template-rows:auto;background:transparent}.profile-comparison__bg-layer{grid-area:1/1;position:relative;width:100%;height:100%;pointer-events:none;display:grid;grid-template-columns:1fr;grid-template-rows:1fr}.profile-comparison__frame{grid-area:1/1;position:relative;pointer-events:none}.profile-comparison__frame.bg-frame{width:100%;height:100%;overflow:hidden;-webkit-mask-composite:source-in;mask-composite:intersect}.profile-comparison__frame.bg-frame.profile-comparison__frame--left{clip-path:polygon(0% 0%,96.2% 9.5%,100% 13.5%,100% 82.4%,96.4% 86.3%,0% 100%);-webkit-mask-image:var(--v-mask),linear-gradient(to right,black 40%,transparent 100%);mask-image:var(--v-mask),linear-gradient(to right,black 40%,transparent 100%)}.profile-comparison__frame.bg-frame.profile-comparison__frame--right{clip-path:polygon(100% 0%,100% 100%,94.7% 99.3%,3.6% 86.3%,0% 82.4%,0% 13.5%,3.8% 9.5%,95.3% .5%);-webkit-mask-image:var(--v-mask),linear-gradient(to left,black 40%,transparent 100%);mask-image:var(--v-mask),linear-gradient(to left,black 40%,transparent 100%)}.profile-comparison__frame--left{transform:translate(-24%) scale(.86)}.profile-comparison__frame--left.is-animating-nudge{animation:nudgeLeft .5s ease-out}.profile-comparison__frame--left .profile-comparison__frame-svg{-webkit-mask-image:var(--edge-mask-left, linear-gradient(to right, transparent 0%, rgba(0, 0, 0, .4) 14px, black var(--edge-shading-width, 28px)));mask-image:var(--edge-mask-left, linear-gradient(to right, transparent 0%, rgba(0, 0, 0, .4) 14px, black var(--edge-shading-width, 28px)))}.profile-comparison__frame--left:after{content:\"\";position:absolute;inset:0;pointer-events:none;background:linear-gradient(to right,var(--edge-shading-color, rgba(44, 40, 47, .7)) 0%,rgba(44,40,47,.3) 14px,transparent var(--edge-shading-width, 28px));z-index:5;display:var(--edge-shading-display, block)}.profile-comparison__frame--left .profile-comparison__frame-svg--glass{transform:scale(1.02) translate(1px);transform-origin:center}.profile-comparison__frame--right{transform:translate(24%) scale(.86)}.profile-comparison__frame--right.is-animating-nudge{animation:nudgeRight .5s ease-out}.profile-comparison__frame--right .profile-comparison__frame-svg{-webkit-mask-image:var(--edge-mask-right, linear-gradient(to left, transparent 0%, rgba(0, 0, 0, .4) 14px, black var(--edge-shading-width, 28px)));mask-image:var(--edge-mask-right, linear-gradient(to left, transparent, black var(--edge-shading-width, 28px)))}.profile-comparison__frame--right:after{content:\"\";position:absolute;inset:0;pointer-events:none;background:linear-gradient(to left,var(--edge-shading-color, rgba(44, 40, 47, .7)) 0%,rgba(44,40,47,.3) 14px,transparent var(--edge-shading-width, 28px));z-index:5;display:var(--edge-shading-display, block)}.profile-comparison__frame--right .profile-comparison__frame-svg--glass{transform:scale(1.02) translate(-1px);transform-origin:center}.profile-comparison__frame--right .profile-comparison__frame-svg--bottom{width:113.2%}.profile-comparison__frame-svg{position:absolute;inset:0;width:100%;height:100%;pointer-events:auto;overflow:visible}.profile-comparison__frame-svg--glass{z-index:1}.profile-comparison__frame-svg--middle{z-index:2}.profile-comparison__frame-svg--left,.profile-comparison__frame-svg--right{z-index:3}.profile-comparison__frame-svg--bottom{width:113.7%;height:112.6%;inset:51% auto auto 50%;transform:translate(-50%,-51%)}.profile-comparison__bg{position:absolute;inset:0;background-size:360px auto;background-repeat:no-repeat;filter:blur(2px);pointer-events:auto;transition:filter .6s ease;--vignette-mask: linear-gradient(to bottom, transparent, black 10%, black 90%, transparent), linear-gradient(to right, transparent, black 10%, black 90%, transparent)}.profile-comparison__bg--left{background-position:left center;margin-right:20%;-webkit-mask-image:var(--vignette-mask),linear-gradient(to right,black 50%,transparent 100%);mask-image:var(--vignette-mask),linear-gradient(to right,black 50%,transparent 100%);-webkit-mask-composite:source-in;mask-composite:intersect}.profile-comparison__bg--right{background-position:right center;margin-left:20%;-webkit-mask-image:var(--vignette-mask),linear-gradient(to left,black 50%,transparent 100%);mask-image:var(--vignette-mask),linear-gradient(to left,black 50%,transparent 100%);-webkit-mask-composite:source-in;mask-composite:intersect}@keyframes viewProfileBob{0%{transform:translateY(0)}50%{transform:translateY(-3px)}to{transform:translateY(0)}}.profile-comparison svg text{transition:opacity .3s ease}.profile-comparison svg text.is-animating-bob{animation:viewProfileBob .6s ease-in-out;fill:#fff!important}.profile-comparison__content{position:relative;z-index:20;padding:50px 12% 75px;min-height:100%;display:flex;flex-direction:column;justify-content:flex-start;color:#ecf0f1;font-family:Gilroy,serif;pointer-events:none}.profile-comparison__content--left{align-items:flex-end;text-align:right;padding-right:30%}.profile-comparison__content--right{align-items:flex-start;text-align:left;padding-left:30%}.profile-comparison__list{display:flex;flex-direction:column;gap:10px;width:100%;pointer-events:auto}.profile-comparison__list--left{align-items:flex-end;text-align:right;padding-right:30%}.profile-comparison__list--right{align-items:flex-start;text-align:left;padding-left:30%}.profile-comparison__item{display:flex;align-items:center;height:22px;width:170px;flex-shrink:0;animation:textDropIn .4s ease-out backwards}.profile-comparison__item--left{justify-content:flex-end}.profile-comparison__item--left .profile-comparison__dash{margin-right:12px;flex-shrink:0}.profile-comparison__item--right{justify-content:flex-start}.profile-comparison__item--right .profile-comparison__dash{margin-left:12px;flex-shrink:0}.profile-comparison__text{font-family:Gilroy,sans-serif;font-size:15px;font-weight:100!important;color:#fff;width:100%;display:block}.profile-comparison__text ::ng-deep *{font-weight:400!important}.profile-comparison__dash{display:block}.profile-comparison__item-text{display:none}.profile-comparison__center{grid-area:1/1;display:flex;align-items:flex-start;justify-content:center;z-index:15;pointer-events:none;transform:scale(.86)}.profile-comparison__center-inner{display:flex;flex-direction:column;align-items:center;text-align:center;justify-content:flex-start;padding:50px 0 75px;gap:10px;width:100%}.profile-comparison__center-item{height:22px;display:flex;align-items:center;justify-content:center;width:140px;flex-shrink:0;animation:textDropIn .4s ease-out backwards}.profile-comparison__center-text{font-family:Gilroy,sans-serif;font-size:15px;font-weight:700;color:#fff;width:100%;display:block}.profile-comparison__center-text--empty{color:#ecf0f180}@media(max-width:480px){.profile-comparison{padding:12px}.profile-comparison__inner{max-width:300px}.profile-comparison__name{font-size:11px}.profile-comparison__list{font-size:9px}.profile-comparison__center-text{font-size:10px}}.loading-indicator{position:fixed;top:25%;left:50%;transform:translate(-50%,-50%);background:transparent;color:#fff;padding:20px 30px;border-radius:10px;text-align:center;z-index:1000;display:flex;flex-direction:column;align-items:center;gap:8px}.loading-spinner{width:40px;height:40px;border:4px solid rgba(255,255,255,.3);border-top:4px solid #667eea;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.loading-indicator p{margin:0;font-size:16px;font-weight:500}.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}\n"] }]
3026
2348
  }], ctorParameters: () => [{ type: ProfileComparisonBackendService }, { type: i0.Renderer2 }, { type: FileConversionService }, { type: ImageCompressionService }, { type: i0.ChangeDetectorRef }, { type: undefined, decorators: [{
3027
2349
  type: Optional
3028
2350
  }, {
@@ -3030,6 +2352,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
3030
2352
  args: [PROFILE_COMPARISON_VERBOSE_LOGGING]
3031
2353
  }] }], propDecorators: { config: [{
3032
2354
  type: Input
2355
+ }], backendMode: [{
2356
+ type: Input
2357
+ }], backendUrl: [{
2358
+ type: Input
3033
2359
  }], fadeAllEdges: [{
3034
2360
  type: Input
3035
2361
  }], matrixDataChange: [{
@@ -3044,54 +2370,42 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
3044
2370
  }], rightContainer: [{
3045
2371
  type: ViewChild,
3046
2372
  args: ['rightContainer']
3047
- }], profileFlex: [{
2373
+ }], leftFrame: [{
3048
2374
  type: ViewChild,
3049
- args: ['profileFlex']
3050
- }], profileImgLeft: [{
2375
+ args: ['leftFrame']
2376
+ }], rightFrame: [{
3051
2377
  type: ViewChild,
3052
- args: ['profileImgLeft']
3053
- }], profileImgRight: [{
2378
+ args: ['rightFrame']
2379
+ }], leftBgFrame: [{
3054
2380
  type: ViewChild,
3055
- args: ['profileImgRight']
2381
+ args: ['leftBgFrame']
2382
+ }], rightBgFrame: [{
2383
+ type: ViewChild,
2384
+ args: ['rightBgFrame']
3056
2385
  }], shapeContainer: [{
3057
2386
  type: ViewChild,
3058
2387
  args: ['shapeContainer']
3059
- }], shapeBg: [{
3060
- type: ViewChild,
3061
- args: ['shapeBg']
3062
- }], shapeBg1: [{
3063
- type: ViewChild,
3064
- args: ['shapeBg1']
3065
- }], shapeBg2: [{
3066
- type: ViewChild,
3067
- args: ['shapeBg2']
3068
- }], shapeTextLeft: [{
3069
- type: ViewChild,
3070
- args: ['shapeTextLeft']
3071
- }], shapeTextRight: [{
3072
- type: ViewChild,
3073
- args: ['shapeTextRight']
3074
2388
  }], shapeTextCenter: [{
3075
2389
  type: ViewChild,
3076
2390
  args: ['shapeTextCenter']
3077
2391
  }] } });
3078
2392
 
3079
2393
  class ProfileComparisonLibModule {
3080
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: ProfileComparisonLibModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
3081
- static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "20.3.16", ngImport: i0, type: ProfileComparisonLibModule, declarations: [ProfileComparisonLibComponent], imports: [CommonModule,
2394
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: ProfileComparisonLibModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
2395
+ static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "20.3.19", ngImport: i0, type: ProfileComparisonLibModule, declarations: [ProfileComparisonLibComponent], imports: [CommonModule,
3082
2396
  HttpClientModule,
3083
2397
  FormsModule,
3084
- ReactiveFormsModule], exports: [ProfileComparisonLibComponent] });
3085
- static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: ProfileComparisonLibModule, providers: [
3086
- ProfileComparisonBackendService,
3087
- EmbeddingService,
3088
- OpenAIEmbeddingService
2398
+ ReactiveFormsModule,
2399
+ LibMarqueeModule], exports: [ProfileComparisonLibComponent] });
2400
+ static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: ProfileComparisonLibModule, providers: [
2401
+ ProfileComparisonBackendService
3089
2402
  ], imports: [CommonModule,
3090
2403
  HttpClientModule,
3091
2404
  FormsModule,
3092
- ReactiveFormsModule] });
2405
+ ReactiveFormsModule,
2406
+ LibMarqueeModule] });
3093
2407
  }
3094
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: ProfileComparisonLibModule, decorators: [{
2408
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: ProfileComparisonLibModule, decorators: [{
3095
2409
  type: NgModule,
3096
2410
  args: [{
3097
2411
  declarations: [
@@ -3101,15 +2415,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
3101
2415
  CommonModule,
3102
2416
  HttpClientModule,
3103
2417
  FormsModule,
3104
- ReactiveFormsModule
2418
+ ReactiveFormsModule,
2419
+ LibMarqueeModule
3105
2420
  ],
3106
2421
  exports: [
3107
2422
  ProfileComparisonLibComponent
3108
2423
  ],
3109
2424
  providers: [
3110
- ProfileComparisonBackendService,
3111
- EmbeddingService,
3112
- OpenAIEmbeddingService
2425
+ ProfileComparisonBackendService
3113
2426
  ]
3114
2427
  }]
3115
2428
  }] });
@@ -3122,5 +2435,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
3122
2435
  * Generated bundle index. Do not edit.
3123
2436
  */
3124
2437
 
3125
- export { CachePersistenceService, EmbeddingService, FileConversionService, ImageCompressionService, OpenAIEmbeddingService, PROFILE_COMPARISON_API_BASE_URL, PROFILE_COMPARISON_VERBOSE_LOGGING, ProfileComparisonBackendService, ProfileComparisonLibComponent, ProfileComparisonLibModule, ProfileComparisonLibService, ProfileService };
2438
+ export { BACKEND_MODE_URLS, BackendMode, CachePersistenceService, FileConversionService, ImageCompressionService, OpenAIEmbeddingService, PROFILE_COMPARISON_API_BASE_URL, PROFILE_COMPARISON_VERBOSE_LOGGING, ProfileComparisonBackendService, ProfileComparisonLibComponent, ProfileComparisonLibModule, ProfileComparisonLibService, ProfileService, resolveBackendUrl };
3126
2439
  //# sourceMappingURL=naniteninja-profile-comparison-lib.mjs.map