@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.
- package/README.md +167 -272
- package/fesm2022/naniteninja-profile-comparison-lib.mjs +352 -1039
- package/fesm2022/naniteninja-profile-comparison-lib.mjs.map +1 -1
- package/index.d.ts +77 -75
- package/package.json +2 -4
|
@@ -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,
|
|
4
|
-
import { from,
|
|
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
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
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.
|
|
176
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.
|
|
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.
|
|
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
|
|
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.
|
|
999
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
1118
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.
|
|
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.
|
|
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.
|
|
1900
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.
|
|
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.
|
|
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.
|
|
1935
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.
|
|
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.
|
|
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
|
|
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.
|
|
2192
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.
|
|
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.
|
|
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(
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2229
|
-
|
|
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
|
-
|
|
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.
|
|
2246
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.
|
|
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.
|
|
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: () => [
|
|
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
|
-
|
|
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
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
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
|
-
|
|
2415
|
-
this.backendConfigured = !!
|
|
2416
|
-
this.log('ngOnInit', 'backendConfigured', this.backendConfigured, '
|
|
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
|
-
|
|
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
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
2760
|
-
const
|
|
2761
|
-
if (!
|
|
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
|
-
|
|
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
|
|
2780
|
-
const
|
|
2781
|
-
const
|
|
2782
|
-
const
|
|
2783
|
-
|
|
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
|
|
2801
|
-
|
|
2802
|
-
|
|
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 || !
|
|
2106
|
+
if (!isDragging || !activeFrame)
|
|
2807
2107
|
return;
|
|
2808
2108
|
const deltaX = e.clientX - dragStartX;
|
|
2809
|
-
const containerWidth =
|
|
2810
|
-
|
|
2811
|
-
let
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
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
|
|
2817
|
-
|
|
2818
|
-
|
|
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
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
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
|
-
|
|
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
|
|
2137
|
+
const resetDrag = () => {
|
|
2839
2138
|
isDragging = false;
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
if (
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
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',
|
|
2857
|
-
document.removeEventListener('mouseleave',
|
|
2157
|
+
document.removeEventListener('mouseup', resetDrag);
|
|
2158
|
+
document.removeEventListener('mouseleave', resetDrag);
|
|
2858
2159
|
setTimeout(() => {
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
if (
|
|
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
|
-
[
|
|
2869
|
-
|
|
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
|
-
|
|
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',
|
|
2878
|
-
document.addEventListener('mouseleave',
|
|
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
|
|
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
|
|
2215
|
+
this.user2Image = dataUrl; }), map(() => void 0)));
|
|
2894
2216
|
}
|
|
2895
|
-
return tasks.length > 0 ? forkJoin(tasks).pipe(map
|
|
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
|
|
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.
|
|
2983
|
-
const rightContainerEl = this.
|
|
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.
|
|
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.
|
|
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
|
-
}],
|
|
2373
|
+
}], leftFrame: [{
|
|
3048
2374
|
type: ViewChild,
|
|
3049
|
-
args: ['
|
|
3050
|
-
}],
|
|
2375
|
+
args: ['leftFrame']
|
|
2376
|
+
}], rightFrame: [{
|
|
3051
2377
|
type: ViewChild,
|
|
3052
|
-
args: ['
|
|
3053
|
-
}],
|
|
2378
|
+
args: ['rightFrame']
|
|
2379
|
+
}], leftBgFrame: [{
|
|
3054
2380
|
type: ViewChild,
|
|
3055
|
-
args: ['
|
|
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.
|
|
3081
|
-
static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "20.3.
|
|
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
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
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.
|
|
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 {
|
|
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
|