@naniteninja/profile-comparison-lib 0.0.1 → 1.0.0
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 +193 -314
- package/fesm2022/naniteninja-profile-comparison-lib.mjs +1061 -1256
- package/fesm2022/naniteninja-profile-comparison-lib.mjs.map +1 -1
- package/index.d.ts +168 -210
- package/package.json +3 -19
|
@@ -1,17 +1,184 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { Injectable, EventEmitter, ViewChild, Output, Input, Component, NgModule } from '@angular/core';
|
|
3
|
-
import { switchMap, map, catchError, shareReplay, tap } from 'rxjs/operators';
|
|
4
|
-
import { from, throwError, of, forkJoin, defer, switchMap as switchMap$1, map as map$1, Observable, retryWhen, concatMap, timer,
|
|
5
|
-
import * as i3 from '@angular/forms';
|
|
6
|
-
import { FormGroup, FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms';
|
|
2
|
+
import { InjectionToken, Injectable, Optional, Inject, EventEmitter, ViewChild, Output, Input, Component, NgModule } from '@angular/core';
|
|
3
|
+
import { switchMap, map, catchError, shareReplay, tap as tap$1 } from 'rxjs/operators';
|
|
4
|
+
import { from, throwError, of, forkJoin, defer, switchMap as switchMap$1, map as map$1, Observable, BehaviorSubject, tap, retryWhen, concatMap, timer, catchError as catchError$1 } from 'rxjs';
|
|
7
5
|
import * as use from '@tensorflow-models/universal-sentence-encoder';
|
|
8
6
|
import '@tensorflow/tfjs';
|
|
9
7
|
import * as i1 from '@angular/common/http';
|
|
10
8
|
import { HttpHeaders, HttpClientModule } from '@angular/common/http';
|
|
11
9
|
import * as i2 from '@angular/common';
|
|
12
10
|
import { CommonModule } from '@angular/common';
|
|
13
|
-
import
|
|
14
|
-
|
|
11
|
+
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* When provided, ProfileService and OpenAIEmbeddingService call this backend base URL
|
|
15
|
+
* (e.g. 'http://localhost:3000/api') for all profile/embedding endpoints instead of
|
|
16
|
+
* calling external APIs directly with keys. Can be a string or a getter function for runtime toggle.
|
|
17
|
+
* When not provided or getter returns null, the lib uses the direct path with keys.
|
|
18
|
+
*/
|
|
19
|
+
const PROFILE_COMPARISON_API_BASE_URL = new InjectionToken('PROFILE_COMPARISON_API_BASE_URL');
|
|
20
|
+
/** Optional getter: when true, lib and backend service log steps to console for diagnosis. */
|
|
21
|
+
const PROFILE_COMPARISON_VERBOSE_LOGGING = new InjectionToken('PROFILE_COMPARISON_VERBOSE_LOGGING');
|
|
22
|
+
|
|
23
|
+
const DB_NAME = 'profile-comparison-cache';
|
|
24
|
+
const DB_VERSION = 1;
|
|
25
|
+
const STORE_OPENAI_SPACER = 'openai-spacer';
|
|
26
|
+
const STORE_OPENAI_ALIGNMENT = 'openai-alignment';
|
|
27
|
+
const STORE_OPENAI_SIMILARITY = 'openai-similarity';
|
|
28
|
+
const STORE_OPENAI_EMBEDDINGS = 'openai-embeddings';
|
|
29
|
+
const STORE_PROFILE_FACE = 'profile-face';
|
|
30
|
+
const STORE_PROFILE_COMPARE = 'profile-compare';
|
|
31
|
+
const STORE_NAMES = [
|
|
32
|
+
STORE_OPENAI_SPACER,
|
|
33
|
+
STORE_OPENAI_ALIGNMENT,
|
|
34
|
+
STORE_OPENAI_SIMILARITY,
|
|
35
|
+
STORE_OPENAI_EMBEDDINGS,
|
|
36
|
+
STORE_PROFILE_FACE,
|
|
37
|
+
STORE_PROFILE_COMPARE
|
|
38
|
+
];
|
|
39
|
+
/**
|
|
40
|
+
* Persists cache entries to IndexedDB per device (mobile-friendly).
|
|
41
|
+
* No-op when IndexedDB is unavailable (e.g. SSR, private mode on some browsers).
|
|
42
|
+
*/
|
|
43
|
+
class CachePersistenceService {
|
|
44
|
+
db = null;
|
|
45
|
+
openPromise = null;
|
|
46
|
+
openDb() {
|
|
47
|
+
if (typeof indexedDB === 'undefined')
|
|
48
|
+
return Promise.resolve(null);
|
|
49
|
+
if (this.db)
|
|
50
|
+
return Promise.resolve(this.db);
|
|
51
|
+
if (this.openPromise)
|
|
52
|
+
return this.openPromise;
|
|
53
|
+
this.openPromise = new Promise((resolve) => {
|
|
54
|
+
try {
|
|
55
|
+
const req = indexedDB.open(DB_NAME, DB_VERSION);
|
|
56
|
+
req.onerror = () => resolve(null);
|
|
57
|
+
req.onsuccess = () => {
|
|
58
|
+
this.db = req.result;
|
|
59
|
+
resolve(this.db);
|
|
60
|
+
};
|
|
61
|
+
req.onupgradeneeded = (e) => {
|
|
62
|
+
const db = e.target.result;
|
|
63
|
+
STORE_NAMES.forEach((name) => {
|
|
64
|
+
if (!db.objectStoreNames.contains(name))
|
|
65
|
+
db.createObjectStore(name);
|
|
66
|
+
});
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
resolve(null);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
return this.openPromise;
|
|
74
|
+
}
|
|
75
|
+
getStore(storeName, mode = 'readonly') {
|
|
76
|
+
return this.openDb().then((db) => {
|
|
77
|
+
if (!db)
|
|
78
|
+
return null;
|
|
79
|
+
try {
|
|
80
|
+
return db.transaction(storeName, mode).objectStore(storeName);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
get(storeName, key) {
|
|
88
|
+
return this.openDb().then((db) => {
|
|
89
|
+
if (!db)
|
|
90
|
+
return undefined;
|
|
91
|
+
return new Promise((resolve) => {
|
|
92
|
+
try {
|
|
93
|
+
const tx = db.transaction(storeName, 'readonly');
|
|
94
|
+
const store = tx.objectStore(storeName);
|
|
95
|
+
const req = store.get(key);
|
|
96
|
+
req.onsuccess = () => resolve(req.result ?? undefined);
|
|
97
|
+
req.onerror = () => resolve(undefined);
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
resolve(undefined);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
set(storeName, key, value) {
|
|
106
|
+
return this.openDb().then((db) => {
|
|
107
|
+
if (!db)
|
|
108
|
+
return;
|
|
109
|
+
return new Promise((resolve) => {
|
|
110
|
+
try {
|
|
111
|
+
const tx = db.transaction(storeName, 'readwrite');
|
|
112
|
+
const store = tx.objectStore(storeName);
|
|
113
|
+
store.put(value, key);
|
|
114
|
+
tx.oncomplete = () => resolve();
|
|
115
|
+
tx.onerror = () => resolve();
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
resolve();
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
getAll(storeName) {
|
|
124
|
+
return this.openDb().then((db) => {
|
|
125
|
+
if (!db)
|
|
126
|
+
return [];
|
|
127
|
+
return new Promise((resolve) => {
|
|
128
|
+
try {
|
|
129
|
+
const tx = db.transaction(storeName, 'readonly');
|
|
130
|
+
const store = tx.objectStore(storeName);
|
|
131
|
+
const req = store.openCursor();
|
|
132
|
+
const entries = [];
|
|
133
|
+
req.onsuccess = () => {
|
|
134
|
+
const cursor = req.result;
|
|
135
|
+
if (cursor) {
|
|
136
|
+
entries.push([cursor.key, cursor.value]);
|
|
137
|
+
cursor.continue();
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
resolve(entries);
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
req.onerror = () => resolve([]);
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
resolve([]);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
clear(storeName) {
|
|
152
|
+
return this.openDb().then((db) => {
|
|
153
|
+
if (!db)
|
|
154
|
+
return;
|
|
155
|
+
const stores = storeName ? [storeName] : [...STORE_NAMES];
|
|
156
|
+
return Promise.all(stores.map((s) => new Promise((resolve) => {
|
|
157
|
+
try {
|
|
158
|
+
const tx = db.transaction(s, 'readwrite');
|
|
159
|
+
tx.objectStore(s).clear();
|
|
160
|
+
tx.oncomplete = () => resolve();
|
|
161
|
+
tx.onerror = () => resolve();
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
resolve();
|
|
165
|
+
}
|
|
166
|
+
}))).then(() => undefined);
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
static STORE_OPENAI_SPACER = STORE_OPENAI_SPACER;
|
|
170
|
+
static STORE_OPENAI_ALIGNMENT = STORE_OPENAI_ALIGNMENT;
|
|
171
|
+
static STORE_OPENAI_SIMILARITY = STORE_OPENAI_SIMILARITY;
|
|
172
|
+
static STORE_OPENAI_EMBEDDINGS = STORE_OPENAI_EMBEDDINGS;
|
|
173
|
+
static STORE_PROFILE_FACE = STORE_PROFILE_FACE;
|
|
174
|
+
static STORE_PROFILE_COMPARE = STORE_PROFILE_COMPARE;
|
|
175
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: CachePersistenceService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
176
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: CachePersistenceService, providedIn: 'root' });
|
|
177
|
+
}
|
|
178
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: CachePersistenceService, decorators: [{
|
|
179
|
+
type: Injectable,
|
|
180
|
+
args: [{ providedIn: 'root' }]
|
|
181
|
+
}] });
|
|
15
182
|
|
|
16
183
|
class EmbeddingService {
|
|
17
184
|
model$ = null;
|
|
@@ -964,6 +1131,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
|
|
|
964
1131
|
*/
|
|
965
1132
|
class OpenAIEmbeddingService {
|
|
966
1133
|
http;
|
|
1134
|
+
apiBaseUrl;
|
|
1135
|
+
persistence;
|
|
967
1136
|
static API_URL = 'https://api.openai.com/v1/chat/completions';
|
|
968
1137
|
static MODEL = 'gpt-4o';
|
|
969
1138
|
static HEADER_CONTENT_TYPE = 'Content-Type';
|
|
@@ -981,8 +1150,73 @@ class OpenAIEmbeddingService {
|
|
|
981
1150
|
alignmentCache = new Map();
|
|
982
1151
|
// Cache for new spacer-based alignment
|
|
983
1152
|
spacerAlignmentCache = new Map();
|
|
984
|
-
|
|
1153
|
+
// Cache for similarity (textA, textB) -> score
|
|
1154
|
+
similarityCache = new Map();
|
|
1155
|
+
// Cache for embeddings: JSON.stringify(texts) -> number[][]
|
|
1156
|
+
embeddingsCache = new Map();
|
|
1157
|
+
spacerLoaded = false;
|
|
1158
|
+
alignmentLoaded = false;
|
|
1159
|
+
similarityLoaded = false;
|
|
1160
|
+
embeddingsLoaded = false;
|
|
1161
|
+
spacerLoadPromise = null;
|
|
1162
|
+
alignmentLoadPromise = null;
|
|
1163
|
+
similarityLoadPromise = null;
|
|
1164
|
+
embeddingsLoadPromise = null;
|
|
1165
|
+
rawResponse$ = new BehaviorSubject('');
|
|
1166
|
+
constructor(http, apiBaseUrl, persistence) {
|
|
985
1167
|
this.http = http;
|
|
1168
|
+
this.apiBaseUrl = apiBaseUrl;
|
|
1169
|
+
this.persistence = persistence;
|
|
1170
|
+
}
|
|
1171
|
+
loadSpacerCache() {
|
|
1172
|
+
if (this.spacerLoaded)
|
|
1173
|
+
return Promise.resolve();
|
|
1174
|
+
if (!this.spacerLoadPromise) {
|
|
1175
|
+
this.spacerLoadPromise = this.persistence.getAll(CachePersistenceService.STORE_OPENAI_SPACER).then((entries) => {
|
|
1176
|
+
entries.forEach(([k, v]) => this.spacerAlignmentCache.set(k, v));
|
|
1177
|
+
this.spacerLoaded = true;
|
|
1178
|
+
});
|
|
1179
|
+
}
|
|
1180
|
+
return this.spacerLoadPromise;
|
|
1181
|
+
}
|
|
1182
|
+
loadAlignmentCache() {
|
|
1183
|
+
if (this.alignmentLoaded)
|
|
1184
|
+
return Promise.resolve();
|
|
1185
|
+
if (!this.alignmentLoadPromise) {
|
|
1186
|
+
this.alignmentLoadPromise = this.persistence.getAll(CachePersistenceService.STORE_OPENAI_ALIGNMENT).then((entries) => {
|
|
1187
|
+
entries.forEach(([k, v]) => this.alignmentCache.set(k, v));
|
|
1188
|
+
this.alignmentLoaded = true;
|
|
1189
|
+
});
|
|
1190
|
+
}
|
|
1191
|
+
return this.alignmentLoadPromise;
|
|
1192
|
+
}
|
|
1193
|
+
loadSimilarityCache() {
|
|
1194
|
+
if (this.similarityLoaded)
|
|
1195
|
+
return Promise.resolve();
|
|
1196
|
+
if (!this.similarityLoadPromise) {
|
|
1197
|
+
this.similarityLoadPromise = this.persistence.getAll(CachePersistenceService.STORE_OPENAI_SIMILARITY).then((entries) => {
|
|
1198
|
+
entries.forEach(([k, v]) => this.similarityCache.set(k, v));
|
|
1199
|
+
this.similarityLoaded = true;
|
|
1200
|
+
});
|
|
1201
|
+
}
|
|
1202
|
+
return this.similarityLoadPromise;
|
|
1203
|
+
}
|
|
1204
|
+
loadEmbeddingsCache() {
|
|
1205
|
+
if (this.embeddingsLoaded)
|
|
1206
|
+
return Promise.resolve();
|
|
1207
|
+
if (!this.embeddingsLoadPromise) {
|
|
1208
|
+
this.embeddingsLoadPromise = this.persistence.getAll(CachePersistenceService.STORE_OPENAI_EMBEDDINGS).then((entries) => {
|
|
1209
|
+
entries.forEach(([k, v]) => this.embeddingsCache.set(k, v));
|
|
1210
|
+
this.embeddingsLoaded = true;
|
|
1211
|
+
});
|
|
1212
|
+
}
|
|
1213
|
+
return this.embeddingsLoadPromise;
|
|
1214
|
+
}
|
|
1215
|
+
getBaseUrl() {
|
|
1216
|
+
const u = this.apiBaseUrl;
|
|
1217
|
+
if (u == null)
|
|
1218
|
+
return null;
|
|
1219
|
+
return typeof u === 'function' ? u() : u;
|
|
986
1220
|
}
|
|
987
1221
|
/**
|
|
988
1222
|
* Get aligned lists with spacers.
|
|
@@ -993,127 +1227,130 @@ class OpenAIEmbeddingService {
|
|
|
993
1227
|
const validB = listB.filter(i => i && i.trim() && i.trim() !== OpenAIEmbeddingService.SPACER_SMALL);
|
|
994
1228
|
if (validA.length === 0 && validB.length === 0)
|
|
995
1229
|
return of({ listA: [], listB: [] });
|
|
996
|
-
// Cache key
|
|
997
1230
|
const key = JSON.stringify({ a: validA, b: validB, v: 'spacer-v35' });
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1231
|
+
return from(this.loadSpacerCache()).pipe(switchMap(() => {
|
|
1232
|
+
if (this.spacerAlignmentCache.has(key)) {
|
|
1233
|
+
console.log('Alignment cache hit');
|
|
1234
|
+
return of(this.spacerAlignmentCache.get(key));
|
|
1235
|
+
}
|
|
1236
|
+
const base = this.getBaseUrl();
|
|
1237
|
+
if (base) {
|
|
1238
|
+
const url = `${base.replace(/\/$/, '')}/profile/align-lists`;
|
|
1239
|
+
return this.http
|
|
1240
|
+
.post(url, { listA: validA, listB: validB }, {
|
|
1241
|
+
headers: new HttpHeaders({ 'Content-Type': 'application/json' }),
|
|
1242
|
+
})
|
|
1243
|
+
.pipe(map((result) => {
|
|
1244
|
+
const finalResult = { listA: result.listA || [], listB: result.listB || [] };
|
|
1245
|
+
this.spacerAlignmentCache.set(key, finalResult);
|
|
1246
|
+
this.persistence.set(CachePersistenceService.STORE_OPENAI_SPACER, key, finalResult);
|
|
1247
|
+
if (result.listA?.length && result.listB?.length) {
|
|
1248
|
+
this.rawResponse$.next(JSON.stringify(result));
|
|
1249
|
+
}
|
|
1250
|
+
return finalResult;
|
|
1251
|
+
}), catchError((error) => {
|
|
1252
|
+
console.error('Backend align-lists error:', error);
|
|
1253
|
+
return of({ listA: [], listB: [] });
|
|
1254
|
+
}));
|
|
1255
|
+
}
|
|
1256
|
+
return this.requestAlignedListsDirect(validA, validB, key, apiKey);
|
|
1257
|
+
}));
|
|
1258
|
+
}
|
|
1259
|
+
requestAlignedListsDirect(validA, validB, key, apiKey) {
|
|
1002
1260
|
const headers = new HttpHeaders({
|
|
1003
1261
|
[OpenAIEmbeddingService.HEADER_CONTENT_TYPE]: OpenAIEmbeddingService.CONTENT_TYPE_JSON,
|
|
1004
1262
|
[OpenAIEmbeddingService.HEADER_AUTHORIZATION]: `${OpenAIEmbeddingService.BEARER_PREFIX}${apiKey}`
|
|
1005
1263
|
});
|
|
1006
|
-
const prompt = `
|
|
1007
|
-
You are an expert semantic alignment engine.
|
|
1008
|
-
Task: Align the items from List A to List B based on semantic similarity.
|
|
1009
|
-
Context: This is a heuristic, human-semantic alignment (use embeddings later if you want it algorithmically strict).
|
|
1010
|
-
|
|
1011
|
-
List A: ${JSON.stringify(validA)}
|
|
1012
|
-
List B: ${JSON.stringify(validB)}
|
|
1013
|
-
|
|
1014
|
-
SEMANTIC STRATEGY (3-PASS PROCESS):
|
|
1015
|
-
1. **Pass 1: Exact & Strong Matches**: First, lock in all exact synonyms (e.g., "Dog" vs "Puppy") and strong category matches (e.g., "Tennis" vs "Badminton"). These are your anchors.
|
|
1016
|
-
2. **Pass 2: Thematic Clustering**: Next, group remaining items by sub-theme (e.g., "Water Sports", "Asian Cuisine"). Align items within these themes even if they aren't perfect pairs.
|
|
1017
|
-
3. **Pass 3: Contextual & Loose Matches**: Look for looser associations (e.g., "Reading" vs "Library").
|
|
1018
|
-
4. **Isolation**: If an item truly has NO relation to anything remaining in the other list, ISOLATE it using "----". Do NOT force a bad match.
|
|
1019
|
-
|
|
1020
|
-
Rules:
|
|
1021
|
-
1. Align items into clusters with variable dash-gaps to show semantic distance.
|
|
1022
|
-
2. Use "-" for a small hop (related but distinct).
|
|
1023
|
-
3. Use "----" for a hard theme switch (different category).
|
|
1024
|
-
4. You MUST reorder items to create the best semantic clusters.
|
|
1025
|
-
5. Ensure the output arrays "listA" and "listB" have the SAME length.
|
|
1026
|
-
6. If an item has no match, align it with "-" or "----" as appropriate, or just a gap.
|
|
1027
|
-
7. Output MUST be a JSON object with "listA" and "listB".
|
|
1028
|
-
8. **COMPLETENESS**: You MUST include EVERY item from the input List A and List B in the output. Do not omit any items.
|
|
1029
|
-
9. **Handling Leftovers**: If you have items left over at the end that don't match well, still include them in the list, aligned with "-" if necessary, or grouped together if they are somewhat related.
|
|
1030
|
-
10. **Spacer Alignment**: You MUST align "----" with "----" in both lists. Do NOT align a hard switch "----" with a word. If you need a switch on one side, put it on the other side too.
|
|
1031
|
-
11. **Strict Side Adherence**: Items from Input List A MUST ONLY appear in Output List A. Items from Input List B MUST ONLY appear in Output List B. Do not mix them up.
|
|
1032
|
-
12. **Mandatory Spacers**: You MUST insert a spacer ("-" or "----") between clusters of related items. Avoid putting "----" between every single item; use it only when the topic truly shifts.
|
|
1033
|
-
13. **NO TRAILING SPACER**: The last item in both lists MUST be a word. Do NOT put a spacer at the very end.
|
|
1034
|
-
14. **Length Mismatch Handling**: If one list is shorter than the other, you MUST fill the empty slots in the shorter list with spacers ("-") ONLY. DO NOT invent words or reuse words to fill the space.
|
|
1035
|
-
15. **HIGH SIMILARITY CONSOLIDATION (>= 0.8)**: This is CRITICAL for center display:
|
|
1036
|
-
- When you find a pair with VERY HIGH semantic similarity (>= 0.8), you MUST output the SAME TEXT in both listA and listB at that position.
|
|
1037
|
-
-
|
|
1038
|
-
- **
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
-
|
|
1076
|
-
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
- "
|
|
1083
|
-
- "
|
|
1084
|
-
- "
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
- **Did you miss any items from Input List B? ADD THEM NOW.**
|
|
1097
|
-
- If you are unsure where an item belongs, check the Input Lists provided above. Do NOT guess.
|
|
1098
|
-
|
|
1099
|
-
Example of Desired Output Logic:
|
|
1100
|
-
- "Volunteering at animal shelter" vs "Dog training" -> Related? Yes. Similarity >= 0.8? No (different activities)
|
|
1101
|
-
- NEXT PAIR: "Trail biking" vs "Rock climbing" -> Is this related to Animals? No. -> INSERT "----"
|
|
1102
|
-
- "Trail biking" vs "Rock climbing" -> Related? Yes (Outdoors).
|
|
1103
|
-
- NEXT PAIR: "Jogging" vs "Running" -> Similarity >= 0.8? YES! -> Output "Jogging × Running" in BOTH lists
|
|
1104
|
-
- "Jogging" vs "Running" -> Related? Yes (Exercise).
|
|
1105
|
-
- NEXT PAIR: "Beach volleyball" vs "Salsa dancing" -> Is this related to Exercise? Yes. -> INSERT "-" (Small hop)
|
|
1106
|
-
- "Beach volleyball" vs "Salsa dancing" -> Related? Yes (Hobbies). Similarity >= 0.8? No (different hobbies)
|
|
1107
|
-
- NEXT PAIR: "Fermenting kimchi" vs "Korean cooking" -> Is this related to Hobbies? No. -> INSERT "----"
|
|
1108
|
-
|
|
1109
|
-
JSON Output Structure:
|
|
1110
|
-
{
|
|
1111
|
-
"listA": ["Consolidated Item × Match", "----", "Unique Item A", "-" , "Unique Item A1" , "-" , "Unique Item A2" ,"----" , "Unique Item A3", "-" , "Unique Item A4 × Unique Item B4" , "----" , Unique Item A5, "----" , Uniue Item A6 ],
|
|
1112
|
-
"listB": ["Consolidated Item × Match", "----", "Unique Item B", "-" , "Unique Item B1" , "-" , "Unique Item B2" ,"----" , "Unique Item B3", "-" , "Unique Item A4 × Unique Item B4" , "----" , Unique Item B5, "----" , - ]
|
|
1113
|
-
}
|
|
1114
|
-
Note: In this case, List A has more items, so the last remaining item is displayed as a single card.In other cases, List B may have more words, and in that situation, the extra word from List B is shown as a single card.
|
|
1115
|
-
Each unique item in List A has a corresponding unique item in List B that is highly similar in meaning or related in terms of concept.
|
|
1116
|
-
VARIATION & SIMILARITY CONTROL: Maintain strict consistency. If the same semantic relationship appears multiple times, align it identically every time.
|
|
1264
|
+
const prompt = `
|
|
1265
|
+
You are an expert semantic alignment engine.
|
|
1266
|
+
Task: Align the items from List A to List B based on semantic similarity.
|
|
1267
|
+
Context: This is a heuristic, human-semantic alignment (use embeddings later if you want it algorithmically strict).
|
|
1268
|
+
|
|
1269
|
+
List A: ${JSON.stringify(validA)}
|
|
1270
|
+
List B: ${JSON.stringify(validB)}
|
|
1271
|
+
|
|
1272
|
+
SEMANTIC STRATEGY (3-PASS PROCESS):
|
|
1273
|
+
1. **Pass 1: Exact & Strong Matches**: First, lock in all exact synonyms (e.g., "Dog" vs "Puppy") and strong category matches (e.g., "Tennis" vs "Badminton"). These are your anchors.
|
|
1274
|
+
2. **Pass 2: Thematic Clustering**: Next, group remaining items by sub-theme (e.g., "Water Sports", "Asian Cuisine"). Align items within these themes even if they aren't perfect pairs.
|
|
1275
|
+
3. **Pass 3: Contextual & Loose Matches**: Look for looser associations (e.g., "Reading" vs "Library").
|
|
1276
|
+
4. **Isolation**: If an item truly has NO relation to anything remaining in the other list, ISOLATE it using "----". Do NOT force a bad match.
|
|
1277
|
+
|
|
1278
|
+
Rules:
|
|
1279
|
+
1. Align items into clusters with variable dash-gaps to show semantic distance.
|
|
1280
|
+
2. Use "-" for a small hop (related but distinct).
|
|
1281
|
+
3. Use "----" for a hard theme switch (different category).
|
|
1282
|
+
4. You MUST reorder items to create the best semantic clusters.
|
|
1283
|
+
5. Ensure the output arrays "listA" and "listB" have the SAME length.
|
|
1284
|
+
6. If an item has no match, align it with "-" or "----" as appropriate, or just a gap.
|
|
1285
|
+
7. Output MUST be a JSON object with "listA" and "listB".
|
|
1286
|
+
8. **COMPLETENESS**: You MUST include EVERY item from the input List A and List B in the output. Do not omit any items.
|
|
1287
|
+
9. **Handling Leftovers**: If you have items left over at the end that don't match well, still include them in the list, aligned with "-" if necessary, or grouped together if they are somewhat related.
|
|
1288
|
+
10. **Spacer Alignment**: You MUST align "----" with "----" in both lists. Do NOT align a hard switch "----" with a word. If you need a switch on one side, put it on the other side too.
|
|
1289
|
+
11. **Strict Side Adherence**: Items from Input List A MUST ONLY appear in Output List A. Items from Input List B MUST ONLY appear in Output List B. Do not mix them up.
|
|
1290
|
+
12. **Mandatory Spacers**: You MUST insert a spacer ("-" or "----") between clusters of related items. Avoid putting "----" between every single item; use it only when the topic truly shifts.
|
|
1291
|
+
13. **NO TRAILING SPACER**: The last item in both lists MUST be a word. Do NOT put a spacer at the very end.
|
|
1292
|
+
14. **Length Mismatch Handling**: If one list is shorter than the other, you MUST fill the empty slots in the shorter list with spacers ("-") ONLY. DO NOT invent words or reuse words to fill the space.
|
|
1293
|
+
15. **HIGH SIMILARITY CONSOLIDATION (>= 0.8)**: This is CRITICAL for center display:
|
|
1294
|
+
- When you find a pair with VERY HIGH semantic similarity (>= 0.8), you MUST output the SAME TEXT in both listA and listB at that position.
|
|
1295
|
+
- **Key principle**: If the items are synonyms, very similar activities, or one is a specific type of the other, consolidate them!
|
|
1296
|
+
- **STRICT CONSTRAINT**: You can ONLY consolidate if BOTH items actually exist in the input lists and are semantically related.
|
|
1297
|
+
- **NEVER** invent an item for List B just to make a pair with List A. If List A has "figma" and List B has NOTHING related, output "figma" in List A and "-" in List B. DO NOT put "figma" in List B.
|
|
1298
|
+
- **STRICT SYMMETRY**: If you use the "×" symbol to consolidate, you MUST output the EXACT SAME string in BOTH "listA" and "listB" at that index.
|
|
1299
|
+
- This allows the system to automatically move these items to the center display!
|
|
1300
|
+
- **NEVER** use "×" on one side and a spacer on the other. That is a critical failure.
|
|
1301
|
+
- **NEVER** include similarity scores or any other numbers in the output strings unless they are part of the original item text.
|
|
1302
|
+
- **ONLY** use "×" as the separator for consolidated items. Do not use "/", "&", or other symbols for this purpose.
|
|
1303
|
+
- Example: "Jogging" (List A) + "Running" (List B) -> "Jogging × Running" in BOTH listA and listB.
|
|
1304
|
+
- **NEVER DROP CONSOLIDATED ITEMS**: If you consolidate "Jogging" and "Running" into "Jogging × Running", you MUST put this string in **BOTH** List A and List B. Do NOT put it in List A and a spacer in List B. That would delete "Running" from the output.
|
|
1305
|
+
- **CRITICAL ERROR PREVENTION**: Do NOT drop items from List B. If "Running" is in Input List B, it MUST appear in Output List B (either as "Running" or "Jogging × Running"). If you output "Jogging × Running" in List A but "-" in List B, you have FAILED.
|
|
1306
|
+
- **COMPLETENESS CHECK**: You MUST verify that EVERY item from the input lists is present in the output. If "Running" is in Input List B, it MUST be in Output List B (or consolidated). DO NOT DROP ITEMS.
|
|
1307
|
+
- **DETERMINISM**: Process the list from top to bottom. Do not change your output between runs.
|
|
1308
|
+
- This allows the system to automatically move these items to the center display!
|
|
1309
|
+
16. Do not insert any new, inferred, or related words. Each pair must be created strictly from the existing list items only.
|
|
1310
|
+
|
|
1311
|
+
NEGATIVE CONSTRAINTS:
|
|
1312
|
+
- NEVER put an item from List A into List B.
|
|
1313
|
+
- NEVER put an item from List B into List A.
|
|
1314
|
+
- NEVER invent new items. Only use items from the input lists.
|
|
1315
|
+
- **NEVER DUPLICATE ITEMS**. Each item from the input list must appear EXACTLY ONCE in the output.
|
|
1316
|
+
|
|
1317
|
+
VERIFICATION STEP:
|
|
1318
|
+
Before outputting, verify every single item:
|
|
1319
|
+
- "Is this item in Input List A?" -> If yes, put it in Output List A.
|
|
1320
|
+
- "Is this item in Input List B?" -> If yes, put it in Output List B.
|
|
1321
|
+
- "Is this item in neither?" -> It is a hallucination. DELETE IT.
|
|
1322
|
+
|
|
1323
|
+
CRITICAL: OUTPUT MUST CONTAIN EXACTLY THE SAME ITEMS AS INPUT. NO DELETIONS ALLOWED.
|
|
1324
|
+
|
|
1325
|
+
FINAL SANITY CHECK:
|
|
1326
|
+
- **HALLUCINATION CHECK**: Go through every word in your Output List A. Is it EXACTLY present in Input List A? If not, DELETE IT. Do the same for List B.
|
|
1327
|
+
- **DUPLICATION CHECK**: Scan List A and List B. Does ANY word appear more than once? If so, DELETE the extra occurrences immediately. Each item must appear EXACTLY ONCE.
|
|
1328
|
+
- **COUNT CHECK**: Count the number of non-spacer items in your Output List A. Does it match the number of items in Input List A? If not, you are missing items. FIND THEM AND ADD THEM.
|
|
1329
|
+
- **COUNT CHECK**: Do the same for List B.
|
|
1330
|
+
- Look at your generated "listA". Are there any items that belong to Input List B? REMOVE THEM immediately.
|
|
1331
|
+
- Look at your generated "listB". Are there any items that belong to Input List A? REMOVE THEM immediately.
|
|
1332
|
+
- **Did you miss any items from Input List A? ADD THEM NOW.**
|
|
1333
|
+
- **Did you miss any items from Input List B? ADD THEM NOW.**
|
|
1334
|
+
- If you are unsure where an item belongs, check the Input Lists provided above. Do NOT guess.
|
|
1335
|
+
|
|
1336
|
+
Example of Desired Output Logic:
|
|
1337
|
+
- "Volunteering at animal shelter" vs "Dog training" -> Related? Yes. Similarity >= 0.8? No (different activities)
|
|
1338
|
+
- NEXT PAIR: "Trail biking" vs "Rock climbing" -> Is this related to Animals? No. -> INSERT "----"
|
|
1339
|
+
- "Trail biking" vs "Rock climbing" -> Related? Yes (Outdoors).
|
|
1340
|
+
- NEXT PAIR: "Jogging" vs "Running" -> Similarity >= 0.8? YES! -> Output "Jogging × Running" in BOTH lists
|
|
1341
|
+
- "Jogging" vs "Running" -> Related? Yes (Exercise).
|
|
1342
|
+
- NEXT PAIR: "Beach volleyball" vs "Salsa dancing" -> Is this related to Exercise? Yes. -> INSERT "-" (Small hop)
|
|
1343
|
+
- "Beach volleyball" vs "Salsa dancing" -> Related? Yes (Hobbies). Similarity >= 0.8? No (different hobbies)
|
|
1344
|
+
- NEXT PAIR: "Fermenting kimchi" vs "Korean cooking" -> Is this related to Hobbies? No. -> INSERT "----"
|
|
1345
|
+
|
|
1346
|
+
JSON Output Structure:
|
|
1347
|
+
{
|
|
1348
|
+
"listA": ["Consolidated Item × Match", "----", "Unique Item A", "-" , "Unique Item A1" , "-" , "Unique Item A2" ,"----" , "Unique Item A3", "-" , "Unique Item A4 × Unique Item B4" , "----" , Unique Item A5, "----" , Uniue Item A6 ],
|
|
1349
|
+
"listB": ["Consolidated Item × Match", "----", "Unique Item B", "-" , "Unique Item B1" , "-" , "Unique Item B2" ,"----" , "Unique Item B3", "-" , "Unique Item A4 × Unique Item B4" , "----" , Unique Item B5, "----" , - ]
|
|
1350
|
+
}
|
|
1351
|
+
Note: In this case, List A has more items, so the last remaining item is displayed as a single card.In other cases, List B may have more words, and in that situation, the extra word from List B is shown as a single card.
|
|
1352
|
+
Each unique item in List A has a corresponding unique item in List B that is highly similar in meaning or related in terms of concept.
|
|
1353
|
+
VARIATION & SIMILARITY CONTROL: Maintain strict consistency. If the same semantic relationship appears multiple times, align it identically every time.
|
|
1117
1354
|
`;
|
|
1118
1355
|
const body = {
|
|
1119
1356
|
model: this.MODEL,
|
|
@@ -1128,6 +1365,10 @@ class OpenAIEmbeddingService {
|
|
|
1128
1365
|
return this.http.post(this.API_URL, body, { headers }).pipe(map(response => {
|
|
1129
1366
|
try {
|
|
1130
1367
|
const content = response.choices[0].message.content;
|
|
1368
|
+
console.log('OpenAI LLM Output (getAlignedLists):', content);
|
|
1369
|
+
if (content) {
|
|
1370
|
+
this.rawResponse$.next(content);
|
|
1371
|
+
}
|
|
1131
1372
|
const result = JSON.parse(content);
|
|
1132
1373
|
// Handle both "listA/listB" format and "alignment" array format
|
|
1133
1374
|
if (result.alignment && Array.isArray(result.alignment)) {
|
|
@@ -1164,6 +1405,7 @@ class OpenAIEmbeddingService {
|
|
|
1164
1405
|
const sanitized = this.sanitizeResponse(result.listA, result.listB, validA, validB);
|
|
1165
1406
|
const finalResult = { listA: sanitized.listA, listB: sanitized.listB };
|
|
1166
1407
|
this.spacerAlignmentCache.set(key, finalResult);
|
|
1408
|
+
this.persistence.set(CachePersistenceService.STORE_OPENAI_SPACER, key, finalResult);
|
|
1167
1409
|
return finalResult;
|
|
1168
1410
|
}
|
|
1169
1411
|
catch (e) {
|
|
@@ -1276,67 +1518,66 @@ class OpenAIEmbeddingService {
|
|
|
1276
1518
|
const validB = listB.filter(i => i && i.trim() && i.trim() !== OpenAIEmbeddingService.SPACER_SMALL);
|
|
1277
1519
|
if (validA.length === 0 && validB.length === 0)
|
|
1278
1520
|
return of([]);
|
|
1279
|
-
// Key for (A, B)
|
|
1280
1521
|
const keyDirect = JSON.stringify({ a: validA, b: validB });
|
|
1281
|
-
// Key for (B, A) - assuming validB passed as first arg
|
|
1282
1522
|
const keyReverse = JSON.stringify({ a: validB, b: validA });
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
}
|
|
1298
|
-
return
|
|
1299
|
-
}
|
|
1300
|
-
|
|
1523
|
+
return from(this.loadAlignmentCache()).pipe(switchMap(() => {
|
|
1524
|
+
if (this.alignmentCache.has(keyDirect)) {
|
|
1525
|
+
console.log('Alignment cache hit (Direct)');
|
|
1526
|
+
return of(this.alignmentCache.get(keyDirect).rows);
|
|
1527
|
+
}
|
|
1528
|
+
if (this.alignmentCache.has(keyReverse)) {
|
|
1529
|
+
console.log('Alignment cache hit (Reverse) - Swapping results');
|
|
1530
|
+
const cached = this.alignmentCache.get(keyReverse);
|
|
1531
|
+
const swappedRows = cached.rows.map(row => ({
|
|
1532
|
+
left: row.right,
|
|
1533
|
+
right: row.left,
|
|
1534
|
+
score: row.score
|
|
1535
|
+
}));
|
|
1536
|
+
return of(swappedRows);
|
|
1537
|
+
}
|
|
1538
|
+
return this.requestGroupAlignLists(validA, validB, listA, listB, keyDirect, apiKey);
|
|
1539
|
+
}));
|
|
1540
|
+
}
|
|
1541
|
+
requestGroupAlignLists(validA, validB, listA, listB, keyDirect, apiKey) {
|
|
1301
1542
|
const headers = new HttpHeaders({
|
|
1302
1543
|
[OpenAIEmbeddingService.HEADER_CONTENT_TYPE]: OpenAIEmbeddingService.CONTENT_TYPE_JSON,
|
|
1303
1544
|
[OpenAIEmbeddingService.HEADER_AUTHORIZATION]: `${OpenAIEmbeddingService.BEARER_PREFIX}${apiKey}`
|
|
1304
1545
|
});
|
|
1305
1546
|
// Construct the prompt
|
|
1306
|
-
const prompt = `
|
|
1307
|
-
You are an expert semantic alignment engine.
|
|
1308
|
-
Task: Align the items from List A to List B based on semantic similarity.
|
|
1309
|
-
|
|
1310
|
-
List A: ${JSON.stringify(validA)}
|
|
1311
|
-
List B: ${JSON.stringify(validB)}
|
|
1312
|
-
|
|
1313
|
-
Rules:
|
|
1314
|
-
1. Match items that are semantically similar.
|
|
1315
|
-
2. Support One-to-One, One-to-Many, and Many-to-Many matches.
|
|
1316
|
-
3. **CRITICAL**: Avoid duplicate alignments. If multiple items in List B are highly similar to a single item in List A, group them ALL under that single List A item.
|
|
1317
|
-
4. **CONSISTENCY**: The alignment must be symmetric. If "Mutton" matches "Chicken" when "Mutton" is on the left, it MUST also match "Chicken" when "Mutton" is on the right.
|
|
1318
|
-
5. **ORDER**: You MUST reorder the items from BOTH lists to create the best semantic clusters. Place related groups next to each other (e.g., put all food items together, all tech items together).
|
|
1319
|
-
6. If an item in List A has no match in List B, map it to "-".
|
|
1320
|
-
7. If an item in List B is not matched to anything in List A, add a row with "-" on the left.
|
|
1321
|
-
8. Output MUST be a JSON object with a single key "alignment" containing an array of objects.
|
|
1322
|
-
9. Each object in the array must have:
|
|
1323
|
-
- "left": string (item from List A or "-")
|
|
1324
|
-
- "right": string (item from List B or "-")
|
|
1325
|
-
- "score": number (similarity score between 0 and 1)
|
|
1326
|
-
|
|
1327
|
-
Example Output Format:
|
|
1328
|
-
{
|
|
1329
|
-
"alignment": [
|
|
1330
|
-
{ "left": "Seafood", "right": "Fish", "score": 0.9 },
|
|
1331
|
-
{ "left": "Seafood", "right": "Clams", "score": 0.85 },
|
|
1332
|
-
{ "left": "Seafood", "right": "Crab", "score": 0.88 },
|
|
1333
|
-
{ "left": "Chicken", "right": "Meat", "score": 0.8 },
|
|
1334
|
-
{ "left": "Chicken", "right": "Poultry", "score": 0.9 },
|
|
1335
|
-
{ "left": "Chicken", "right": "Mutton", "score": 0.85 },
|
|
1336
|
-
{ "left": "Coffee", "right": "Cafés", "score": 0.9 },
|
|
1337
|
-
{ "left": "Coding", "right": "-", "score": 0 }
|
|
1338
|
-
]
|
|
1339
|
-
}
|
|
1547
|
+
const prompt = `
|
|
1548
|
+
You are an expert semantic alignment engine.
|
|
1549
|
+
Task: Align the items from List A to List B based on semantic similarity.
|
|
1550
|
+
|
|
1551
|
+
List A: ${JSON.stringify(validA)}
|
|
1552
|
+
List B: ${JSON.stringify(validB)}
|
|
1553
|
+
|
|
1554
|
+
Rules:
|
|
1555
|
+
1. Match items that are semantically similar.
|
|
1556
|
+
2. Support One-to-One, One-to-Many, and Many-to-Many matches.
|
|
1557
|
+
3. **CRITICAL**: Avoid duplicate alignments. If multiple items in List B are highly similar to a single item in List A, group them ALL under that single List A item.
|
|
1558
|
+
4. **CONSISTENCY**: The alignment must be symmetric. If "Mutton" matches "Chicken" when "Mutton" is on the left, it MUST also match "Chicken" when "Mutton" is on the right.
|
|
1559
|
+
5. **ORDER**: You MUST reorder the items from BOTH lists to create the best semantic clusters. Place related groups next to each other (e.g., put all food items together, all tech items together).
|
|
1560
|
+
6. If an item in List A has no match in List B, map it to "-".
|
|
1561
|
+
7. If an item in List B is not matched to anything in List A, add a row with "-" on the left.
|
|
1562
|
+
8. Output MUST be a JSON object with a single key "alignment" containing an array of objects.
|
|
1563
|
+
9. Each object in the array must have:
|
|
1564
|
+
- "left": string (item from List A or "-")
|
|
1565
|
+
- "right": string (item from List B or "-")
|
|
1566
|
+
- "score": number (similarity score between 0 and 1)
|
|
1567
|
+
|
|
1568
|
+
Example Output Format:
|
|
1569
|
+
{
|
|
1570
|
+
"alignment": [
|
|
1571
|
+
{ "left": "Seafood", "right": "Fish", "score": 0.9 },
|
|
1572
|
+
{ "left": "Seafood", "right": "Clams", "score": 0.85 },
|
|
1573
|
+
{ "left": "Seafood", "right": "Crab", "score": 0.88 },
|
|
1574
|
+
{ "left": "Chicken", "right": "Meat", "score": 0.8 },
|
|
1575
|
+
{ "left": "Chicken", "right": "Poultry", "score": 0.9 },
|
|
1576
|
+
{ "left": "Chicken", "right": "Mutton", "score": 0.85 },
|
|
1577
|
+
{ "left": "Coffee", "right": "Cafés", "score": 0.9 },
|
|
1578
|
+
{ "left": "Coding", "right": "-", "score": 0 }
|
|
1579
|
+
]
|
|
1580
|
+
}
|
|
1340
1581
|
`;
|
|
1341
1582
|
const body = {
|
|
1342
1583
|
model: this.MODEL,
|
|
@@ -1351,6 +1592,10 @@ class OpenAIEmbeddingService {
|
|
|
1351
1592
|
return this.http.post(this.API_URL, body, { headers }).pipe(map(response => {
|
|
1352
1593
|
try {
|
|
1353
1594
|
const content = response.choices[0].message.content;
|
|
1595
|
+
console.log('OpenAI LLM Output (groupAlignLists):', content);
|
|
1596
|
+
if (content) {
|
|
1597
|
+
this.rawResponse$.next(content);
|
|
1598
|
+
}
|
|
1354
1599
|
const result = JSON.parse(content);
|
|
1355
1600
|
if (!result.alignment || !Array.isArray(result.alignment)) {
|
|
1356
1601
|
throw new Error('Invalid JSON structure from OpenAI');
|
|
@@ -1497,11 +1742,9 @@ class OpenAIEmbeddingService {
|
|
|
1497
1742
|
}
|
|
1498
1743
|
finalRowsClean.push(row);
|
|
1499
1744
|
});
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
rows: finalRowsClean
|
|
1504
|
-
});
|
|
1745
|
+
const cacheEntry = { originalA: validA, originalB: validB, rows: finalRowsClean };
|
|
1746
|
+
this.alignmentCache.set(cacheKey, cacheEntry);
|
|
1747
|
+
this.persistence.set(CachePersistenceService.STORE_OPENAI_ALIGNMENT, cacheKey, cacheEntry);
|
|
1505
1748
|
return finalRowsClean;
|
|
1506
1749
|
}
|
|
1507
1750
|
catch (e) {
|
|
@@ -1523,29 +1766,55 @@ class OpenAIEmbeddingService {
|
|
|
1523
1766
|
if (!a.length || !b.length) {
|
|
1524
1767
|
return of(0);
|
|
1525
1768
|
}
|
|
1526
|
-
const
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
const EMBEDDINGS_URL = 'https://api.openai.com/v1/embeddings';
|
|
1531
|
-
const body = {
|
|
1532
|
-
model: 'text-embedding-3-small',
|
|
1533
|
-
input: [a, b]
|
|
1534
|
-
};
|
|
1535
|
-
return this.http.post(EMBEDDINGS_URL, body, { headers }).pipe(map(res => {
|
|
1536
|
-
const data = res?.data;
|
|
1537
|
-
if (!Array.isArray(data) || data.length < 2) {
|
|
1538
|
-
throw new Error('Invalid embeddings response from OpenAI');
|
|
1769
|
+
const simKey = [a, b].sort().join('\0');
|
|
1770
|
+
return from(this.loadSimilarityCache()).pipe(switchMap(() => {
|
|
1771
|
+
if (this.similarityCache.has(simKey)) {
|
|
1772
|
+
return of(this.similarityCache.get(simKey));
|
|
1539
1773
|
}
|
|
1540
|
-
const
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1774
|
+
const base = this.getBaseUrl();
|
|
1775
|
+
if (base) {
|
|
1776
|
+
const url = `${base.replace(/\/$/, '')}/profile/calculate-similarity`;
|
|
1777
|
+
return this.http
|
|
1778
|
+
.post(url, { textA: a, textB: b }, {
|
|
1779
|
+
headers: new HttpHeaders({ 'Content-Type': 'application/json' }),
|
|
1780
|
+
})
|
|
1781
|
+
.pipe(map((res) => {
|
|
1782
|
+
const similarity = res.similarity ?? 0;
|
|
1783
|
+
this.similarityCache.set(simKey, similarity);
|
|
1784
|
+
this.persistence.set(CachePersistenceService.STORE_OPENAI_SIMILARITY, simKey, similarity);
|
|
1785
|
+
return similarity;
|
|
1786
|
+
}), catchError((error) => {
|
|
1787
|
+
const msg = (error?.error && (error.error.error?.message || error.error.message)) || error?.message || 'Backend calculate-similarity failed';
|
|
1788
|
+
return throwError(() => new Error(msg));
|
|
1789
|
+
}));
|
|
1544
1790
|
}
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1791
|
+
const headers = new HttpHeaders({
|
|
1792
|
+
[OpenAIEmbeddingService.HEADER_CONTENT_TYPE]: OpenAIEmbeddingService.CONTENT_TYPE_JSON,
|
|
1793
|
+
[OpenAIEmbeddingService.HEADER_AUTHORIZATION]: `${OpenAIEmbeddingService.BEARER_PREFIX}${apiKey}`
|
|
1794
|
+
});
|
|
1795
|
+
const EMBEDDINGS_URL = 'https://api.openai.com/v1/embeddings';
|
|
1796
|
+
const body = {
|
|
1797
|
+
model: 'text-embedding-3-small',
|
|
1798
|
+
input: [a, b]
|
|
1799
|
+
};
|
|
1800
|
+
return this.http.post(EMBEDDINGS_URL, body, { headers }).pipe(map(res => {
|
|
1801
|
+
const data = res?.data;
|
|
1802
|
+
if (!Array.isArray(data) || data.length < 2) {
|
|
1803
|
+
throw new Error('Invalid embeddings response from OpenAI');
|
|
1804
|
+
}
|
|
1805
|
+
const v1 = data[0]?.embedding;
|
|
1806
|
+
const v2 = data[1]?.embedding;
|
|
1807
|
+
if (!v1 || !v2) {
|
|
1808
|
+
throw new Error('Embeddings not present in OpenAI response');
|
|
1809
|
+
}
|
|
1810
|
+
const similarity = this.cosineSimilarityVectors(v1, v2);
|
|
1811
|
+
this.similarityCache.set(simKey, similarity);
|
|
1812
|
+
this.persistence.set(CachePersistenceService.STORE_OPENAI_SIMILARITY, simKey, similarity);
|
|
1813
|
+
return similarity;
|
|
1814
|
+
}), catchError((error) => {
|
|
1815
|
+
const msg = (error?.error && (error.error.error?.message || error.error.message)) || error?.message || 'OpenAI embeddings request failed';
|
|
1816
|
+
return throwError(() => new Error(msg));
|
|
1817
|
+
}));
|
|
1549
1818
|
}));
|
|
1550
1819
|
}
|
|
1551
1820
|
/**
|
|
@@ -1554,24 +1823,50 @@ class OpenAIEmbeddingService {
|
|
|
1554
1823
|
getEmbeddings(texts, apiKey) {
|
|
1555
1824
|
if (!texts || texts.length === 0)
|
|
1556
1825
|
return of([]);
|
|
1557
|
-
const
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
const EMBEDDINGS_URL = 'https://api.openai.com/v1/embeddings';
|
|
1562
|
-
const body = {
|
|
1563
|
-
model: 'text-embedding-3-small',
|
|
1564
|
-
input: texts
|
|
1565
|
-
};
|
|
1566
|
-
return this.http.post(EMBEDDINGS_URL, body, { headers }).pipe(map(res => {
|
|
1567
|
-
const data = res?.data;
|
|
1568
|
-
if (!Array.isArray(data)) {
|
|
1569
|
-
throw new Error('Invalid embeddings response from OpenAI');
|
|
1826
|
+
const embKey = JSON.stringify(texts);
|
|
1827
|
+
return from(this.loadEmbeddingsCache()).pipe(switchMap(() => {
|
|
1828
|
+
if (this.embeddingsCache.has(embKey)) {
|
|
1829
|
+
return of(this.embeddingsCache.get(embKey));
|
|
1570
1830
|
}
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1831
|
+
const base = this.getBaseUrl();
|
|
1832
|
+
if (base) {
|
|
1833
|
+
const url = `${base.replace(/\/$/, '')}/profile/embeddings`;
|
|
1834
|
+
return this.http
|
|
1835
|
+
.post(url, { input: texts }, {
|
|
1836
|
+
headers: new HttpHeaders({ 'Content-Type': 'application/json' }),
|
|
1837
|
+
})
|
|
1838
|
+
.pipe(map((res) => {
|
|
1839
|
+
const result = (res.data || []).map((item) => item.embedding || []);
|
|
1840
|
+
this.embeddingsCache.set(embKey, result);
|
|
1841
|
+
this.persistence.set(CachePersistenceService.STORE_OPENAI_EMBEDDINGS, embKey, result);
|
|
1842
|
+
return result;
|
|
1843
|
+
}), catchError((error) => {
|
|
1844
|
+
const msg = (error?.error && (error.error.error?.message || error.error.message)) || error?.message || 'Backend embeddings failed';
|
|
1845
|
+
return throwError(() => new Error(msg));
|
|
1846
|
+
}));
|
|
1847
|
+
}
|
|
1848
|
+
const headers = new HttpHeaders({
|
|
1849
|
+
[OpenAIEmbeddingService.HEADER_CONTENT_TYPE]: OpenAIEmbeddingService.CONTENT_TYPE_JSON,
|
|
1850
|
+
[OpenAIEmbeddingService.HEADER_AUTHORIZATION]: `${OpenAIEmbeddingService.BEARER_PREFIX}${apiKey}`
|
|
1851
|
+
});
|
|
1852
|
+
const EMBEDDINGS_URL = 'https://api.openai.com/v1/embeddings';
|
|
1853
|
+
const body = {
|
|
1854
|
+
model: 'text-embedding-3-small',
|
|
1855
|
+
input: texts
|
|
1856
|
+
};
|
|
1857
|
+
return this.http.post(EMBEDDINGS_URL, body, { headers }).pipe(map(res => {
|
|
1858
|
+
const data = res?.data;
|
|
1859
|
+
if (!Array.isArray(data)) {
|
|
1860
|
+
throw new Error('Invalid embeddings response from OpenAI');
|
|
1861
|
+
}
|
|
1862
|
+
const result = data.map((item) => item.embedding);
|
|
1863
|
+
this.embeddingsCache.set(embKey, result);
|
|
1864
|
+
this.persistence.set(CachePersistenceService.STORE_OPENAI_EMBEDDINGS, embKey, result);
|
|
1865
|
+
return result;
|
|
1866
|
+
}), catchError((error) => {
|
|
1867
|
+
const msg = (error?.error && (error.error.error?.message || error.error.message)) || error?.message || 'OpenAI embeddings request failed';
|
|
1868
|
+
return throwError(() => new Error(msg));
|
|
1869
|
+
}));
|
|
1575
1870
|
}));
|
|
1576
1871
|
}
|
|
1577
1872
|
// Private helpers
|
|
@@ -1594,8 +1889,14 @@ class OpenAIEmbeddingService {
|
|
|
1594
1889
|
clearCache() {
|
|
1595
1890
|
this.alignmentCache.clear();
|
|
1596
1891
|
this.spacerAlignmentCache.clear();
|
|
1597
|
-
|
|
1598
|
-
|
|
1892
|
+
this.similarityCache.clear();
|
|
1893
|
+
this.embeddingsCache.clear();
|
|
1894
|
+
this.persistence.clear(CachePersistenceService.STORE_OPENAI_SPACER);
|
|
1895
|
+
this.persistence.clear(CachePersistenceService.STORE_OPENAI_ALIGNMENT);
|
|
1896
|
+
this.persistence.clear(CachePersistenceService.STORE_OPENAI_SIMILARITY);
|
|
1897
|
+
this.persistence.clear(CachePersistenceService.STORE_OPENAI_EMBEDDINGS);
|
|
1898
|
+
}
|
|
1899
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: OpenAIEmbeddingService, deps: [{ token: i1.HttpClient }, { token: PROFILE_COMPARISON_API_BASE_URL, optional: true }, { token: CachePersistenceService }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1599
1900
|
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: OpenAIEmbeddingService, providedIn: 'root' });
|
|
1600
1901
|
}
|
|
1601
1902
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: OpenAIEmbeddingService, decorators: [{
|
|
@@ -1603,7 +1904,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
|
|
|
1603
1904
|
args: [{
|
|
1604
1905
|
providedIn: 'root'
|
|
1605
1906
|
}]
|
|
1606
|
-
}], ctorParameters: () => [{ type: i1.HttpClient }
|
|
1907
|
+
}], ctorParameters: () => [{ type: i1.HttpClient }, { type: undefined, decorators: [{
|
|
1908
|
+
type: Optional
|
|
1909
|
+
}, {
|
|
1910
|
+
type: Inject,
|
|
1911
|
+
args: [PROFILE_COMPARISON_API_BASE_URL]
|
|
1912
|
+
}] }, { type: CachePersistenceService }] });
|
|
1607
1913
|
|
|
1608
1914
|
class ProfileComparisonLibService {
|
|
1609
1915
|
constructor() { }
|
|
@@ -1635,7 +1941,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
|
|
|
1635
1941
|
}]
|
|
1636
1942
|
}], ctorParameters: () => [] });
|
|
1637
1943
|
|
|
1638
|
-
// Face++ credentials must be provided by the caller (do not import consumer app environments from the library).
|
|
1639
1944
|
var FacePlusPlusFormFields;
|
|
1640
1945
|
(function (FacePlusPlusFormFields) {
|
|
1641
1946
|
FacePlusPlusFormFields["API_KEY"] = "api_key";
|
|
@@ -1653,28 +1958,154 @@ var FacePlusPlusErrorTypes;
|
|
|
1653
1958
|
})(FacePlusPlusErrorTypes || (FacePlusPlusErrorTypes = {}));
|
|
1654
1959
|
class ProfileService {
|
|
1655
1960
|
http;
|
|
1961
|
+
apiBaseUrl;
|
|
1962
|
+
persistence;
|
|
1656
1963
|
static BASE_URL = 'https://api.api-ninjas.com/v1/';
|
|
1657
1964
|
static FACE_PLUS_PLUS_URL = 'https://api-us.faceplusplus.com/facepp/v3/detect';
|
|
1658
1965
|
baseUrl = ProfileService.BASE_URL;
|
|
1659
1966
|
baseUrlf = ProfileService.FACE_PLUS_PLUS_URL;
|
|
1660
|
-
|
|
1967
|
+
faceCache = new Map();
|
|
1968
|
+
compareInterestsCache = new Map();
|
|
1969
|
+
faceLoaded = false;
|
|
1970
|
+
compareLoaded = false;
|
|
1971
|
+
faceLoadPromise = null;
|
|
1972
|
+
compareLoadPromise = null;
|
|
1973
|
+
/** Content-based cache key: hash long strings/data URLs so same content yields same key and we avoid huge keys. */
|
|
1974
|
+
static faceCacheKey(key) {
|
|
1975
|
+
if (key == null || key === '')
|
|
1976
|
+
return undefined;
|
|
1977
|
+
const toHash = key.startsWith('data:') && key.includes(',')
|
|
1978
|
+
? (key.split(',')[1] ?? key)
|
|
1979
|
+
: key;
|
|
1980
|
+
if (toHash.length > 500 || key.length > 2000) {
|
|
1981
|
+
let h = 5381;
|
|
1982
|
+
for (let i = 0; i < toHash.length; i++) {
|
|
1983
|
+
h = ((h << 5) + h + toHash.charCodeAt(i)) | 0;
|
|
1984
|
+
}
|
|
1985
|
+
return `face:${h}`;
|
|
1986
|
+
}
|
|
1987
|
+
return key;
|
|
1988
|
+
}
|
|
1989
|
+
constructor(http, apiBaseUrl, persistence) {
|
|
1661
1990
|
this.http = http;
|
|
1991
|
+
this.apiBaseUrl = apiBaseUrl;
|
|
1992
|
+
this.persistence = persistence;
|
|
1993
|
+
}
|
|
1994
|
+
loadFaceCache() {
|
|
1995
|
+
if (this.faceLoaded)
|
|
1996
|
+
return Promise.resolve();
|
|
1997
|
+
if (!this.faceLoadPromise) {
|
|
1998
|
+
this.faceLoadPromise = this.persistence.getAll(CachePersistenceService.STORE_PROFILE_FACE).then((entries) => {
|
|
1999
|
+
entries.forEach(([k, v]) => this.faceCache.set(k, v));
|
|
2000
|
+
this.faceLoaded = true;
|
|
2001
|
+
});
|
|
2002
|
+
}
|
|
2003
|
+
return this.faceLoadPromise;
|
|
2004
|
+
}
|
|
2005
|
+
loadCompareCache() {
|
|
2006
|
+
if (this.compareLoaded)
|
|
2007
|
+
return Promise.resolve();
|
|
2008
|
+
if (!this.compareLoadPromise) {
|
|
2009
|
+
this.compareLoadPromise = this.persistence.getAll(CachePersistenceService.STORE_PROFILE_COMPARE).then((entries) => {
|
|
2010
|
+
entries.forEach(([k, v]) => this.compareInterestsCache.set(k, v));
|
|
2011
|
+
this.compareLoaded = true;
|
|
2012
|
+
});
|
|
2013
|
+
}
|
|
2014
|
+
return this.compareLoadPromise;
|
|
2015
|
+
}
|
|
2016
|
+
getBaseUrl() {
|
|
2017
|
+
const u = this.apiBaseUrl;
|
|
2018
|
+
if (u == null)
|
|
2019
|
+
return null;
|
|
2020
|
+
return typeof u === 'function' ? u() : u;
|
|
1662
2021
|
}
|
|
1663
2022
|
compareInterests(text_1, text_2, apiKey) {
|
|
1664
|
-
const
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
2023
|
+
const a = (text_1 ?? '').trim();
|
|
2024
|
+
const b = (text_2 ?? '').trim();
|
|
2025
|
+
const cacheKey = a && b ? [a, b].sort().join('\0') : '';
|
|
2026
|
+
return from(this.loadCompareCache()).pipe(switchMap(() => {
|
|
2027
|
+
if (cacheKey && this.compareInterestsCache.has(cacheKey)) {
|
|
2028
|
+
return of(this.compareInterestsCache.get(cacheKey));
|
|
2029
|
+
}
|
|
2030
|
+
const base = this.getBaseUrl();
|
|
2031
|
+
if (base) {
|
|
2032
|
+
const url = `${base.replace(/\/$/, '')}/profile/compare-interests`;
|
|
2033
|
+
return this.http.post(url, { text_1: a || text_1, text_2: b || text_2 }, {
|
|
2034
|
+
headers: new HttpHeaders({ 'Content-Type': 'application/json' }),
|
|
2035
|
+
}).pipe(tap((res) => {
|
|
2036
|
+
if (cacheKey) {
|
|
2037
|
+
this.compareInterestsCache.set(cacheKey, res);
|
|
2038
|
+
this.persistence.set(CachePersistenceService.STORE_PROFILE_COMPARE, cacheKey, res);
|
|
2039
|
+
}
|
|
2040
|
+
}));
|
|
2041
|
+
}
|
|
2042
|
+
const headers = new HttpHeaders({
|
|
2043
|
+
'X-Api-Key': apiKey,
|
|
2044
|
+
'Content-Type': 'application/json',
|
|
2045
|
+
});
|
|
2046
|
+
return this.http.post(`${this.baseUrl}textsimilarity`, { text_1: a || text_1, text_2: b || text_2 }, { headers }).pipe(tap((res) => {
|
|
2047
|
+
if (cacheKey) {
|
|
2048
|
+
this.compareInterestsCache.set(cacheKey, res);
|
|
2049
|
+
this.persistence.set(CachePersistenceService.STORE_PROFILE_COMPARE, cacheKey, res);
|
|
2050
|
+
}
|
|
2051
|
+
}));
|
|
2052
|
+
}));
|
|
2053
|
+
}
|
|
2054
|
+
detectFace(image, creds, options) {
|
|
2055
|
+
const rawKey = options?.cacheKey ?? (typeof image === 'string' ? image : undefined);
|
|
2056
|
+
const cacheKey = ProfileService.faceCacheKey(rawKey);
|
|
2057
|
+
return from(this.loadFaceCache()).pipe(switchMap(() => {
|
|
2058
|
+
if (cacheKey && this.faceCache.has(cacheKey)) {
|
|
2059
|
+
console.log('Face detection cache hit');
|
|
2060
|
+
return of(this.faceCache.get(cacheKey));
|
|
2061
|
+
}
|
|
2062
|
+
const base = this.getBaseUrl();
|
|
2063
|
+
if (base) {
|
|
2064
|
+
const url = `${base.replace(/\/$/, '')}/profile/detect-face`;
|
|
2065
|
+
let body;
|
|
2066
|
+
if (typeof image === 'string') {
|
|
2067
|
+
const isDataUrl = image.startsWith('data:');
|
|
2068
|
+
const base64 = isDataUrl ? (image.split(',')[1] || '') : image;
|
|
2069
|
+
body = { image_base64: base64 };
|
|
2070
|
+
return this.http.post(url, body, {
|
|
2071
|
+
headers: new HttpHeaders({ 'Content-Type': 'application/json' }),
|
|
2072
|
+
}).pipe(retryWhen((errors) => errors.pipe(concatMap((error, i) => {
|
|
2073
|
+
const status = error?.status;
|
|
2074
|
+
const shouldRetry = (status === 403 || status === 429 || status === 503 || status === 401) && i < 1;
|
|
2075
|
+
if (shouldRetry)
|
|
2076
|
+
return timer(3000);
|
|
2077
|
+
return throwError(() => error);
|
|
2078
|
+
}))), tap((res) => {
|
|
2079
|
+
if (cacheKey) {
|
|
2080
|
+
this.faceCache.set(cacheKey, res);
|
|
2081
|
+
this.persistence.set(CachePersistenceService.STORE_PROFILE_FACE, cacheKey, res);
|
|
2082
|
+
}
|
|
2083
|
+
}));
|
|
2084
|
+
}
|
|
2085
|
+
const formData = new FormData();
|
|
2086
|
+
formData.append('image_file', image, image.name);
|
|
2087
|
+
return this.http.post(url, formData).pipe(retryWhen((errors) => errors.pipe(concatMap((error, i) => {
|
|
2088
|
+
const status = error?.status;
|
|
2089
|
+
const shouldRetry = (status === 403 || status === 429 || status === 503 || status === 401) && i < 1;
|
|
2090
|
+
if (shouldRetry)
|
|
2091
|
+
return timer(3000);
|
|
2092
|
+
return throwError(() => error);
|
|
2093
|
+
}))), tap((res) => {
|
|
2094
|
+
if (cacheKey) {
|
|
2095
|
+
this.faceCache.set(cacheKey, res);
|
|
2096
|
+
this.persistence.set(CachePersistenceService.STORE_PROFILE_FACE, cacheKey, res);
|
|
2097
|
+
}
|
|
2098
|
+
}));
|
|
2099
|
+
}
|
|
2100
|
+
return this.detectFaceDirect(image, creds, cacheKey);
|
|
2101
|
+
}));
|
|
1669
2102
|
}
|
|
1670
|
-
|
|
2103
|
+
detectFaceDirect(image, creds, cacheKey) {
|
|
1671
2104
|
const formData = new FormData();
|
|
1672
|
-
// Use Face++ credentials from caller; the library must not depend on consumer application environments
|
|
1673
2105
|
const apiKey = creds?.apiKey ?? '';
|
|
1674
2106
|
const apiSecret = creds?.apiSecret ?? '';
|
|
1675
2107
|
formData.append(FacePlusPlusFormFields.API_KEY, apiKey);
|
|
1676
2108
|
formData.append(FacePlusPlusFormFields.API_SECRET, apiSecret);
|
|
1677
|
-
// Provide either image_file (binary) or image_base64
|
|
1678
2109
|
if (typeof image === 'string') {
|
|
1679
2110
|
const isDataUrl = image.startsWith('data:');
|
|
1680
2111
|
const base64 = isDataUrl ? (image.split(',')[1] || '') : image;
|
|
@@ -1683,9 +2114,7 @@ class ProfileService {
|
|
|
1683
2114
|
else {
|
|
1684
2115
|
formData.append(FacePlusPlusFormFields.IMAGE_FILE, image, image.name);
|
|
1685
2116
|
}
|
|
1686
|
-
// Ask Face++ to return facial landmarks so eyes and nose can be positioned accurately
|
|
1687
2117
|
formData.append(FacePlusPlusFormFields.RETURN_LANDMARK, '1');
|
|
1688
|
-
// Map Face++ response to IFaceDetectionResult shape
|
|
1689
2118
|
return this.http.post(`${this.baseUrlf}`, formData).pipe(
|
|
1690
2119
|
// Retry once after a short delay on Face++ rate-limit/security responses
|
|
1691
2120
|
retryWhen((errors) => errors.pipe(concatMap((error, i) => {
|
|
@@ -1744,23 +2173,100 @@ class ProfileService {
|
|
|
1744
2173
|
: [];
|
|
1745
2174
|
const imageUrl = typeof image === 'string' && image.startsWith('http') ? image : '';
|
|
1746
2175
|
const result = { imageUrl, faces };
|
|
2176
|
+
if (cacheKey) {
|
|
2177
|
+
this.faceCache.set(cacheKey, result);
|
|
2178
|
+
this.persistence.set(CachePersistenceService.STORE_PROFILE_FACE, cacheKey, result);
|
|
2179
|
+
}
|
|
1747
2180
|
return result;
|
|
1748
2181
|
}));
|
|
1749
2182
|
}
|
|
1750
|
-
|
|
2183
|
+
clearFaceCache() {
|
|
2184
|
+
this.faceCache.clear();
|
|
2185
|
+
this.persistence.clear(CachePersistenceService.STORE_PROFILE_FACE);
|
|
2186
|
+
}
|
|
2187
|
+
clearCompareInterestsCache() {
|
|
2188
|
+
this.compareInterestsCache.clear();
|
|
2189
|
+
this.persistence.clear(CachePersistenceService.STORE_PROFILE_COMPARE);
|
|
2190
|
+
}
|
|
2191
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: ProfileService, deps: [{ token: i1.HttpClient }, { token: PROFILE_COMPARISON_API_BASE_URL, optional: true }, { token: CachePersistenceService }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1751
2192
|
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: ProfileService, providedIn: 'root' });
|
|
1752
2193
|
}
|
|
1753
2194
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: ProfileService, decorators: [{
|
|
1754
2195
|
type: Injectable,
|
|
1755
2196
|
args: [{ providedIn: 'root' }]
|
|
1756
|
-
}], ctorParameters: () => [{ type: i1.HttpClient }
|
|
2197
|
+
}], ctorParameters: () => [{ type: i1.HttpClient }, { type: undefined, decorators: [{
|
|
2198
|
+
type: Optional
|
|
2199
|
+
}, {
|
|
2200
|
+
type: Inject,
|
|
2201
|
+
args: [PROFILE_COMPARISON_API_BASE_URL]
|
|
2202
|
+
}] }, { type: CachePersistenceService }] });
|
|
2203
|
+
|
|
2204
|
+
/**
|
|
2205
|
+
* Thin-client service: sends config to the backend and returns the display payload.
|
|
2206
|
+
* The lib does not call third-party APIs; the backend does.
|
|
2207
|
+
*/
|
|
2208
|
+
class ProfileComparisonBackendService {
|
|
2209
|
+
http;
|
|
2210
|
+
apiBaseUrl;
|
|
2211
|
+
getVerboseLogging;
|
|
2212
|
+
constructor(http, apiBaseUrl, getVerboseLogging) {
|
|
2213
|
+
this.http = http;
|
|
2214
|
+
this.apiBaseUrl = apiBaseUrl;
|
|
2215
|
+
this.getVerboseLogging = getVerboseLogging;
|
|
2216
|
+
}
|
|
2217
|
+
log(...args) {
|
|
2218
|
+
if (this.getVerboseLogging?.() === true) {
|
|
2219
|
+
console.log('[ProfileComparisonBackend]', ...args);
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2222
|
+
getBaseUrl() {
|
|
2223
|
+
const u = this.apiBaseUrl;
|
|
2224
|
+
if (u == null)
|
|
2225
|
+
return null;
|
|
2226
|
+
return typeof u === 'function' ? u() : u;
|
|
2227
|
+
}
|
|
2228
|
+
getComparison(config) {
|
|
2229
|
+
const base = this.getBaseUrl();
|
|
2230
|
+
this.log('getComparison', 'baseUrl', base ?? '(null)');
|
|
2231
|
+
if (!base || !base.trim()) {
|
|
2232
|
+
this.log('getComparison', 'returning error: backend not configured');
|
|
2233
|
+
return throwError(() => new Error('Backend not configured'));
|
|
2234
|
+
}
|
|
2235
|
+
const url = `${base.replace(/\/$/, '')}/profile/compare-full`;
|
|
2236
|
+
this.log('getComparison', 'POST', url);
|
|
2237
|
+
return this.http.post(url, { config }).pipe(tap$1(() => this.log('getComparison', 'POST success')), catchError((err) => {
|
|
2238
|
+
this.log('getComparison', 'POST error', err);
|
|
2239
|
+
const msg = err?.error?.message ?? err?.message ?? err?.statusText ?? String(err);
|
|
2240
|
+
const status = err?.status ?? err?.statusCode;
|
|
2241
|
+
console.error('[ProfileComparisonBackend] getComparison failed', status, msg);
|
|
2242
|
+
return throwError(() => err);
|
|
2243
|
+
}));
|
|
2244
|
+
}
|
|
2245
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: ProfileComparisonBackendService, deps: [{ token: i1.HttpClient }, { token: PROFILE_COMPARISON_API_BASE_URL, optional: true }, { token: PROFILE_COMPARISON_VERBOSE_LOGGING, optional: true }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
2246
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: ProfileComparisonBackendService, providedIn: 'root' });
|
|
2247
|
+
}
|
|
2248
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: ProfileComparisonBackendService, decorators: [{
|
|
2249
|
+
type: Injectable,
|
|
2250
|
+
args: [{ providedIn: 'root' }]
|
|
2251
|
+
}], ctorParameters: () => [{ type: i1.HttpClient }, { type: undefined, decorators: [{
|
|
2252
|
+
type: Optional
|
|
2253
|
+
}, {
|
|
2254
|
+
type: Inject,
|
|
2255
|
+
args: [PROFILE_COMPARISON_API_BASE_URL]
|
|
2256
|
+
}] }, { type: undefined, decorators: [{
|
|
2257
|
+
type: Optional
|
|
2258
|
+
}, {
|
|
2259
|
+
type: Inject,
|
|
2260
|
+
args: [PROFILE_COMPARISON_VERBOSE_LOGGING]
|
|
2261
|
+
}] }] });
|
|
1757
2262
|
|
|
1758
2263
|
class ProfileComparisonLibComponent {
|
|
1759
|
-
|
|
1760
|
-
openaiEmbeddingService;
|
|
2264
|
+
backendService;
|
|
1761
2265
|
renderer;
|
|
1762
2266
|
fileConversionService;
|
|
1763
2267
|
imageCompressionService;
|
|
2268
|
+
cdr;
|
|
2269
|
+
getVerboseLogging;
|
|
1764
2270
|
static DEFAULT_OBJECT_POSITION = '50% 50%';
|
|
1765
2271
|
static DEFAULT_EYE_ALIGNMENT_BIAS_PX = 50;
|
|
1766
2272
|
static DEFAULT_IMAGE_FORMAT = 'image/jpeg';
|
|
@@ -1779,6 +2285,20 @@ class ProfileComparisonLibComponent {
|
|
|
1779
2285
|
static CONSOLIDATION_SEPARATOR_SLASH = '/';
|
|
1780
2286
|
static FACEPP_RATE_LIMIT_DELAY_MS = 5000;
|
|
1781
2287
|
static DEFAULT_PROFILE_IMAGE_NAME = 'ProfilePg1.png';
|
|
2288
|
+
// Drag and shape constants
|
|
2289
|
+
static SHAPE_BG_HEIGHT = '350px';
|
|
2290
|
+
static SHAPE_BG_OBJECT_FIT = 'cover';
|
|
2291
|
+
static SHAPE_BG1_OBJECT_POSITION = 'right';
|
|
2292
|
+
static SHAPE_BG2_OBJECT_POSITION = 'left';
|
|
2293
|
+
static DRAG_MIN_WIDTH = 200;
|
|
2294
|
+
static DRAG_MAX_WIDTH = 400;
|
|
2295
|
+
static MAX_DRAG_DISTANCE = 100;
|
|
2296
|
+
static CENTER_MOVE_MULTIPLIER = 0.5;
|
|
2297
|
+
static TEXT_MOVE_MULTIPLIER = 0.3;
|
|
2298
|
+
static TRANSITION_WIDTH = 'width 0.3s ease';
|
|
2299
|
+
static TRANSITION_TRANSFORM = 'transform 0.3s ease';
|
|
2300
|
+
static TRANSFORM_RESET = 'translateX(0px)';
|
|
2301
|
+
static TRANSITION_RESET_DELAY_MS = 300;
|
|
1782
2302
|
config = {
|
|
1783
2303
|
person1Interests: [],
|
|
1784
2304
|
person2Interests: [],
|
|
@@ -1786,20 +2306,19 @@ class ProfileComparisonLibComponent {
|
|
|
1786
2306
|
user1Image: '',
|
|
1787
2307
|
user2Image: '',
|
|
1788
2308
|
};
|
|
1789
|
-
apiNinjasKey = '';
|
|
1790
|
-
faceplusKey = '';
|
|
1791
|
-
faceplusSecret = '';
|
|
1792
|
-
openaiApiKey = '';
|
|
1793
2309
|
fadeAllEdges = false;
|
|
1794
2310
|
matrixDataChange = new EventEmitter();
|
|
2311
|
+
rawLLMOutputChange = new EventEmitter();
|
|
2312
|
+
viewProfileClick = new EventEmitter();
|
|
1795
2313
|
selectedFile = null;
|
|
1796
|
-
result;
|
|
1797
2314
|
firstImageData = null;
|
|
1798
2315
|
secondImageData = null;
|
|
1799
2316
|
user1Transform = '';
|
|
1800
2317
|
user2Transform = '';
|
|
1801
2318
|
alignmentCalculated = false;
|
|
1802
2319
|
isAligning = false;
|
|
2320
|
+
/** Set when backend returns an error (e.g. 503 API keys not configured). */
|
|
2321
|
+
backendError = null;
|
|
1803
2322
|
user1ObjectPosition = ProfileComparisonLibComponent.DEFAULT_OBJECT_POSITION;
|
|
1804
2323
|
user2ObjectPosition = ProfileComparisonLibComponent.DEFAULT_OBJECT_POSITION;
|
|
1805
2324
|
user1NaturalWidth = 0;
|
|
@@ -1837,20 +2356,8 @@ class ProfileComparisonLibComponent {
|
|
|
1837
2356
|
similarity = 0;
|
|
1838
2357
|
similarityMatrixCsv = '';
|
|
1839
2358
|
matrixData = null;
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
apiKeyInputValue = '';
|
|
1843
|
-
quotaExhausted = false;
|
|
1844
|
-
customFaceppKey = '';
|
|
1845
|
-
customFaceppSecret = '';
|
|
1846
|
-
showFaceppKeyModal = false;
|
|
1847
|
-
faceppKeyInputValue = '';
|
|
1848
|
-
faceppSecretInputValue = '';
|
|
1849
|
-
faceppQuotaExhausted = false;
|
|
1850
|
-
customOpenAIKey = '';
|
|
1851
|
-
showOpenAIKeyModal = false;
|
|
1852
|
-
openaiKeyInputValue = '';
|
|
1853
|
-
openaiQuotaExhausted = false;
|
|
2359
|
+
/** When false, backend URL is not provided — show "Configure backend". */
|
|
2360
|
+
backendConfigured = false;
|
|
1854
2361
|
compressionConfig = {
|
|
1855
2362
|
maxWidth: ProfileComparisonLibComponent.DEFAULT_MAX_WIDTH,
|
|
1856
2363
|
maxHeight: ProfileComparisonLibComponent.DEFAULT_MAX_HEIGHT,
|
|
@@ -1864,50 +2371,199 @@ class ProfileComparisonLibComponent {
|
|
|
1864
2371
|
minHeight: ProfileComparisonLibComponent.DEFAULT_MIN_HEIGHT,
|
|
1865
2372
|
};
|
|
1866
2373
|
computeSub;
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
activeRequestEpoch = 0;
|
|
1870
|
-
form = new FormGroup({
|
|
1871
|
-
interest1: new FormControl(''),
|
|
1872
|
-
interest2: new FormControl(''),
|
|
1873
|
-
});
|
|
2374
|
+
/** Incremented on each fetchComparison(); used to ignore stale success/error from an older request. */
|
|
2375
|
+
fetchRequestId = 0;
|
|
1874
2376
|
leftContainer;
|
|
1875
2377
|
rightContainer;
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
2378
|
+
profileFlex;
|
|
2379
|
+
profileImgLeft;
|
|
2380
|
+
profileImgRight;
|
|
2381
|
+
shapeContainer;
|
|
2382
|
+
shapeBg;
|
|
2383
|
+
shapeBg1;
|
|
2384
|
+
shapeBg2;
|
|
2385
|
+
shapeTextLeft;
|
|
2386
|
+
shapeTextRight;
|
|
2387
|
+
shapeTextCenter;
|
|
2388
|
+
constructor(backendService, renderer, fileConversionService, imageCompressionService, cdr, getVerboseLogging) {
|
|
2389
|
+
this.backendService = backendService;
|
|
1879
2390
|
this.renderer = renderer;
|
|
1880
2391
|
this.fileConversionService = fileConversionService;
|
|
1881
2392
|
this.imageCompressionService = imageCompressionService;
|
|
2393
|
+
this.cdr = cdr;
|
|
2394
|
+
this.getVerboseLogging = getVerboseLogging;
|
|
2395
|
+
}
|
|
2396
|
+
log(...args) {
|
|
2397
|
+
if (this.getVerboseLogging?.() === true) {
|
|
2398
|
+
console.log('[ProfileComparisonLib]', ...args);
|
|
2399
|
+
}
|
|
1882
2400
|
}
|
|
1883
2401
|
anchorSide = 'left';
|
|
2402
|
+
rawLLMOutput = '';
|
|
1884
2403
|
ngOnInit() {
|
|
2404
|
+
if (this.getVerboseLogging?.())
|
|
2405
|
+
console.log('[ProfileComparisonLib] verbose logging ON');
|
|
2406
|
+
this.log('ngOnInit', 'config received', {
|
|
2407
|
+
person1Len: this.config?.person1Interests?.length,
|
|
2408
|
+
person2Len: this.config?.person2Interests?.length,
|
|
2409
|
+
hasUser1Image: !!this.config?.user1Image?.length,
|
|
2410
|
+
hasUser2Image: !!this.config?.user2Image?.length,
|
|
2411
|
+
});
|
|
1885
2412
|
this.baseConfig = JSON.parse(JSON.stringify(this.config));
|
|
1886
|
-
this.loadCustomConfig();
|
|
1887
2413
|
this.updateConfigProperties();
|
|
1888
|
-
this.
|
|
1889
|
-
this.
|
|
1890
|
-
this.
|
|
1891
|
-
this.
|
|
2414
|
+
const base = this.backendService.getBaseUrl();
|
|
2415
|
+
this.backendConfigured = !!(base && base.trim());
|
|
2416
|
+
this.log('ngOnInit', 'backendConfigured', this.backendConfigured, 'base', base ?? '(null)');
|
|
2417
|
+
if (!this.backendConfigured) {
|
|
2418
|
+
this.log('ngOnInit', 'exiting early: backend not configured');
|
|
2419
|
+
this.user1Transform = 'translateY(0px)';
|
|
2420
|
+
this.user2Transform = 'translateY(0px)';
|
|
2421
|
+
this.alignmentCalculated = false;
|
|
2422
|
+
return;
|
|
2423
|
+
}
|
|
2424
|
+
this.user1Transform = 'translateY(0px)';
|
|
2425
|
+
this.user2Transform = 'translateY(0px)';
|
|
2426
|
+
this.alignmentCalculated = false;
|
|
2427
|
+
this.log('ngOnInit', 'starting compressConfigImagesIfNeeded then fetchComparison');
|
|
1892
2428
|
this.compressConfigImagesIfNeeded().subscribe({
|
|
1893
|
-
next: () =>
|
|
2429
|
+
next: () => {
|
|
2430
|
+
this.log('ngOnInit', 'compressConfigImagesIfNeeded done, calling fetchComparison');
|
|
2431
|
+
this.fetchComparison();
|
|
2432
|
+
},
|
|
1894
2433
|
error: (e) => {
|
|
1895
2434
|
console.warn('Config image compression failed, continuing:', e);
|
|
1896
|
-
this.
|
|
2435
|
+
this.log('ngOnInit', 'compressConfigImagesIfNeeded error, calling fetchComparison anyway');
|
|
2436
|
+
this.fetchComparison();
|
|
1897
2437
|
}
|
|
1898
2438
|
});
|
|
1899
|
-
this.user1Transform = 'translateY(0px)';
|
|
1900
|
-
this.user2Transform = 'translateY(0px)';
|
|
1901
|
-
this.alignmentCalculated = false;
|
|
1902
2439
|
}
|
|
1903
2440
|
ngAfterViewInit() {
|
|
1904
2441
|
this.waitForImagesAndInitDrag();
|
|
1905
2442
|
}
|
|
1906
2443
|
ngOnChanges(changes) {
|
|
1907
2444
|
if (changes['config']) {
|
|
2445
|
+
const first = changes['config'].firstChange;
|
|
2446
|
+
this.log('ngOnChanges', 'config changed', 'firstChange', first, 'backendConfigured', this.backendConfigured);
|
|
1908
2447
|
this.updateConfigProperties();
|
|
2448
|
+
if (!this.backendConfigured) {
|
|
2449
|
+
this.log('ngOnChanges', 'skipping fetch: backend not configured');
|
|
2450
|
+
return;
|
|
2451
|
+
}
|
|
2452
|
+
if (!first) {
|
|
2453
|
+
this.log('ngOnChanges', 'running compressConfigImagesIfNeeded then fetchComparison');
|
|
2454
|
+
this.compressConfigImagesIfNeeded().subscribe({
|
|
2455
|
+
next: () => {
|
|
2456
|
+
this.log('ngOnChanges', 'compress done, calling fetchComparison');
|
|
2457
|
+
this.fetchComparison();
|
|
2458
|
+
},
|
|
2459
|
+
error: (e) => {
|
|
2460
|
+
console.warn('Config image compression failed, continuing:', e);
|
|
2461
|
+
this.log('ngOnChanges', 'compress error, calling fetchComparison anyway');
|
|
2462
|
+
this.fetchComparison();
|
|
2463
|
+
}
|
|
2464
|
+
});
|
|
2465
|
+
}
|
|
2466
|
+
else {
|
|
2467
|
+
this.log('ngOnChanges', 'skipping fetch: firstChange (ngOnInit will run)');
|
|
2468
|
+
}
|
|
1909
2469
|
}
|
|
1910
2470
|
}
|
|
2471
|
+
/** Use for template: hide left/right item when it's a shared (center) item. Compares case-insensitively. */
|
|
2472
|
+
isInCenter(interest) {
|
|
2473
|
+
if (!interest || !this.centerItem?.length)
|
|
2474
|
+
return false;
|
|
2475
|
+
const n = String(interest).trim().toLowerCase();
|
|
2476
|
+
return this.centerItem.some((c) => String(c).trim().toLowerCase() === n);
|
|
2477
|
+
}
|
|
2478
|
+
fetchComparison() {
|
|
2479
|
+
this.log('fetchComparison', 'start');
|
|
2480
|
+
if (this.isAligning) {
|
|
2481
|
+
this.log('fetchComparison', 'skipped: already aligning (single-flight)');
|
|
2482
|
+
return;
|
|
2483
|
+
}
|
|
2484
|
+
if (this.computeSub) {
|
|
2485
|
+
this.computeSub.unsubscribe();
|
|
2486
|
+
this.computeSub = undefined;
|
|
2487
|
+
}
|
|
2488
|
+
const requestId = ++this.fetchRequestId;
|
|
2489
|
+
this.isAligning = true;
|
|
2490
|
+
this.backendError = null;
|
|
2491
|
+
const normalized = this.buildConfigWithDefaults(this.config, this.baseConfig);
|
|
2492
|
+
const configToSend = {
|
|
2493
|
+
person1Interests: normalized.person1Interests,
|
|
2494
|
+
person2Interests: normalized.person2Interests,
|
|
2495
|
+
person3Interests: normalized.person3Interests,
|
|
2496
|
+
user1Image: this.user1Image,
|
|
2497
|
+
user2Image: this.user2Image,
|
|
2498
|
+
};
|
|
2499
|
+
this.log('fetchComparison', 'calling backend getComparison', {
|
|
2500
|
+
person1Len: configToSend.person1Interests.length,
|
|
2501
|
+
person2Len: configToSend.person2Interests.length,
|
|
2502
|
+
user1ImageLen: (configToSend.user1Image || '').length,
|
|
2503
|
+
user2ImageLen: (configToSend.user2Image || '').length,
|
|
2504
|
+
});
|
|
2505
|
+
this.computeSub = this.backendService.getComparison(configToSend).subscribe({
|
|
2506
|
+
next: (payload) => {
|
|
2507
|
+
if (requestId !== this.fetchRequestId) {
|
|
2508
|
+
this.log('fetchComparison', 'ignoring stale success', { requestId, current: this.fetchRequestId });
|
|
2509
|
+
return;
|
|
2510
|
+
}
|
|
2511
|
+
this.log('fetchComparison', 'backend response received', { aligned1: payload?.alignedPerson1Interests?.length, aligned2: payload?.alignedPerson2Interests?.length });
|
|
2512
|
+
this.isAligning = false;
|
|
2513
|
+
this.backendError = null;
|
|
2514
|
+
const a1 = payload.alignedPerson1Interests ?? [];
|
|
2515
|
+
const a2 = payload.alignedPerson2Interests ?? [];
|
|
2516
|
+
const sameLists = a1.length === a2.length && a1.every((v, i) => String(v).trim().toLowerCase() === String(a2[i]).trim().toLowerCase());
|
|
2517
|
+
if (sameLists && a1.length > 0) {
|
|
2518
|
+
this.log('fetchComparison', 'backend returned identical aligned lists; using raw lists to keep sides distinct', { len: a1.length });
|
|
2519
|
+
this.alignedPerson1Interests = [];
|
|
2520
|
+
this.alignedPerson2Interests = [];
|
|
2521
|
+
this.displayPerson1Interests = this.person1Interests;
|
|
2522
|
+
this.displayPerson2Interests = this.person2Interests;
|
|
2523
|
+
this.centerItem = payload.centerItem ?? [];
|
|
2524
|
+
}
|
|
2525
|
+
else {
|
|
2526
|
+
this.alignedPerson1Interests = a1;
|
|
2527
|
+
this.alignedPerson2Interests = a2;
|
|
2528
|
+
this.centerItem = payload.centerItem ?? [];
|
|
2529
|
+
this.displayPerson1Interests = a1.length ? a1 : this.person1Interests;
|
|
2530
|
+
this.displayPerson2Interests = a2.length ? a2 : this.person2Interests;
|
|
2531
|
+
}
|
|
2532
|
+
this.matrixData = payload.matrixData ?? null;
|
|
2533
|
+
if (this.matrixData)
|
|
2534
|
+
this.matrixDataChange.emit(this.matrixData);
|
|
2535
|
+
if (payload.rawLLMOutput != null) {
|
|
2536
|
+
this.rawLLMOutput = payload.rawLLMOutput;
|
|
2537
|
+
this.rawLLMOutputChange.emit(payload.rawLLMOutput);
|
|
2538
|
+
}
|
|
2539
|
+
this.firstImageData = payload.face1 ?? null;
|
|
2540
|
+
this.secondImageData = payload.face2 ?? null;
|
|
2541
|
+
const faces1 = (this.firstImageData && typeof this.firstImageData === 'object' && 'faces' in this.firstImageData && Array.isArray(this.firstImageData.faces)) ? this.firstImageData.faces : [];
|
|
2542
|
+
const faces2 = (this.secondImageData && typeof this.secondImageData === 'object' && 'faces' in this.secondImageData && Array.isArray(this.secondImageData.faces)) ? this.secondImageData.faces : [];
|
|
2543
|
+
this.user1FaceRaw = faces1.length ? faces1[0] : null;
|
|
2544
|
+
this.user2FaceRaw = faces2.length ? faces2[0] : null;
|
|
2545
|
+
if (this.user1NaturalWidth && this.user1NaturalHeight)
|
|
2546
|
+
this.updateObjectPositionForFace(1);
|
|
2547
|
+
if (this.user2NaturalWidth && this.user2NaturalHeight)
|
|
2548
|
+
this.updateObjectPositionForFace(2);
|
|
2549
|
+
if (this.firstImageData && this.secondImageData)
|
|
2550
|
+
this.calculateFaceAlignment(this.firstImageData, this.secondImageData);
|
|
2551
|
+
this.cdr.detectChanges();
|
|
2552
|
+
},
|
|
2553
|
+
error: (err) => {
|
|
2554
|
+
if (requestId !== this.fetchRequestId) {
|
|
2555
|
+
this.log('fetchComparison', 'ignoring stale error', { requestId, current: this.fetchRequestId, status: err?.status });
|
|
2556
|
+
return;
|
|
2557
|
+
}
|
|
2558
|
+
this.log('fetchComparison', 'backend error', err);
|
|
2559
|
+
const msg = err?.error?.message ?? err?.message ?? err?.statusText ?? (typeof err?.error === 'string' ? err.error : null) ?? 'Request failed';
|
|
2560
|
+
this.backendError = msg;
|
|
2561
|
+
console.error('[ProfileComparisonLib] fetchComparison error', err?.status ?? '', msg);
|
|
2562
|
+
this.isAligning = false;
|
|
2563
|
+
this.cdr.detectChanges();
|
|
2564
|
+
}
|
|
2565
|
+
});
|
|
2566
|
+
}
|
|
1911
2567
|
updateConfigProperties() {
|
|
1912
2568
|
const normalized = this.buildConfigWithDefaults(this.config, this.baseConfig);
|
|
1913
2569
|
this.config = normalized;
|
|
@@ -2065,8 +2721,7 @@ class ProfileComparisonLibComponent {
|
|
|
2065
2721
|
}
|
|
2066
2722
|
getProfileContainerSize(which) {
|
|
2067
2723
|
try {
|
|
2068
|
-
const
|
|
2069
|
-
const el = document.querySelector(selector);
|
|
2724
|
+
const el = (which === 1 ? this.profileImgLeft : this.profileImgRight)?.nativeElement || null;
|
|
2070
2725
|
if (!el)
|
|
2071
2726
|
return { width: 0, height: 0 };
|
|
2072
2727
|
const rect = el.getBoundingClientRect();
|
|
@@ -2078,7 +2733,7 @@ class ProfileComparisonLibComponent {
|
|
|
2078
2733
|
}
|
|
2079
2734
|
getOverlapSizeCssPx() {
|
|
2080
2735
|
try {
|
|
2081
|
-
const root =
|
|
2736
|
+
const root = this.profileFlex?.nativeElement || null;
|
|
2082
2737
|
const fallback = 40;
|
|
2083
2738
|
if (!root)
|
|
2084
2739
|
return fallback;
|
|
@@ -2092,7 +2747,7 @@ class ProfileComparisonLibComponent {
|
|
|
2092
2747
|
}
|
|
2093
2748
|
setOverlapSizeCssPx(px) {
|
|
2094
2749
|
try {
|
|
2095
|
-
const root =
|
|
2750
|
+
const root = this.profileFlex?.nativeElement || null;
|
|
2096
2751
|
if (!root)
|
|
2097
2752
|
return;
|
|
2098
2753
|
const clamped = Math.max(8, Math.min(80, Math.round(px)));
|
|
@@ -2101,15 +2756,16 @@ class ProfileComparisonLibComponent {
|
|
|
2101
2756
|
catch { }
|
|
2102
2757
|
}
|
|
2103
2758
|
waitForImagesAndInitDrag() {
|
|
2104
|
-
const shapeBg1 =
|
|
2105
|
-
const shapeBg2 =
|
|
2759
|
+
const shapeBg1 = this.shapeBg1?.nativeElement;
|
|
2760
|
+
const shapeBg2 = this.shapeBg2?.nativeElement;
|
|
2106
2761
|
if (!shapeBg1 || !shapeBg2) {
|
|
2107
2762
|
setTimeout(() => this.waitForImagesAndInitDrag(), 100);
|
|
2108
2763
|
return;
|
|
2109
2764
|
}
|
|
2110
2765
|
const checkImagesLoaded = () => {
|
|
2111
|
-
|
|
2112
|
-
const
|
|
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;
|
|
2113
2769
|
if (img1Loaded && img2Loaded) {
|
|
2114
2770
|
setTimeout(() => this.initDrag(), 50);
|
|
2115
2771
|
}
|
|
@@ -2120,34 +2776,37 @@ class ProfileComparisonLibComponent {
|
|
|
2120
2776
|
checkImagesLoaded();
|
|
2121
2777
|
}
|
|
2122
2778
|
initDrag() {
|
|
2123
|
-
const shapeBg1 =
|
|
2124
|
-
const shapeBg2 =
|
|
2125
|
-
const shapeTextLeft =
|
|
2126
|
-
const shapeTextRight =
|
|
2779
|
+
const shapeBg1 = this.shapeBg1?.nativeElement;
|
|
2780
|
+
const shapeBg2 = this.shapeBg2?.nativeElement;
|
|
2781
|
+
const shapeTextLeft = this.shapeTextLeft?.nativeElement || null;
|
|
2782
|
+
const shapeTextRight = this.shapeTextRight?.nativeElement || null;
|
|
2127
2783
|
if (!shapeBg1 || !shapeBg2 || !shapeTextLeft || !shapeTextRight)
|
|
2128
2784
|
return;
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
const
|
|
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;
|
|
2139
2798
|
let isDragging = false;
|
|
2140
2799
|
let dragStartX = 0;
|
|
2141
2800
|
let initialWidth1 = 0;
|
|
2142
2801
|
let initialWidth2 = 0;
|
|
2143
2802
|
let activeImg = null;
|
|
2144
|
-
|
|
2803
|
+
// Use ViewChild for center text
|
|
2804
|
+
const shapeTextCenter = this.shapeTextCenter?.nativeElement || null;
|
|
2145
2805
|
const onMouseMove = (e) => {
|
|
2146
2806
|
if (!isDragging || !activeImg)
|
|
2147
2807
|
return;
|
|
2148
2808
|
const deltaX = e.clientX - dragStartX;
|
|
2149
|
-
const containerWidth =
|
|
2150
|
-
const centerPosition = containerWidth / 2;
|
|
2809
|
+
const containerWidth = this.shapeContainer?.nativeElement?.clientWidth || 0;
|
|
2151
2810
|
let newWidth1 = initialWidth1;
|
|
2152
2811
|
let newWidth2 = initialWidth2;
|
|
2153
2812
|
if (activeImg === shapeBg1) {
|
|
@@ -2160,43 +2819,51 @@ class ProfileComparisonLibComponent {
|
|
|
2160
2819
|
}
|
|
2161
2820
|
newWidth1 = Math.max(minWidth, Math.min(maxWidth, newWidth1));
|
|
2162
2821
|
newWidth2 = Math.max(minWidth, Math.min(maxWidth, newWidth2));
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2822
|
+
shapeBg1El.style.width = newWidth1 + 'px';
|
|
2823
|
+
shapeBg1El.style.height = ProfileComparisonLibComponent.SHAPE_BG_HEIGHT;
|
|
2824
|
+
shapeBg2El.style.width = newWidth2 + 'px';
|
|
2825
|
+
shapeBg2El.style.height = ProfileComparisonLibComponent.SHAPE_BG_HEIGHT;
|
|
2167
2826
|
const widthChange1 = newWidth1 - originalWidth1;
|
|
2168
2827
|
const widthChange2 = newWidth2 - originalWidth2;
|
|
2169
2828
|
const dragDirection = deltaX > 0 ? 1 : -1;
|
|
2170
2829
|
const dragDistance = Math.abs(deltaX);
|
|
2171
|
-
const maxDragDistance =
|
|
2172
|
-
const centerMoveDistance = Math.min(dragDistance, maxDragDistance) * dragDirection *
|
|
2173
|
-
shapeTextLeft.style.transform = `translateX(${widthChange1 *
|
|
2174
|
-
shapeTextRight.style.transform = `translateX(${-widthChange2 *
|
|
2175
|
-
shapeTextCenter
|
|
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)`;
|
|
2834
|
+
if (shapeTextCenter) {
|
|
2835
|
+
shapeTextCenter.style.transform = `translateX(${centerMoveDistance * ProfileComparisonLibComponent.TEXT_MOVE_MULTIPLIER}px)`;
|
|
2836
|
+
}
|
|
2176
2837
|
};
|
|
2177
2838
|
const resetWidths = () => {
|
|
2178
2839
|
isDragging = false;
|
|
2179
2840
|
activeImg = null;
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
shapeTextLeft.style.transition =
|
|
2183
|
-
shapeTextRight.style.transition =
|
|
2184
|
-
shapeTextCenter
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2841
|
+
shapeBg1El.style.transition = ProfileComparisonLibComponent.TRANSITION_WIDTH;
|
|
2842
|
+
shapeBg2El.style.transition = ProfileComparisonLibComponent.TRANSITION_WIDTH;
|
|
2843
|
+
shapeTextLeft.style.transition = ProfileComparisonLibComponent.TRANSITION_TRANSFORM;
|
|
2844
|
+
shapeTextRight.style.transition = ProfileComparisonLibComponent.TRANSITION_TRANSFORM;
|
|
2845
|
+
if (shapeTextCenter) {
|
|
2846
|
+
shapeTextCenter.style.transition = ProfileComparisonLibComponent.TRANSITION_TRANSFORM;
|
|
2847
|
+
}
|
|
2848
|
+
shapeBg1El.style.width = originalWidth1 + 'px';
|
|
2849
|
+
shapeBg2El.style.width = originalWidth2 + 'px';
|
|
2850
|
+
shapeTextLeft.style.transform = ProfileComparisonLibComponent.TRANSFORM_RESET;
|
|
2851
|
+
shapeTextRight.style.transform = ProfileComparisonLibComponent.TRANSFORM_RESET;
|
|
2852
|
+
if (shapeTextCenter) {
|
|
2853
|
+
shapeTextCenter.style.transform = ProfileComparisonLibComponent.TRANSFORM_RESET;
|
|
2854
|
+
}
|
|
2190
2855
|
document.removeEventListener('mousemove', onMouseMove);
|
|
2191
2856
|
document.removeEventListener('mouseup', resetWidths);
|
|
2192
2857
|
document.removeEventListener('mouseleave', resetWidths);
|
|
2193
2858
|
setTimeout(() => {
|
|
2194
|
-
|
|
2195
|
-
|
|
2859
|
+
shapeBg1El.style.transition = '';
|
|
2860
|
+
shapeBg2El.style.transition = '';
|
|
2196
2861
|
shapeTextLeft.style.transition = '';
|
|
2197
2862
|
shapeTextRight.style.transition = '';
|
|
2198
|
-
shapeTextCenter
|
|
2199
|
-
|
|
2863
|
+
if (shapeTextCenter) {
|
|
2864
|
+
shapeTextCenter.style.transition = '';
|
|
2865
|
+
}
|
|
2866
|
+
}, ProfileComparisonLibComponent.TRANSITION_RESET_DELAY_MS);
|
|
2200
2867
|
};
|
|
2201
2868
|
[shapeBg1, shapeBg2].forEach((img) => {
|
|
2202
2869
|
img.addEventListener('mousedown', (e) => {
|
|
@@ -2204,243 +2871,31 @@ class ProfileComparisonLibComponent {
|
|
|
2204
2871
|
isDragging = true;
|
|
2205
2872
|
activeImg = img;
|
|
2206
2873
|
dragStartX = e.clientX;
|
|
2207
|
-
initialWidth1 = shapeBg1.
|
|
2208
|
-
initialWidth2 = shapeBg2.
|
|
2874
|
+
initialWidth1 = shapeBg1.getBoundingClientRect().width;
|
|
2875
|
+
initialWidth2 = shapeBg2.getBoundingClientRect().width;
|
|
2209
2876
|
document.addEventListener('mousemove', onMouseMove);
|
|
2210
2877
|
document.addEventListener('mouseup', resetWidths);
|
|
2211
2878
|
document.addEventListener('mouseleave', resetWidths);
|
|
2212
2879
|
});
|
|
2213
2880
|
});
|
|
2214
2881
|
}
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
this.computeSub.unsubscribe();
|
|
2218
|
-
this.computeSub = undefined;
|
|
2219
|
-
}
|
|
2220
|
-
const requestEpoch = this.activeRequestEpoch;
|
|
2221
|
-
this.centerItem = [];
|
|
2222
|
-
this.computeSub = forkJoin({
|
|
2223
|
-
a: this.sortByMaxSimilarity(this.person1Interests, [this.person2Interests, this.person3Interests]),
|
|
2224
|
-
b: this.sortByMaxSimilarity(this.person2Interests, [this.person1Interests, this.person3Interests]),
|
|
2225
|
-
c: this.sortByMaxSimilarity(this.person3Interests, [this.person1Interests, this.person2Interests]),
|
|
2226
|
-
}).subscribe((result) => {
|
|
2227
|
-
this.sortedA = result.a;
|
|
2228
|
-
this.sortedB = result.b;
|
|
2229
|
-
this.sortedC = result.c;
|
|
2230
|
-
this.displayPerson1Interests = result.a.map((item) => item.item);
|
|
2231
|
-
this.displayPerson2Interests = result.b.map((item) => item.item);
|
|
2232
|
-
this.isAligning = true;
|
|
2233
|
-
this.createEmbeddingBasedAlignment(result.a, result.b)
|
|
2234
|
-
.then(() => {
|
|
2235
|
-
this.isAligning = false;
|
|
2236
|
-
this.generateSimilarityMatrix();
|
|
2237
|
-
})
|
|
2238
|
-
.catch((error) => {
|
|
2239
|
-
console.warn('Embedding alignment failed, falling back to semantic:', error);
|
|
2240
|
-
this.createSemanticAlignment(result.a, result.b).then(() => {
|
|
2241
|
-
this.isAligning = false;
|
|
2242
|
-
this.generateSimilarityMatrix();
|
|
2243
|
-
});
|
|
2244
|
-
});
|
|
2245
|
-
});
|
|
2246
|
-
}
|
|
2247
|
-
async createSemanticAlignment(person1Results, person2Results) {
|
|
2248
|
-
this.alignedPerson1Interests = person1Results.map((item) => item.item);
|
|
2249
|
-
this.alignedPerson2Interests = person2Results.map((item) => item.item);
|
|
2250
|
-
const maxLength = Math.max(this.alignedPerson1Interests.length, this.alignedPerson2Interests.length);
|
|
2251
|
-
while (this.alignedPerson1Interests.length < maxLength)
|
|
2252
|
-
this.alignedPerson1Interests.push(ProfileComparisonLibComponent.SPACER_SMALL);
|
|
2253
|
-
while (this.alignedPerson2Interests.length < maxLength)
|
|
2254
|
-
this.alignedPerson2Interests.push(ProfileComparisonLibComponent.SPACER_SMALL);
|
|
2882
|
+
onViewProfile(side) {
|
|
2883
|
+
this.viewProfileClick.emit({ side });
|
|
2255
2884
|
}
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
.getAlignedLists(baseLeft, baseRight, this.getActiveOpenAIKey())
|
|
2262
|
-
.subscribe({
|
|
2263
|
-
next: (result) => {
|
|
2264
|
-
if (result) {
|
|
2265
|
-
let listA = result.listA;
|
|
2266
|
-
let listB = result.listB;
|
|
2267
|
-
const minLength = Math.min(listA.length, listB.length);
|
|
2268
|
-
for (let i = 0; i < minLength; i++) {
|
|
2269
|
-
const itemA = listA[i];
|
|
2270
|
-
const itemB = listB[i];
|
|
2271
|
-
if (!itemA || !itemB || itemA === ProfileComparisonLibComponent.SPACER_SMALL || itemA === ProfileComparisonLibComponent.SPACER_LARGE || itemB === ProfileComparisonLibComponent.SPACER_SMALL || itemB === ProfileComparisonLibComponent.SPACER_LARGE)
|
|
2272
|
-
continue;
|
|
2273
|
-
const normA = itemA.trim().toLowerCase();
|
|
2274
|
-
const normB = itemB.trim().toLowerCase();
|
|
2275
|
-
let isMatch = normA === normB;
|
|
2276
|
-
if (!isMatch && (normA.includes(ProfileComparisonLibComponent.CONSOLIDATION_SEPARATOR_X) || normA.includes(ProfileComparisonLibComponent.CONSOLIDATION_SEPARATOR_SLASH)) && (normB.includes(ProfileComparisonLibComponent.CONSOLIDATION_SEPARATOR_X) || normB.includes(ProfileComparisonLibComponent.CONSOLIDATION_SEPARATOR_SLASH))) {
|
|
2277
|
-
const separatorRegex = new RegExp(`[${ProfileComparisonLibComponent.CONSOLIDATION_SEPARATOR_X}${ProfileComparisonLibComponent.CONSOLIDATION_SEPARATOR_SLASH}]`);
|
|
2278
|
-
const partsA = normA.split(separatorRegex).map(p => p.trim()).sort();
|
|
2279
|
-
const partsB = normB.split(separatorRegex).map(p => p.trim()).sort();
|
|
2280
|
-
if (partsA.length === partsB.length && partsA.every((val, index) => val === partsB[index]))
|
|
2281
|
-
isMatch = true;
|
|
2282
|
-
}
|
|
2283
|
-
if (isMatch) {
|
|
2284
|
-
const existing = this.centerItem.find(item => item.trim().toLowerCase() === normA);
|
|
2285
|
-
if (!existing)
|
|
2286
|
-
this.centerItem.push(itemA);
|
|
2287
|
-
listA[i] = ProfileComparisonLibComponent.SPACER_SMALL;
|
|
2288
|
-
listB[i] = ProfileComparisonLibComponent.SPACER_SMALL;
|
|
2289
|
-
}
|
|
2290
|
-
}
|
|
2291
|
-
const cleanListA = [];
|
|
2292
|
-
const cleanListB = [];
|
|
2293
|
-
for (let i = 0; i < listA.length; i++) {
|
|
2294
|
-
const itemA = listA[i];
|
|
2295
|
-
const itemB = listB[i];
|
|
2296
|
-
const isSpacerRow = (!itemA || itemA === ProfileComparisonLibComponent.SPACER_SMALL || itemA === ProfileComparisonLibComponent.SPACER_LARGE) && (!itemB || itemB === ProfileComparisonLibComponent.SPACER_SMALL || itemB === ProfileComparisonLibComponent.SPACER_LARGE);
|
|
2297
|
-
if (isSpacerRow) {
|
|
2298
|
-
const lastIndex = cleanListA.length - 1;
|
|
2299
|
-
if (lastIndex >= 0) {
|
|
2300
|
-
const lastA = cleanListA[lastIndex];
|
|
2301
|
-
const lastB = cleanListB[lastIndex];
|
|
2302
|
-
const lastWasSpacer = (!lastA || lastA === ProfileComparisonLibComponent.SPACER_SMALL || lastA === ProfileComparisonLibComponent.SPACER_LARGE) && (!lastB || lastB === ProfileComparisonLibComponent.SPACER_SMALL || lastB === ProfileComparisonLibComponent.SPACER_LARGE);
|
|
2303
|
-
if (lastWasSpacer) {
|
|
2304
|
-
if (itemA === ProfileComparisonLibComponent.SPACER_LARGE || itemB === ProfileComparisonLibComponent.SPACER_LARGE || lastA === ProfileComparisonLibComponent.SPACER_LARGE || lastB === ProfileComparisonLibComponent.SPACER_LARGE) {
|
|
2305
|
-
cleanListA[lastIndex] = ProfileComparisonLibComponent.SPACER_LARGE;
|
|
2306
|
-
cleanListB[lastIndex] = ProfileComparisonLibComponent.SPACER_LARGE;
|
|
2307
|
-
}
|
|
2308
|
-
continue;
|
|
2309
|
-
}
|
|
2310
|
-
}
|
|
2311
|
-
}
|
|
2312
|
-
cleanListA.push(itemA);
|
|
2313
|
-
cleanListB.push(itemB);
|
|
2314
|
-
}
|
|
2315
|
-
if (cleanListA.length > 0) {
|
|
2316
|
-
const firstA = cleanListA[0];
|
|
2317
|
-
const firstB = cleanListB[0];
|
|
2318
|
-
if ((!firstA || firstA === ProfileComparisonLibComponent.SPACER_SMALL || firstA === ProfileComparisonLibComponent.SPACER_LARGE) && (!firstB || firstB === ProfileComparisonLibComponent.SPACER_SMALL || firstB === ProfileComparisonLibComponent.SPACER_LARGE)) {
|
|
2319
|
-
cleanListA.shift();
|
|
2320
|
-
cleanListB.shift();
|
|
2321
|
-
}
|
|
2322
|
-
}
|
|
2323
|
-
if (cleanListA.length > 0) {
|
|
2324
|
-
const lastIndex = cleanListA.length - 1;
|
|
2325
|
-
const lastA = cleanListA[lastIndex];
|
|
2326
|
-
const lastB = cleanListB[lastIndex];
|
|
2327
|
-
if ((!lastA || lastA === ProfileComparisonLibComponent.SPACER_SMALL || lastA === ProfileComparisonLibComponent.SPACER_LARGE) && (!lastB || lastB === ProfileComparisonLibComponent.SPACER_SMALL || lastB === ProfileComparisonLibComponent.SPACER_LARGE)) {
|
|
2328
|
-
cleanListA.pop();
|
|
2329
|
-
cleanListB.pop();
|
|
2330
|
-
}
|
|
2331
|
-
}
|
|
2332
|
-
this.alignedPerson1Interests = cleanListA;
|
|
2333
|
-
this.alignedPerson2Interests = cleanListB;
|
|
2334
|
-
}
|
|
2335
|
-
else {
|
|
2336
|
-
this.createSemanticAlignment(person1Results, person2Results);
|
|
2337
|
-
}
|
|
2338
|
-
},
|
|
2339
|
-
error: (error) => {
|
|
2340
|
-
this.createSemanticAlignment(person1Results, person2Results);
|
|
2341
|
-
}
|
|
2342
|
-
});
|
|
2885
|
+
compressConfigImagesIfNeeded() {
|
|
2886
|
+
const tasks = [];
|
|
2887
|
+
if (this.user1Image) {
|
|
2888
|
+
tasks.push(this.compressImageStringToDataUrl(this.user1Image, 'ConfigUser1').pipe(tap$1((dataUrl) => { if (dataUrl)
|
|
2889
|
+
this.user1Image = dataUrl; }), map$1(() => void 0)));
|
|
2343
2890
|
}
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2891
|
+
if (this.user2Image) {
|
|
2892
|
+
tasks.push(this.compressImageStringToDataUrl(this.user2Image, 'ConfigUser2').pipe(tap$1((dataUrl) => { if (dataUrl)
|
|
2893
|
+
this.user2Image = dataUrl; }), map$1(() => void 0)));
|
|
2347
2894
|
}
|
|
2895
|
+
return tasks.length > 0 ? forkJoin(tasks).pipe(map$1(() => void 0)) : of(void 0);
|
|
2348
2896
|
}
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
const others = otherLists.flat().filter(o => !!o && o.trim() !== ProfileComparisonLibComponent.SPACER_SMALL && o.trim().length > 0);
|
|
2352
|
-
const validPersonList = personList.filter(i => !!i && i.trim() !== ProfileComparisonLibComponent.SPACER_SMALL && i.trim().length > 0);
|
|
2353
|
-
const CONCURRENCY = 4;
|
|
2354
|
-
return from(validPersonList).pipe(concatMap((item) => from(others).pipe(mergeMap((o) => this.profileService.compareInterests(item, o, this.getActiveApiKey()).pipe(map$1((res) => res.similarity), catchError$1((error) => {
|
|
2355
|
-
if (requestEpoch === this.activeRequestEpoch)
|
|
2356
|
-
this.handleApiQuotaError(error);
|
|
2357
|
-
return of(0);
|
|
2358
|
-
})), CONCURRENCY), toArray(), map$1((scores) => {
|
|
2359
|
-
const maxScore = scores.length ? Math.max(...scores) : 0;
|
|
2360
|
-
const itemNorm = item.trim().toLowerCase();
|
|
2361
|
-
const hasExact = others.some(o => o && o.trim().toLowerCase() === itemNorm);
|
|
2362
|
-
if (hasExact) {
|
|
2363
|
-
const existing = this.centerItem.find(i => i.trim().toLowerCase() === itemNorm);
|
|
2364
|
-
if (!existing)
|
|
2365
|
-
this.centerItem.push(item);
|
|
2366
|
-
}
|
|
2367
|
-
return { item, score: maxScore };
|
|
2368
|
-
}))), toArray(), map$1((results) => results.sort((a, b) => b.score - a.score)));
|
|
2369
|
-
}
|
|
2370
|
-
onViewProfile() {
|
|
2371
|
-
alert('You have been routed to a profile page ');
|
|
2372
|
-
}
|
|
2373
|
-
getActiveApiKey() {
|
|
2374
|
-
return this.customApiKey || this.apiNinjasKey;
|
|
2375
|
-
}
|
|
2376
|
-
updateApiKey() {
|
|
2377
|
-
if (this.apiKeyInputValue.trim()) {
|
|
2378
|
-
this.customApiKey = this.apiKeyInputValue.trim();
|
|
2379
|
-
this.saveApiKeyToStorage(this.customApiKey);
|
|
2380
|
-
this.showApiKeyModal = false;
|
|
2381
|
-
this.quotaExhausted = false;
|
|
2382
|
-
this.activeRequestEpoch++;
|
|
2383
|
-
this.compute();
|
|
2384
|
-
this.initializeFaceDetection();
|
|
2385
|
-
}
|
|
2386
|
-
}
|
|
2387
|
-
closeApiKeyModal() {
|
|
2388
|
-
this.showApiKeyModal = false;
|
|
2389
|
-
}
|
|
2390
|
-
handleApiQuotaError(error) {
|
|
2391
|
-
if (error?.status === 400 || error?.status === 429 || error?.status === 403 || error?.error?.message?.toLowerCase().includes('quota')) {
|
|
2392
|
-
this.quotaExhausted = true;
|
|
2393
|
-
this.showApiKeyModal = true;
|
|
2394
|
-
}
|
|
2395
|
-
}
|
|
2396
|
-
saveApiKeyToStorage(apiKey) {
|
|
2397
|
-
try {
|
|
2398
|
-
localStorage.setItem('apiNinjasCustomKey', apiKey);
|
|
2399
|
-
}
|
|
2400
|
-
catch { }
|
|
2401
|
-
}
|
|
2402
|
-
loadApiKeyFromStorage() {
|
|
2403
|
-
try {
|
|
2404
|
-
const savedKey = localStorage.getItem('apiNinjasCustomKey');
|
|
2405
|
-
if (savedKey) {
|
|
2406
|
-
this.customApiKey = savedKey;
|
|
2407
|
-
this.apiKeyInputValue = savedKey;
|
|
2408
|
-
}
|
|
2409
|
-
}
|
|
2410
|
-
catch { }
|
|
2411
|
-
}
|
|
2412
|
-
onFileInputChange(event) {
|
|
2413
|
-
const files = event.target.files;
|
|
2414
|
-
this.selectedFile = files && files.length > 0 ? files[0] : null;
|
|
2415
|
-
}
|
|
2416
|
-
compressConfigImagesIfNeeded() {
|
|
2417
|
-
const tasks = [];
|
|
2418
|
-
if (this.user1Image) {
|
|
2419
|
-
tasks.push(this.compressImageStringToDataUrl(this.user1Image, 'ConfigUser1').pipe(tap((dataUrl) => { if (dataUrl)
|
|
2420
|
-
this.user1Image = dataUrl; }), map$1(() => void 0)));
|
|
2421
|
-
}
|
|
2422
|
-
if (this.user2Image) {
|
|
2423
|
-
tasks.push(this.compressImageStringToDataUrl(this.user2Image, 'ConfigUser2').pipe(tap((dataUrl) => { if (dataUrl)
|
|
2424
|
-
this.user2Image = dataUrl; }), map$1(() => void 0)));
|
|
2425
|
-
}
|
|
2426
|
-
return tasks.length > 0 ? forkJoin(tasks).pipe(map$1(() => void 0)) : of(void 0);
|
|
2427
|
-
}
|
|
2428
|
-
compressImageStringToDataUrl(imageStr, baseName) {
|
|
2429
|
-
return this.fileConversionService.getFileForImageString(imageStr, `${baseName}.jpg`).pipe(switchMap$1(file => this.imageCompressionService.compressImageFile(file, this.compressionConfig)), map$1(res => res.dataUrl), catchError$1(() => of(null)));
|
|
2430
|
-
}
|
|
2431
|
-
loadCustomConfig() {
|
|
2432
|
-
try {
|
|
2433
|
-
const raw = localStorage.getItem('customProfileConfig');
|
|
2434
|
-
if (!raw) {
|
|
2435
|
-
this.config = this.baseConfig || this.config;
|
|
2436
|
-
return;
|
|
2437
|
-
}
|
|
2438
|
-
const parsed = JSON.parse(raw);
|
|
2439
|
-
this.config = this.buildConfigWithDefaults(parsed, this.baseConfig);
|
|
2440
|
-
}
|
|
2441
|
-
catch {
|
|
2442
|
-
this.config = this.baseConfig || this.config;
|
|
2443
|
-
}
|
|
2897
|
+
compressImageStringToDataUrl(imageStr, baseName) {
|
|
2898
|
+
return this.fileConversionService.getFileForImageString(imageStr, `${baseName}.jpg`).pipe(switchMap$1(file => this.imageCompressionService.compressImageFile(file, this.compressionConfig)), map$1(res => res.dataUrl), catchError$1(() => of(null)));
|
|
2444
2899
|
}
|
|
2445
2900
|
getEyeCoordinatesFromBBox(bbox) {
|
|
2446
2901
|
const eyeY = bbox.y + bbox.height * 0.38;
|
|
@@ -2498,60 +2953,6 @@ class ProfileComparisonLibComponent {
|
|
|
2498
2953
|
faceRight: { x: bbox.x + (3 * bbox.width) / 4, y: bbox.y + bbox.height / 2 },
|
|
2499
2954
|
};
|
|
2500
2955
|
}
|
|
2501
|
-
initializeFaceDetection() {
|
|
2502
|
-
if (this.detectFaceSub1)
|
|
2503
|
-
this.detectFaceSub1.unsubscribe();
|
|
2504
|
-
if (this.detectFaceSub2)
|
|
2505
|
-
this.detectFaceSub2.unsubscribe();
|
|
2506
|
-
let user1FaceData = null;
|
|
2507
|
-
let user2FaceData = null;
|
|
2508
|
-
const shouldDelayUser2 = !!this.user1Image && !!this.user2Image;
|
|
2509
|
-
const FACEPP_RATE_LIMIT_DELAY_MS = ProfileComparisonLibComponent.FACEPP_RATE_LIMIT_DELAY_MS;
|
|
2510
|
-
if (this.user1Image) {
|
|
2511
|
-
this.fileConversionService.getFileForImageString(this.user1Image, ProfileComparisonLibComponent.DEFAULT_PROFILE_IMAGE_NAME).subscribe((file) => {
|
|
2512
|
-
this.detectFaceSub1 = this.profileService.detectFace(file, { apiKey: this.getActiveFaceppKey(), apiSecret: this.getActiveFaceppSecret() }).subscribe({
|
|
2513
|
-
next: (res) => {
|
|
2514
|
-
this.firstImageData = res;
|
|
2515
|
-
user1FaceData = res;
|
|
2516
|
-
try {
|
|
2517
|
-
const faces = res?.faces || [];
|
|
2518
|
-
this.user1FaceRaw = Array.isArray(faces) && faces.length > 0 ? faces[0] : null;
|
|
2519
|
-
this.updateObjectPositionForFace(1);
|
|
2520
|
-
}
|
|
2521
|
-
catch { }
|
|
2522
|
-
if (user1FaceData && user2FaceData)
|
|
2523
|
-
this.calculateFaceAlignment(user1FaceData, user2FaceData);
|
|
2524
|
-
},
|
|
2525
|
-
error: (err) => this.handleFaceppError(err),
|
|
2526
|
-
});
|
|
2527
|
-
});
|
|
2528
|
-
}
|
|
2529
|
-
if (this.user2Image) {
|
|
2530
|
-
this.fileConversionService.getFileForImageString(this.user2Image, 'ProfilePg2.png').subscribe((file) => {
|
|
2531
|
-
const startUser2Detect = () => {
|
|
2532
|
-
this.detectFaceSub2 = this.profileService.detectFace(file, { apiKey: this.getActiveFaceppKey(), apiSecret: this.getActiveFaceppSecret() }).subscribe({
|
|
2533
|
-
next: (res) => {
|
|
2534
|
-
this.secondImageData = res;
|
|
2535
|
-
user2FaceData = res;
|
|
2536
|
-
try {
|
|
2537
|
-
const faces = res?.faces || [];
|
|
2538
|
-
this.user2FaceRaw = Array.isArray(faces) && faces.length > 0 ? faces[0] : null;
|
|
2539
|
-
this.updateObjectPositionForFace(2);
|
|
2540
|
-
}
|
|
2541
|
-
catch { }
|
|
2542
|
-
if (user1FaceData && user2FaceData)
|
|
2543
|
-
this.calculateFaceAlignment(user1FaceData, user2FaceData);
|
|
2544
|
-
},
|
|
2545
|
-
error: (err) => this.handleFaceppError(err),
|
|
2546
|
-
});
|
|
2547
|
-
};
|
|
2548
|
-
if (shouldDelayUser2)
|
|
2549
|
-
setTimeout(startUser2Detect, FACEPP_RATE_LIMIT_DELAY_MS);
|
|
2550
|
-
else
|
|
2551
|
-
startUser2Detect();
|
|
2552
|
-
});
|
|
2553
|
-
}
|
|
2554
|
-
}
|
|
2555
2956
|
calculateFaceAlignment(user1Data, user2Data) {
|
|
2556
2957
|
let faces1 = [];
|
|
2557
2958
|
let faces2 = [];
|
|
@@ -2578,8 +2979,8 @@ class ProfileComparisonLibComponent {
|
|
|
2578
2979
|
const user2FaceCoords = this.getFaceCoordinatesFromBBox(face2);
|
|
2579
2980
|
const user1Eyes = this.getEyeCoordinatesFromFace(face1);
|
|
2580
2981
|
const user2Eyes = this.getEyeCoordinatesFromFace(face2);
|
|
2581
|
-
const leftContainerEl =
|
|
2582
|
-
const rightContainerEl =
|
|
2982
|
+
const leftContainerEl = this.profileImgLeft?.nativeElement || null;
|
|
2983
|
+
const rightContainerEl = this.profileImgRight?.nativeElement || null;
|
|
2583
2984
|
const containerWidth1 = leftContainerEl?.clientWidth || 160;
|
|
2584
2985
|
const containerHeight1 = leftContainerEl?.clientHeight || 550;
|
|
2585
2986
|
const containerWidth2 = rightContainerEl?.clientWidth || 160;
|
|
@@ -2616,646 +3017,63 @@ class ProfileComparisonLibComponent {
|
|
|
2616
3017
|
isValidFaceData(face) {
|
|
2617
3018
|
return !!face && typeof face.x === 'number' && face.width > 0 && face.height > 0;
|
|
2618
3019
|
}
|
|
2619
|
-
|
|
2620
|
-
getActiveFaceppSecret() { return this.customFaceppSecret || this.faceplusSecret; }
|
|
2621
|
-
updateFaceppCredentials() {
|
|
2622
|
-
const newKey = (this.faceppKeyInputValue || '').trim();
|
|
2623
|
-
const newSecret = (this.faceppSecretInputValue || '').trim();
|
|
2624
|
-
if (!newKey || !newSecret)
|
|
2625
|
-
return;
|
|
2626
|
-
if (this.detectFaceSub1)
|
|
2627
|
-
this.detectFaceSub1.unsubscribe();
|
|
2628
|
-
if (this.detectFaceSub2)
|
|
2629
|
-
this.detectFaceSub2.unsubscribe();
|
|
2630
|
-
this.customFaceppKey = newKey;
|
|
2631
|
-
this.customFaceppSecret = newSecret;
|
|
2632
|
-
this.saveFaceppCredsToStorage(newKey, newSecret);
|
|
2633
|
-
this.showFaceppKeyModal = false;
|
|
2634
|
-
this.faceppQuotaExhausted = false;
|
|
2635
|
-
this.activeRequestEpoch++;
|
|
2636
|
-
this.initializeFaceDetection();
|
|
2637
|
-
}
|
|
2638
|
-
closeFaceppKeyModal() { this.showFaceppKeyModal = false; }
|
|
2639
|
-
handleFaceppError(error) {
|
|
2640
|
-
this.faceppQuotaExhausted = true;
|
|
2641
|
-
this.showFaceppKeyModal = true;
|
|
2642
|
-
}
|
|
2643
|
-
saveFaceppCredsToStorage(apiKey, apiSecret) {
|
|
2644
|
-
try {
|
|
2645
|
-
localStorage.setItem('faceppApiKey', apiKey);
|
|
2646
|
-
localStorage.setItem('faceppApiSecret', apiSecret);
|
|
2647
|
-
}
|
|
2648
|
-
catch { }
|
|
2649
|
-
}
|
|
2650
|
-
loadFaceppCredsFromStorage() {
|
|
2651
|
-
try {
|
|
2652
|
-
const key = localStorage.getItem('faceppApiKey');
|
|
2653
|
-
const secret = localStorage.getItem('faceppApiSecret');
|
|
2654
|
-
if (key) {
|
|
2655
|
-
this.customFaceppKey = key;
|
|
2656
|
-
this.faceppKeyInputValue = key;
|
|
2657
|
-
}
|
|
2658
|
-
if (secret) {
|
|
2659
|
-
this.customFaceppSecret = secret;
|
|
2660
|
-
this.faceppSecretInputValue = secret;
|
|
2661
|
-
}
|
|
2662
|
-
}
|
|
2663
|
-
catch { }
|
|
2664
|
-
}
|
|
2665
|
-
getActiveOpenAIKey() { return this.customOpenAIKey || this.openaiApiKey; }
|
|
2666
|
-
updateOpenAIKey() {
|
|
2667
|
-
if (this.openaiKeyInputValue.trim()) {
|
|
2668
|
-
this.customOpenAIKey = this.openaiKeyInputValue.trim();
|
|
2669
|
-
this.saveOpenAIKeyToStorage(this.customOpenAIKey);
|
|
2670
|
-
this.showOpenAIKeyModal = false;
|
|
2671
|
-
this.openaiQuotaExhausted = false;
|
|
2672
|
-
this.openaiEmbeddingService.clearCache();
|
|
2673
|
-
this.compute();
|
|
2674
|
-
}
|
|
2675
|
-
}
|
|
2676
|
-
closeOpenAIKeyModal() { this.showOpenAIKeyModal = false; }
|
|
2677
|
-
saveOpenAIKeyToStorage(apiKey) {
|
|
2678
|
-
try {
|
|
2679
|
-
localStorage.setItem('openaiCustomKey', apiKey);
|
|
2680
|
-
}
|
|
2681
|
-
catch { }
|
|
2682
|
-
}
|
|
2683
|
-
loadOpenAIKeyFromStorage() {
|
|
2684
|
-
try {
|
|
2685
|
-
const savedKey = localStorage.getItem('openaiCustomKey');
|
|
2686
|
-
if (savedKey) {
|
|
2687
|
-
this.customOpenAIKey = savedKey;
|
|
2688
|
-
this.openaiKeyInputValue = savedKey;
|
|
2689
|
-
}
|
|
2690
|
-
}
|
|
2691
|
-
catch { }
|
|
2692
|
-
}
|
|
2693
|
-
async generateSimilarityMatrix() {
|
|
2694
|
-
const allInterests = Array.from(new Set([...this.person1Interests, ...this.person2Interests])).filter(i => i && i.trim() && i.trim() !== '-');
|
|
2695
|
-
if (!allInterests.length) {
|
|
2696
|
-
this.matrixData = null;
|
|
2697
|
-
return;
|
|
2698
|
-
}
|
|
2699
|
-
try {
|
|
2700
|
-
const apiKey = this.getActiveOpenAIKey();
|
|
2701
|
-
if (!apiKey)
|
|
2702
|
-
return;
|
|
2703
|
-
this.openaiEmbeddingService.getEmbeddings(allInterests, apiKey).subscribe({
|
|
2704
|
-
next: (embeddings) => {
|
|
2705
|
-
if (!embeddings?.length)
|
|
2706
|
-
return;
|
|
2707
|
-
const abbreviations = {};
|
|
2708
|
-
allInterests.forEach(interest => {
|
|
2709
|
-
const words = interest.split(/[\s/-]+/);
|
|
2710
|
-
let abbr = words.length === 1 ? words[0].substring(0, 3) : words.map(w => w[0]).join('').substring(0, 3);
|
|
2711
|
-
let finalAbbr = abbr;
|
|
2712
|
-
let counter = 1;
|
|
2713
|
-
while (Object.values(abbreviations).includes(finalAbbr))
|
|
2714
|
-
finalAbbr = abbr + counter++;
|
|
2715
|
-
abbreviations[interest] = finalAbbr;
|
|
2716
|
-
});
|
|
2717
|
-
const legend = Object.entries(abbreviations).map(([full, abbr]) => ({ abbr, full }));
|
|
2718
|
-
const headers = allInterests.map(i => abbreviations[i]);
|
|
2719
|
-
const rows = allInterests.map((interest, i) => ({
|
|
2720
|
-
label: abbreviations[interest],
|
|
2721
|
-
values: allInterests.map((_, j) => this.cosineSimilarity(embeddings[i], embeddings[j]).toFixed(2))
|
|
2722
|
-
}));
|
|
2723
|
-
this.matrixData = { legend, headers, rows };
|
|
2724
|
-
this.matrixDataChange.emit(this.matrixData);
|
|
2725
|
-
}
|
|
2726
|
-
});
|
|
2727
|
-
}
|
|
2728
|
-
catch { }
|
|
2729
|
-
}
|
|
2730
|
-
cosineSimilarity(a, b) {
|
|
2731
|
-
let dot = 0, na = 0, nb = 0;
|
|
2732
|
-
for (let i = 0; i < a.length; i++) {
|
|
2733
|
-
dot += a[i] * b[i];
|
|
2734
|
-
na += a[i] * a[i];
|
|
2735
|
-
nb += b[i] * b[i];
|
|
2736
|
-
}
|
|
2737
|
-
return (na === 0 || nb === 0) ? 0 : dot / (Math.sqrt(na) * Math.sqrt(nb));
|
|
2738
|
-
}
|
|
2739
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: ProfileComparisonLibComponent, deps: [{ token: ProfileService }, { token: OpenAIEmbeddingService }, { token: i0.Renderer2 }, { token: FileConversionService }, { token: ImageCompressionService }], target: i0.ɵɵFactoryTarget.Component });
|
|
2740
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.16", type: ProfileComparisonLibComponent, isStandalone: false, selector: "lib-profile-comparison", inputs: { config: "config", apiNinjasKey: "apiNinjasKey", faceplusKey: "faceplusKey", faceplusSecret: "faceplusSecret", openaiApiKey: "openaiApiKey", fadeAllEdges: "fadeAllEdges" }, outputs: { matrixDataChange: "matrixDataChange" }, viewQueries: [{ propertyName: "leftContainer", first: true, predicate: ["leftContainer"], descendants: true }, { propertyName: "rightContainer", first: true, predicate: ["rightContainer"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "<!-- API Quota Exhausted Modal (Independent) -->\r\n<div class=\"api-key-modal-overlay\" *ngIf=\"showApiKeyModal\">\r\n <div class=\"api-key-modal\" (click)=\"$event.stopPropagation()\">\r\n <div class=\"api-key-modal-header\">\r\n <div class=\"warning-icon\">\r\n <svg\r\n xmlns=\"http://www.w3.org/2000/svg\"\r\n width=\"48\"\r\n height=\"48\"\r\n viewBox=\"0 0 24 24\"\r\n fill=\"none\"\r\n stroke=\"currentColor\"\r\n stroke-width=\"2\"\r\n stroke-linecap=\"round\"\r\n stroke-linejoin=\"round\"\r\n >\r\n <path\r\n d=\"M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z\"\r\n ></path>\r\n <line x1=\"12\" y1=\"9\" x2=\"12\" y2=\"13\"></line>\r\n <line x1=\"12\" y1=\"17\" x2=\"12.01\" y2=\"17\"></line>\r\n </svg>\r\n </div>\r\n <h2>API Quota Limit Exceeded</h2>\r\n <button class=\"close-btn\" (click)=\"closeApiKeyModal()\" aria-label=\"Close\">\r\n <svg\r\n xmlns=\"http://www.w3.org/2000/svg\"\r\n width=\"24\"\r\n height=\"24\"\r\n viewBox=\"0 0 24 24\"\r\n fill=\"none\"\r\n stroke=\"currentColor\"\r\n stroke-width=\"2\"\r\n stroke-linecap=\"round\"\r\n stroke-linejoin=\"round\"\r\n >\r\n <line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\"></line>\r\n <line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\"></line>\r\n </svg>\r\n </button>\r\n </div>\r\n <!-- Face++ modal moved outside to top-level sibling overlay -->\r\n\r\n <div class=\"api-key-modal-body\">\r\n <p class=\"modal-message\">\r\n The default API Ninjas key has reached its free monthly quota limit of\r\n <strong>3,000 API calls</strong>.\r\n </p>\r\n <p class=\"modal-instruction\">\r\n Please enter your own API Ninjas key below to continue using the profile\r\n comparison features:\r\n </p>\r\n\r\n <div class=\"api-key-input-group\">\r\n <input\r\n type=\"text\"\r\n [(ngModel)]=\"apiKeyInputValue\"\r\n placeholder=\"Paste your API Ninjas key here\"\r\n class=\"api-key-input\"\r\n autofocus\r\n />\r\n <button class=\"api-key-load-btn\" (click)=\"updateApiKey()\">\r\n Apply Key\r\n </button>\r\n </div>\r\n\r\n <div class=\"modal-hint\">\r\n <p>\r\n <svg\r\n xmlns=\"http://www.w3.org/2000/svg\"\r\n width=\"16\"\r\n height=\"16\"\r\n viewBox=\"0 0 24 24\"\r\n fill=\"none\"\r\n stroke=\"currentColor\"\r\n stroke-width=\"2\"\r\n stroke-linecap=\"round\"\r\n stroke-linejoin=\"round\"\r\n >\r\n <circle cx=\"12\" cy=\"12\" r=\"10\"></circle>\r\n <line x1=\"12\" y1=\"16\" x2=\"12\" y2=\"12\"></line>\r\n <line x1=\"12\" y1=\"8\" x2=\"12.01\" y2=\"8\"></line>\r\n </svg>\r\n Don't have an API key? Get your free key at\r\n <a href=\"https://api-ninjas.com/\" target=\"_blank\" rel=\"noopener\"\r\n >api-ninjas.com</a\r\n >\r\n </p>\r\n </div>\r\n </div>\r\n </div>\r\n</div>\r\n\r\n<!-- Face++ API Credentials Modal (Independent) -->\r\n<div class=\"api-key-modal-overlay facepp-overlay\" *ngIf=\"showFaceppKeyModal\">\r\n <div class=\"api-key-modal\" (click)=\"$event.stopPropagation()\">\r\n <div class=\"api-key-modal-header\">\r\n <div class=\"warning-icon\">\r\n <svg\r\n xmlns=\"http://www.w3.org/2000/svg\"\r\n width=\"48\"\r\n height=\"48\"\r\n viewBox=\"0 0 24 24\"\r\n fill=\"none\"\r\n stroke=\"currentColor\"\r\n stroke-width=\"2\"\r\n stroke-linecap=\"round\"\r\n stroke-linejoin=\"round\"\r\n >\r\n <path\r\n d=\"M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z\"\r\n ></path>\r\n <line x1=\"12\" y1=\"9\" x2=\"12\" y2=\"13\"></line>\r\n <line x1=\"12\" y1=\"17\" x2=\"12.01\" y2=\"17\"></line>\r\n </svg>\r\n </div>\r\n <h2>Face++ Credentials Required</h2>\r\n <button\r\n class=\"close-btn\"\r\n (click)=\"closeFaceppKeyModal()\"\r\n aria-label=\"Close\"\r\n >\r\n <svg\r\n xmlns=\"http://www.w3.org/2000/svg\"\r\n width=\"24\"\r\n height=\"24\"\r\n viewBox=\"0 0 24 24\"\r\n fill=\"none\"\r\n stroke=\"currentColor\"\r\n stroke-width=\"2\"\r\n stroke-linecap=\"round\"\r\n stroke-linejoin=\"round\"\r\n >\r\n <line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\"></line>\r\n <line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\"></line>\r\n </svg>\r\n </button>\r\n </div>\r\n\r\n <div class=\"api-key-modal-body\">\r\n <p class=\"modal-message\">\r\n The Face++ API returned an authorization or rate-limit error. Please\r\n enter your Face++ API Key and API Secret.\r\n </p>\r\n\r\n <div\r\n class=\"api-key-input-group\"\r\n style=\"flex-direction: column; align-items: stretch; gap: 10px\"\r\n >\r\n <input\r\n type=\"text\"\r\n [(ngModel)]=\"faceppKeyInputValue\"\r\n placeholder=\"Face++ API Key\"\r\n class=\"api-key-input\"\r\n />\r\n <input\r\n type=\"text\"\r\n [(ngModel)]=\"faceppSecretInputValue\"\r\n placeholder=\"Face++ API Secret\"\r\n class=\"api-key-input\"\r\n />\r\n <button class=\"api-key-load-btn\" (click)=\"updateFaceppCredentials()\">\r\n Apply Face++ Credentials\r\n </button>\r\n </div>\r\n </div>\r\n </div>\r\n</div>\r\n\r\n<!-- OpenAI API Key Modal (Independent) -->\r\n<div class=\"api-key-modal-overlay openai-overlay\" *ngIf=\"showOpenAIKeyModal\">\r\n <div class=\"api-key-modal\" (click)=\"$event.stopPropagation()\">\r\n <div class=\"api-key-modal-header\">\r\n <div class=\"warning-icon\">\r\n <svg\r\n xmlns=\"http://www.w3.org/2000/svg\"\r\n width=\"48\"\r\n height=\"48\"\r\n viewBox=\"0 0 24 24\"\r\n fill=\"none\"\r\n stroke=\"currentColor\"\r\n stroke-width=\"2\"\r\n stroke-linecap=\"round\"\r\n stroke-linejoin=\"round\"\r\n >\r\n <path\r\n d=\"M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z\"\r\n ></path>\r\n <line x1=\"12\" y1=\"9\" x2=\"12\" y2=\"13\"></line>\r\n <line x1=\"12\" y1=\"17\" x2=\"12.01\" y2=\"17\"></line>\r\n </svg>\r\n </div>\r\n <h2>OpenAI API Key Required</h2>\r\n <button\r\n class=\"close-btn\"\r\n (click)=\"closeOpenAIKeyModal()\"\r\n aria-label=\"Close\"\r\n >\r\n <svg\r\n xmlns=\"http://www.w3.org/2000/svg\"\r\n width=\"24\"\r\n height=\"24\"\r\n viewBox=\"0 0 24 24\"\r\n fill=\"none\"\r\n stroke=\"currentColor\"\r\n stroke-width=\"2\"\r\n stroke-linecap=\"round\"\r\n stroke-linejoin=\"round\"\r\n >\r\n <line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\"></line>\r\n <line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\"></line>\r\n </svg>\r\n </button>\r\n </div>\r\n\r\n <div class=\"api-key-modal-body\">\r\n <p class=\"modal-message\">\r\n The OpenAI API returned an authorization or rate-limit error. Please\r\n enter your OpenAI API key to continue using the text similarity features.\r\n </p>\r\n <p class=\"modal-instruction\">\r\n Text matching now uses OpenAI embeddings (text-embedding-3-small) for improved semantic similarity.\r\n </p>\r\n\r\n <div class=\"api-key-input-group\">\r\n <input\r\n type=\"text\"\r\n [(ngModel)]=\"openaiKeyInputValue\"\r\n placeholder=\"Paste your OpenAI API key here\"\r\n class=\"api-key-input\"\r\n autofocus\r\n />\r\n <button class=\"api-key-load-btn\" (click)=\"updateOpenAIKey()\">\r\n Apply OpenAI Key\r\n </button>\r\n </div>\r\n\r\n <div class=\"modal-hint\">\r\n <p>\r\n <svg\r\n xmlns=\"http://www.w3.org/2000/svg\"\r\n width=\"16\"\r\n height=\"16\"\r\n viewBox=\"0 0 24 24\"\r\n fill=\"none\"\r\n stroke=\"currentColor\"\r\n stroke-width=\"2\"\r\n stroke-linecap=\"round\"\r\n stroke-linejoin=\"round\"\r\n >\r\n <circle cx=\"12\" cy=\"12\" r=\"10\"></circle>\r\n <line x1=\"12\" y1=\"16\" x2=\"12\" y2=\"12\"></line>\r\n <line x1=\"12\" y1=\"8\" x2=\"12.01\" y2=\"8\"></line>\r\n </svg>\r\n Don't have an API key? Get your free key at\r\n <a href=\"https://platform.openai.com/api-keys\" target=\"_blank\" rel=\"noopener\"\r\n >platform.openai.com/api-keys</a\r\n >\r\n </p>\r\n </div>\r\n </div>\r\n </div>\r\n</div>\r\n\r\n<div class=\"blank-box\"></div>\r\n\r\n<div class=\"profile-screen\">\r\n <div class=\"profile-flex\">\r\n <div class=\"profile-img left\" [class.fade-all]=\"fadeAllEdges\">\r\n <img\r\n [src]=\"user1Image\"\r\n alt=\"User 1\"\r\n [style.transform]=\"user1Transform\"\r\n [style.object-position]=\"user1ObjectPosition\"\r\n (load)=\"onUserImageLoad(1, $event)\"\r\n />\r\n </div>\r\n <div class=\"profile-img right\" [class.fade-all]=\"fadeAllEdges\">\r\n <img\r\n [src]=\"user2Image\"\r\n alt=\"User 2\"\r\n [style.transform]=\"user2Transform\"\r\n [style.object-position]=\"user2ObjectPosition\"\r\n (load)=\"onUserImageLoad(2, $event)\"\r\n />\r\n <!-- [style.object-fit]=\"'cover'\" -->\r\n </div>\r\n </div>\r\n\r\n <div class=\"shape\">\r\n <div class=\"shape-bg\">\r\n <img src=\"./assets/s1.svg\" alt=\"Shape\" height=\"350\" class=\"shape-bg1\" />\r\n <img src=\"./assets/s2.svg\" alt=\"Shape\" height=\"350\" class=\"shape-bg2\" />\r\n </div>\r\n\r\n <div class=\"shape-text\">\r\n <div class=\"shape-text-left\">\r\n <div class=\"scroll-container\" #leftContainer>\r\n <ng-container\r\n *ngFor=\"\r\n let interest of alignedPerson1Interests.length > 0\r\n ? alignedPerson1Interests\r\n : displayPerson1Interests.length > 0\r\n ? displayPerson1Interests\r\n : person1Interests;\r\n let i = index\r\n \"\r\n >\r\n <p\r\n class=\"shape-p\"\r\n *ngIf=\"!centerItem.includes(interest)\"\r\n [ngClass]=\"{'dash-item': interest === '-', 'spacer-item': interest === '----', 'aligned-item': interest !== '-' && interest !== '----'}\"\r\n >\r\n {{ interest === \"-\" ? \"\u2014\" : interest }}\r\n </p>\r\n </ng-container>\r\n </div>\r\n <h2 class=\"shape-h2\" (click)=\"onViewProfile()\">View Profile</h2>\r\n </div>\r\n\r\n <div class=\"shape-text-center\">\r\n <ng-container *ngFor=\"let item of centerItem\">\r\n <p class=\"shape-p-center\">{{ item }}</p>\r\n </ng-container>\r\n </div>\r\n <div class=\"shape-text-right\">\r\n <div class=\"scroll-container\" #rightContainer>\r\n <ng-container\r\n *ngFor=\"\r\n let interest of alignedPerson2Interests.length > 0\r\n ? alignedPerson2Interests\r\n : displayPerson2Interests.length > 0\r\n ? displayPerson2Interests\r\n : person2Interests;\r\n let i = index\r\n \"\r\n >\r\n <p\r\n class=\"shape-p-right\"\r\n *ngIf=\"!centerItem.includes(interest)\"\r\n [ngClass]=\"{'dash-item': interest === '-', 'spacer-item': interest === '----', 'aligned-item': interest !== '-' && interest !== '----'}\"\r\n >\r\n {{ interest === \"-\" ? \"\u2014\" : interest }}\r\n </p>\r\n </ng-container>\r\n </div>\r\n <h2 class=\"shape-h2-right\" (click)=\"onViewProfile()\">View Profile</h2>\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <!-- Loading indicator for alignment process -->\r\n <div *ngIf=\"isAligning\" class=\"loading-indicator\">\r\n <div class=\"loading-spinner\"></div>\r\n </div>\r\n</div>\r\n\r\n\r\n", styles: [".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%)}.profile-screen{width:320px;max-width:100%;margin:0 auto;overflow:hidden;position:relative}.blank-box{background-color:#000;height:20px;width:100%;position:absolute;left:0;top:26%;transform:translateY(-50%);z-index:-5}.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}.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 img{cursor:grab}.shape-bg img: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"] }, { kind: "directive", type: i3.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i3.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }] });
|
|
3020
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: ProfileComparisonLibComponent, deps: [{ token: ProfileComparisonBackendService }, { token: i0.Renderer2 }, { token: FileConversionService }, { token: ImageCompressionService }, { token: i0.ChangeDetectorRef }, { token: PROFILE_COMPARISON_VERBOSE_LOGGING, optional: true }], target: i0.ɵɵFactoryTarget.Component });
|
|
3021
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.16", type: ProfileComparisonLibComponent, isStandalone: false, selector: "lib-profile-comparison", inputs: { config: "config", fadeAllEdges: "fadeAllEdges" }, outputs: { matrixDataChange: "matrixDataChange", rawLLMOutputChange: "rawLLMOutputChange", viewProfileClick: "viewProfileClick" }, viewQueries: [{ propertyName: "leftContainer", first: true, predicate: ["leftContainer"], descendants: true }, { propertyName: "rightContainer", first: true, predicate: ["rightContainer"], descendants: true }, { propertyName: "profileFlex", first: true, predicate: ["profileFlex"], descendants: true }, { propertyName: "profileImgLeft", first: true, predicate: ["profileImgLeft"], descendants: true }, { propertyName: "profileImgRight", first: true, predicate: ["profileImgRight"], descendants: true }, { propertyName: "shapeContainer", first: true, predicate: ["shapeContainer"], descendants: true }, { propertyName: "shapeBg", first: true, predicate: ["shapeBg"], descendants: true }, { propertyName: "shapeBg1", first: true, predicate: ["shapeBg1"], descendants: true }, { propertyName: "shapeBg2", first: true, predicate: ["shapeBg2"], descendants: true }, { propertyName: "shapeTextLeft", first: true, predicate: ["shapeTextLeft"], descendants: true }, { propertyName: "shapeTextRight", first: true, predicate: ["shapeTextRight"], descendants: true }, { propertyName: "shapeTextCenter", first: true, predicate: ["shapeTextCenter"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "<div class=\"configure-backend-message\" *ngIf=\"!backendConfigured\">\n Configure backend\n</div>\n\n<div class=\"profile-screen\" *ngIf=\"backendConfigured\">\n <div class=\"backend-error-message\" *ngIf=\"backendError\">{{ backendError }}</div>\n <div #profileFlex class=\"profile-flex\" [class.fade-all]=\"fadeAllEdges\">\n <div #profileImgLeft class=\"profile-img left\" [class.fade-all]=\"fadeAllEdges\">\n <img\n [src]=\"user1Image\"\n alt=\"User 1\"\n [style.transform]=\"user1Transform\"\n [style.object-position]=\"user1ObjectPosition\"\n (load)=\"onUserImageLoad(1, $event)\"\n />\n </div>\n <div #profileImgRight class=\"profile-img right\" [class.fade-all]=\"fadeAllEdges\">\n <img\n [src]=\"user2Image\"\n alt=\"User 2\"\n [style.transform]=\"user2Transform\"\n [style.object-position]=\"user2ObjectPosition\"\n (load)=\"onUserImageLoad(2, $event)\"\n />\n <!-- [style.object-fit]=\"'cover'\" -->\n </div>\n </div>\n\n <div #shapeContainer class=\"shape\">\n <div #shapeBg class=\"shape-bg\" [class.fade-all]=\"fadeAllEdges\">\n <svg width=\"250\" height=\"350\" viewBox=\"0 0 250 350\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" preserveAspectRatio=\"xMaxYMid slice\" style=\"height: 350px;\" class=\"shape-bg1\" #shapeBg1>\n<g filter=\"url(#filter0_i_4_4137)\">\n<path d=\"M215.177 44.4924L-18.823 19.3104C-25.3264 18.6106 -31 23.7064 -31 30.2473V283.169C-31 289.914 -24.9854 295.07 -18.3196 294.04L215.68 257.869C221.043 257.04 225 252.407 225 246.981V55.4497C225 49.8302 220.764 45.0937 215.177 44.4924Z\" fill=\"#00CFE6\" fill-opacity=\"0.07\"/>\n</g>\n<path d=\"M215.177 44.4924L-18.823 19.3104C-25.3264 18.6106 -31 23.7064 -31 30.2473V283.169C-31 289.914 -24.9854 295.07 -18.3196 294.04L215.68 257.869C221.043 257.04 225 252.407 225 246.981V55.4497C225 49.8302 220.764 45.0937 215.177 44.4924Z\" fill=\"url(#paint0_radial_4_4137)\" fill-opacity=\"0.07\" stroke=\"url(#paint1_linear_4_4137)\" stroke-width=\"2\" stroke-miterlimit=\"3.99393\" stroke-linejoin=\"round\"/>\n<g filter=\"url(#filter1_ddddd_4_4137)\">\n<path d=\"M219.081 43.0755L-18.9193 17.2045C-24.8351 16.5614 -30 21.1953 -30 27.1459V287.318C-30 293.455 -24.5217 298.145 -18.4573 297.198L219.543 260.038C224.411 259.278 228 255.075 228 250.148V53.0303C228 47.9257 224.155 43.6271 219.081 43.0755Z\" stroke=\"#61C2AB\" stroke-opacity=\"0.3\" stroke-linejoin=\"round\" shape-rendering=\"crispEdges\"/>\n</g>\n<defs>\n<filter id=\"filter0_i_4_4137\" x=\"-31\" y=\"19.2461\" width=\"256\" height=\"274.925\" filterUnits=\"userSpaceOnUse\" color-interpolation-filters=\"sRGB\">\n<feFlood flood-opacity=\"0\" result=\"BackgroundImageFix\"/>\n<feBlend mode=\"normal\" in=\"SourceGraphic\" in2=\"BackgroundImageFix\" result=\"shape\"/>\n<feColorMatrix in=\"SourceAlpha\" type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0\" result=\"hardAlpha\"/>\n<feOffset/>\n<feGaussianBlur stdDeviation=\"7.95\"/>\n<feComposite in2=\"hardAlpha\" operator=\"arithmetic\" k2=\"-1\" k3=\"1\"/>\n<feColorMatrix type=\"matrix\" values=\"0 0 0 0 0.516667 0 0 0 0 1 0 0 0 1 0 0 0 1 0\"/>\n<feBlend mode=\"normal\" in2=\"shape\" result=\"effect1_innerShadow_4_4137\"/>\n</filter>\n<filter id=\"filter1_ddddd_4_4137\" x=\"-46.2\" y=\"0.944715\" width=\"290.4\" height=\"312.575\" filterUnits=\"userSpaceOnUse\" color-interpolation-filters=\"sRGB\">\n<feFlood flood-opacity=\"0\" result=\"BackgroundImageFix\"/>\n<feColorMatrix in=\"SourceAlpha\" type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0\" result=\"hardAlpha\"/>\n<feOffset/>\n<feGaussianBlur stdDeviation=\"7.95\"/>\n<feComposite in2=\"hardAlpha\" operator=\"out\"/>\n<feColorMatrix type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0\"/>\n<feBlend mode=\"normal\" in2=\"BackgroundImageFix\" result=\"effect1_dropShadow_4_4137\"/>\n<feColorMatrix in=\"SourceAlpha\" type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0\" result=\"hardAlpha\"/>\n<feOffset/>\n<feGaussianBlur stdDeviation=\"7.85\"/>\n<feComposite in2=\"hardAlpha\" operator=\"out\"/>\n<feColorMatrix type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0\"/>\n<feBlend mode=\"normal\" in2=\"effect1_dropShadow_4_4137\" result=\"effect2_dropShadow_4_4137\"/>\n<feColorMatrix in=\"SourceAlpha\" type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0\" result=\"hardAlpha\"/>\n<feOffset/>\n<feGaussianBlur stdDeviation=\"7.85\"/>\n<feComposite in2=\"hardAlpha\" operator=\"out\"/>\n<feColorMatrix type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0\"/>\n<feBlend mode=\"normal\" in2=\"effect2_dropShadow_4_4137\" result=\"effect3_dropShadow_4_4137\"/>\n<feColorMatrix in=\"SourceAlpha\" type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0\" result=\"hardAlpha\"/>\n<feOffset/>\n<feGaussianBlur stdDeviation=\"7.85\"/>\n<feComposite in2=\"hardAlpha\" operator=\"out\"/>\n<feColorMatrix type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0\"/>\n<feBlend mode=\"normal\" in2=\"effect3_dropShadow_4_4137\" result=\"effect4_dropShadow_4_4137\"/>\n<feColorMatrix in=\"SourceAlpha\" type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0\" result=\"hardAlpha\"/>\n<feOffset/>\n<feGaussianBlur stdDeviation=\"7.85\"/>\n<feComposite in2=\"hardAlpha\" operator=\"out\"/>\n<feColorMatrix type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0\"/>\n<feBlend mode=\"normal\" in2=\"effect4_dropShadow_4_4137\" result=\"effect5_dropShadow_4_4137\"/>\n<feBlend mode=\"normal\" in=\"SourceGraphic\" in2=\"effect5_dropShadow_4_4137\" result=\"shape\"/>\n</filter>\n<radialGradient id=\"paint0_radial_4_4137\" cx=\"0\" cy=\"0\" r=\"1\" gradientTransform=\"matrix(65.1034 -81.3964 74.955 49.415 97 157)\" gradientUnits=\"userSpaceOnUse\">\n<stop stop-color=\"#0051E6\" stop-opacity=\"0\"/>\n<stop offset=\"0.615385\" stop-color=\"#0051E6\" stop-opacity=\"0\"/>\n<stop offset=\"1\" stop-color=\"white\"/>\n</radialGradient>\n<linearGradient id=\"paint1_linear_4_4137\" x1=\"97\" y1=\"18\" x2=\"97\" y2=\"296\" gradientUnits=\"userSpaceOnUse\">\n<stop offset=\"0.380829\" stop-color=\"#52FFEB\"/>\n<stop offset=\"1\" stop-color=\"#319990\"/>\n</linearGradient>\n</defs>\n</svg>\n <svg width=\"250\" height=\"350\" viewBox=\"0 0 250 350\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" preserveAspectRatio=\"xMinYMid slice\" style=\"height: 350px;\" class=\"shape-bg2\" #shapeBg2>\n<g filter=\"url(#filter0_i_4_4141)\">\n<path d=\"M29.5864 44.9454L263.826 19.8065C270.329 19.1087 276 24.2041 276 30.7437V283.174C276 289.917 269.988 295.073 263.324 294.046L29.0843 257.937C23.7196 257.11 19.7602 252.476 19.7602 247.048V55.9032C19.7602 50.2824 23.9977 45.5452 29.5864 44.9454Z\" fill=\"#7B00E6\" fill-opacity=\"0.07\"/>\n</g>\n<path d=\"M29.5864 44.9454L263.826 19.8065C270.329 19.1087 276 24.2041 276 30.7437V283.174C276 289.917 269.988 295.073 263.324 294.046L29.0843 257.937C23.7196 257.11 19.7602 252.476 19.7602 247.048V55.9032C19.7602 50.2824 23.9977 45.5452 29.5864 44.9454Z\" fill=\"url(#paint0_radial_4_4141)\" fill-opacity=\"0.07\" stroke=\"url(#paint1_linear_4_4141)\" stroke-width=\"2\" stroke-miterlimit=\"3.99393\" stroke-linejoin=\"round\"/>\n<g filter=\"url(#filter1_ddddd_4_4141)\">\n<path d=\"M25.9212 43.077L264.37 17.2022C270.285 16.5603 275.449 21.194 275.449 27.1438V287.321C275.449 293.457 269.972 298.146 263.909 297.201L25.46 260.036C20.5905 259.277 17 255.074 17 250.146V53.032C17 47.9267 20.8457 43.6277 25.9212 43.077Z\" stroke=\"#C37DFF\" stroke-opacity=\"0.3\" stroke-linejoin=\"round\" shape-rendering=\"crispEdges\"/>\n</g>\n<defs>\n<filter id=\"filter0_i_4_4141\" x=\"19.7602\" y=\"19.7425\" width=\"256.24\" height=\"274.434\" filterUnits=\"userSpaceOnUse\" color-interpolation-filters=\"sRGB\">\n<feFlood flood-opacity=\"0\" result=\"BackgroundImageFix\"/>\n<feBlend mode=\"normal\" in=\"SourceGraphic\" in2=\"BackgroundImageFix\" result=\"shape\"/>\n<feColorMatrix in=\"SourceAlpha\" type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0\" result=\"hardAlpha\"/>\n<feOffset/>\n<feGaussianBlur stdDeviation=\"7.95\"/>\n<feComposite in2=\"hardAlpha\" operator=\"arithmetic\" k2=\"-1\" k3=\"1\"/>\n<feColorMatrix type=\"matrix\" values=\"0 0 0 0 0.482353 0 0 0 0 0 0 0 0 0 0.901961 0 0 0 1 0\"/>\n<feBlend mode=\"normal\" in2=\"shape\" result=\"effect1_innerShadow_4_4141\"/>\n</filter>\n<filter id=\"filter1_ddddd_4_4141\" x=\"0.8\" y=\"0.942639\" width=\"290.849\" height=\"312.58\" filterUnits=\"userSpaceOnUse\" color-interpolation-filters=\"sRGB\">\n<feFlood flood-opacity=\"0\" result=\"BackgroundImageFix\"/>\n<feColorMatrix in=\"SourceAlpha\" type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0\" result=\"hardAlpha\"/>\n<feOffset/>\n<feGaussianBlur stdDeviation=\"7.85\"/>\n<feComposite in2=\"hardAlpha\" operator=\"out\"/>\n<feColorMatrix type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0\"/>\n<feBlend mode=\"normal\" in2=\"BackgroundImageFix\" result=\"effect1_dropShadow_4_4141\"/>\n<feColorMatrix in=\"SourceAlpha\" type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0\" result=\"hardAlpha\"/>\n<feOffset/>\n<feGaussianBlur stdDeviation=\"7.85\"/>\n<feComposite in2=\"hardAlpha\" operator=\"out\"/>\n<feColorMatrix type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0\"/>\n<feBlend mode=\"normal\" in2=\"effect1_dropShadow_4_4141\" result=\"effect2_dropShadow_4_4141\"/>\n<feColorMatrix in=\"SourceAlpha\" type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0\" result=\"hardAlpha\"/>\n<feOffset/>\n<feGaussianBlur stdDeviation=\"7.85\"/>\n<feComposite in2=\"hardAlpha\" operator=\"out\"/>\n<feColorMatrix type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0\"/>\n<feBlend mode=\"normal\" in2=\"effect2_dropShadow_4_4141\" result=\"effect3_dropShadow_4_4141\"/>\n<feColorMatrix in=\"SourceAlpha\" type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0\" result=\"hardAlpha\"/>\n<feOffset/>\n<feGaussianBlur stdDeviation=\"7.85\"/>\n<feComposite in2=\"hardAlpha\" operator=\"out\"/>\n<feColorMatrix type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0\"/>\n<feBlend mode=\"normal\" in2=\"effect3_dropShadow_4_4141\" result=\"effect4_dropShadow_4_4141\"/>\n<feColorMatrix in=\"SourceAlpha\" type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0\" result=\"hardAlpha\"/>\n<feOffset/>\n<feGaussianBlur stdDeviation=\"7.85\"/>\n<feComposite in2=\"hardAlpha\" operator=\"out\"/>\n<feColorMatrix type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0\"/>\n<feBlend mode=\"normal\" in2=\"effect4_dropShadow_4_4141\" result=\"effect5_dropShadow_4_4137\"/>\n<feBlend mode=\"normal\" in=\"SourceGraphic\" in2=\"effect5_dropShadow_4_4137\" result=\"shape\"/>\n</filter>\n<radialGradient id=\"paint0_radial_4_4137\" cx=\"0\" cy=\"0\" r=\"1\" gradientTransform=\"matrix(65.1034 -81.3964 74.955 49.415 97 157)\" gradientUnits=\"userSpaceOnUse\">\n<stop stop-color=\"#0051E6\" stop-opacity=\"0\"/>\n<stop offset=\"0.615385\" stop-color=\"#0051E6\" stop-opacity=\"0\"/>\n<stop offset=\"1\" stop-color=\"white\"/>\n</radialGradient>\n<linearGradient id=\"paint1_linear_4_4137\" x1=\"97\" y1=\"18\" x2=\"97\" y2=\"296\" gradientUnits=\"userSpaceOnUse\">\n<stop offset=\"0.380829\" stop-color=\"#52FFEB\"/>\n<stop offset=\"1\" stop-color=\"#319990\"/>\n</linearGradient>\n</defs>\n</svg>\n </div>\n\n <div class=\"shape-text\">\n <div #shapeTextLeft class=\"shape-text-left\">\n <div class=\"scroll-container\" #leftContainer>\n <ng-container\n *ngFor=\"\n let interest of alignedPerson1Interests.length > 0\n ? alignedPerson1Interests\n : displayPerson1Interests.length > 0\n ? displayPerson1Interests\n : person1Interests;\n let i = index\n \"\n >\n <p\n class=\"shape-p\"\n *ngIf=\"!isInCenter(interest)\"\n [ngClass]=\"{'dash-item': interest === '-', 'spacer-item': interest === '----', 'aligned-item': interest !== '-' && interest !== '----'}\"\n >\n {{ interest === \"-\" ? \"\u2014\" : interest }}\n </p>\n </ng-container>\n </div>\n <h2 class=\"shape-h2\" (click)=\"onViewProfile('left')\">View Profile</h2>\n </div>\n\n <div #shapeTextCenter class=\"shape-text-center\">\n <ng-container *ngFor=\"let item of centerItem\">\n <p class=\"shape-p-center\">{{ item }}</p>\n </ng-container>\n </div>\n <div #shapeTextRight class=\"shape-text-right\">\n <div class=\"scroll-container\" #rightContainer>\n <ng-container\n *ngFor=\"\n let interest of alignedPerson2Interests.length > 0\n ? alignedPerson2Interests\n : displayPerson2Interests.length > 0\n ? displayPerson2Interests\n : person2Interests;\n let i = index\n \"\n >\n <p\n class=\"shape-p-right\"\n *ngIf=\"!isInCenter(interest)\"\n [ngClass]=\"{'dash-item': interest === '-', 'spacer-item': interest === '----', 'aligned-item': interest !== '-' && interest !== '----'}\"\n >\n {{ interest === \"-\" ? \"\u2014\" : interest }}\n </p>\n </ng-container>\n </div>\n <h2 class=\"shape-h2-right\" (click)=\"onViewProfile('right')\">View Profile</h2>\n </div>\n </div>\n </div>\n\n <!-- Loading indicator for alignment process -->\n <div *ngIf=\"isAligning\" class=\"loading-indicator\">\n <div class=\"loading-spinner\"></div>\n </div>\n</div>\n\n\n", styles: [".profile-img.left{-webkit-mask-image:linear-gradient(to right,rgb(0,0,0) 0%,rgb(0,0,0) calc(100% - var(--overlap-size, 40px)),rgba(0,0,0,0) 100%);-webkit-mask-size:100% 100%;-webkit-mask-repeat:no-repeat;mask-image:linear-gradient(to right,#fff 0% calc(100% - var(--overlap-size, 40px)),#fff0);mask-size:100% 100%;mask-repeat:no-repeat;margin-right:calc(-.5 * var(--overlap-size, 40px))}.profile-img.right{-webkit-mask-image:linear-gradient(to left,rgb(0,0,0) 0%,rgb(0,0,0) calc(100% - var(--overlap-size, 40px)),rgba(0,0,0,0) 100%);-webkit-mask-size:100% 100%;-webkit-mask-repeat:no-repeat;mask-image:linear-gradient(to left,#fff 0% calc(100% - var(--overlap-size, 40px)),#fff0);mask-size:100% 100%;mask-repeat:no-repeat;margin-left:calc(-.5 * var(--overlap-size, 40px))}.profile-img.fade-all{-webkit-mask-image:linear-gradient(to bottom,transparent 75px,black 125px,black 425px,transparent 475px),linear-gradient(to right,transparent,black 15%,black 85%,transparent);-webkit-mask-composite:source-in;mask-image:linear-gradient(to bottom,transparent 75px,black 125px,black 425px,transparent 475px),linear-gradient(to right,transparent,black 15%,black 85%,transparent);mask-composite:intersect}.profile-img.fade-all.left{-webkit-mask-image:linear-gradient(to bottom,transparent 75px,black 125px,black 425px,transparent 475px),linear-gradient(to right,transparent 0%,black 15%,black calc(100% - var(--overlap-size, 40px)),transparent 100%);mask-image:linear-gradient(to bottom,transparent 75px,black 125px,black 425px,transparent 475px),linear-gradient(to right,transparent 0%,black 15%,black calc(100% - var(--overlap-size, 40px)),transparent 100%)}.profile-img.fade-all.right{-webkit-mask-image:linear-gradient(to bottom,transparent 75px,black 125px,black 425px,transparent 475px),linear-gradient(to left,transparent 0%,black 15%,black calc(100% - var(--overlap-size, 40px)),transparent 100%);mask-image:linear-gradient(to bottom,transparent 75px,black 125px,black 425px,transparent 475px),linear-gradient(to left,transparent 0%,black 15%,black calc(100% - var(--overlap-size, 40px)),transparent 100%)}.configure-backend-message{padding:1rem;text-align:center;color:#bdc3c7;background:#34495e;border-radius:8px}.backend-error-message{padding:.75rem 1rem;text-align:center;color:#fef3c7;background:#b4530940;border:1px solid rgba(245,158,11,.5);border-radius:8px;margin-bottom:.5rem}.profile-screen{width:320px;max-width:100%;margin:0 auto;overflow:hidden;position:relative}.profile-flex{display:flex;width:100%;height:400px;overflow:hidden;background:linear-gradient(135deg,#3a3a3a,#2a2a2a);gap:0;margin:0;padding:0;position:relative;box-shadow:none;border:none;--overlap-size: 40px}.profile-flex.fade-all{-webkit-mask-image:linear-gradient(to right,transparent 0%,black 15%,black 85%,transparent 100%),linear-gradient(to bottom,transparent 0%,black 15%,black 85%,transparent 100%);-webkit-mask-composite:intersect;mask-image:linear-gradient(to right,transparent 0%,black 15%,black 85%,transparent 100%),linear-gradient(to bottom,transparent 0%,black 15%,black 85%,transparent 100%);mask-composite:intersect}.shape-bg1,.shape-bg2{position:absolute;max-width:250px;opacity:.5;transition:transform .2s ease-out}.shape-bg1{z-index:1;left:0}.shape-bg2{right:0}.shape-bg.fade-all .shape-bg1{-webkit-mask-image:linear-gradient(to right,transparent 0%,black 20%,black 100%);mask-image:linear-gradient(to right,transparent 0%,black 20%,black 100%)}.shape-bg.fade-all .shape-bg2{-webkit-mask-image:linear-gradient(to left,transparent 0%,black 20%,black 100%);mask-image:linear-gradient(to left,transparent 0%,black 20%,black 100%)}.shape-bg svg{cursor:grab}.shape-bg svg:active{cursor:grabbing}.profile-img{width:180px;height:550px;position:relative;flex-shrink:0;background:linear-gradient(135deg,#3a3a3a,#2a2a2a);margin:0;padding:0;left:0;top:-75px;border:none;box-sizing:border-box;overflow:hidden;box-shadow:none;outline:none}.profile-img img{transition:transform .3s ease;width:100%;height:100%;object-fit:cover;display:block;object-position:center center;opacity:.6;transform-origin:center center;border:none;outline:none;box-shadow:none;position:relative;top:0;left:0;filter:contrast(1.2) brightness(1.1) saturate(1.1);image-rendering:auto}.shape{position:absolute;top:40px;width:100%}.shape-bg{position:relative}.shape-text{position:absolute;top:0;left:0;right:0;width:100%;height:100%;z-index:1;padding:0 1.25rem;box-sizing:border-box;pointer-events:none}.shape-text-left{margin-top:40px;text-align:left;position:absolute;top:0;bottom:0;right:calc(50% + 75px);z-index:2;pointer-events:auto;display:flex;flex-direction:column;align-items:flex-end}.shape-text-left .scroll-container{width:100%}.shape-text-center{position:absolute;left:0;right:0;margin:0 auto;width:fit-content;font-family:Gilroy,sans-serif;font-weight:700;font-size:.75rem;line-height:100%;letter-spacing:0%;color:#fff;text-align:center;display:flex;justify-content:center;align-items:center;flex-direction:column;text-shadow:2px 2px 4px rgba(0,0,0,.3);z-index:10;transition:transform .3s ease;pointer-events:none;top:50%;transform:translateY(-50%);padding:0 .625rem;margin-top:3.8125rem}.shape-text-right{margin-top:40px;text-align:right;position:absolute;top:0;bottom:0;left:calc(50% + 70px);z-index:2;pointer-events:auto;display:flex;flex-direction:column;align-items:flex-start}.shape-text-right .scroll-container{width:100%}.shape-p{font-family:Gilroy,sans-serif;font-weight:400;font-size:.75rem;line-height:100%;letter-spacing:0%;color:#fff;padding-bottom:.125rem;text-align:right;text-shadow:2px 2px 4px rgba(0,0,0,.3);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;direction:rtl;-webkit-user-select:none;user-select:none;margin:0}.shape-p-center{font-family:Gilroy,sans-serif;font-weight:700;font-size:.75rem;line-height:100%;letter-spacing:0%;color:#fff;margin:0;text-align:center;display:flex;justify-content:center;align-items:center;text-shadow:2px 2px 4px rgba(0,0,0,.3)}.shape-p-right{font-family:Gilroy,sans-serif;font-weight:400;font-size:.75rem;line-height:100%;letter-spacing:0%;color:#fff;padding-bottom:.125rem;text-align:left;text-shadow:2px 2px 4px rgba(0,0,0,.3);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin:0;-webkit-user-select:none;user-select:none}.dash-item{color:#ffffff4d!important;font-size:.625rem!important;height:.625rem}.spacer-item{height:auto!important;color:#fffc!important;font-weight:700;font-size:.75rem!important;letter-spacing:2px}.shape-h2{font-family:Calistoga,serif;font-weight:400;font-size:.625rem;letter-spacing:0%;color:#ffffff80;text-shadow:2px 2px 4px rgba(0,0,0,.3);cursor:pointer;transition:all .3s ease;border:none;outline:none;white-space:nowrap;pointer-events:auto;z-index:100;position:relative;margin-left:.5rem}.shape-h2:hover{opacity:.8;transform:translateY(-2px)}.shape-h2-right{font-family:Calistoga,serif;font-weight:400;font-size:.625rem;letter-spacing:0%;color:#ffffff80;text-shadow:2px 2px 4px rgba(0,0,0,.3);cursor:pointer;transition:all .3s ease;border:none;outline:none;white-space:nowrap;pointer-events:auto;z-index:100;position:relative;margin-right:3.75rem}.shape-h2-right:hover{opacity:.8;transform:translateY(-2px)}.scroll-when-long{display:inline-block;max-width:4.5625rem;white-space:nowrap;overflow-x:auto;overflow-y:hidden;-webkit-overflow-scrolling:touch}.scroll-when-long::-webkit-scrollbar{height:2px}.scroll-when-long::-webkit-scrollbar-track{background:#eee;border-radius:.625rem}.scroll-when-long::-webkit-scrollbar-thumb{background:#888;border-radius:.625rem}.scroll-when-long::-webkit-scrollbar-thumb:hover{background:#555}.shape-text p{cursor:default;-webkit-user-select:none;user-select:none}.shape-text p:active{cursor:default}#drag-preview{position:absolute;pointer-events:none;display:none;background:#000000b3;color:#fff;padding:5px 10px;border-radius:5px;z-index:9999;max-width:200px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.shape-text-container{width:70px;position:relative;overflow:hidden}.draggable{width:70px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;cursor:grab;-webkit-user-select:none;user-select:none;position:relative;display:inline-block}.dragging{width:auto;text-overflow:clip;cursor:grabbing;position:absolute;z-index:1000}.p-wrapper{width:70px;overflow-x:hidden}.shape-text-center{display:flex;justify-content:space-between;flex-direction:column;gap:10px;height:100%;margin-top:55px}.loading-indicator{position:fixed;top:25%;left:50%;transform:translate(-50%,-50%);background:transparent;color:#fff;padding:20px 30px;border-radius:10px;text-align:center;z-index:1000;display:flex;flex-direction:column;align-items:center;gap:8px}.loading-spinner{width:40px;height:40px;border:4px solid rgba(255,255,255,.3);border-top:4px solid #667eea;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.loading-indicator p{margin:0;font-size:16px;font-weight:500}.api-key-modal-overlay{position:fixed;inset:0;background:#0f172a99;-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px);display:flex;align-items:center;justify-content:center;z-index:2000}.api-key-modal{width:min(640px,92vw);background:#0f172a;color:#e2e8f0;border:1px solid #334155;border-radius:12px;box-shadow:0 20px 50px #00000073;padding:16px 18px}.api-key-modal-header{display:flex;align-items:center;justify-content:space-between;gap:12px}.api-key-modal-header h2{margin:0;font-size:20px;font-weight:700;color:#e2e8f0}.warning-icon{color:#fbbf24}.api-key-modal .close-btn{background:#0b1220;border:1px solid #334155;color:#94a3b8;border-radius:8px;padding:6px;cursor:pointer;line-height:0}.api-key-modal-body{margin-top:12px;display:flex;flex-direction:column;gap:10px}.modal-message,.modal-instruction{margin:0;color:#cbd5e1}.modal-message strong{color:#e5e7eb}.api-key-input-group{display:flex;align-items:center;gap:10px}.api-key-input{flex:1 1 auto;background:#0a1020;color:#e2e8f0;border:1px solid #334155;border-radius:8px;padding:10px 12px}.api-key-input:focus{outline:none;border-color:#2563eb;box-shadow:0 0 0 3px #2563eb33}.api-key-load-btn{background:#2563eb;color:#fff;border:none;border-radius:8px;padding:10px 12px;cursor:pointer;font-weight:600}.api-key-load-btn:hover{background:#1d4ed8}.modal-hint{display:flex;align-items:center;gap:8px;color:#94a3b8}.modal-hint a{color:#93c5fd}\n"], dependencies: [{ kind: "directive", type: i2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] });
|
|
2741
3022
|
}
|
|
2742
3023
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: ProfileComparisonLibComponent, decorators: [{
|
|
2743
3024
|
type: Component,
|
|
2744
|
-
args: [{ selector: 'lib-profile-comparison', standalone: false, template: "<!-- API Quota Exhausted Modal (Independent) -->\r\n<div class=\"api-key-modal-overlay\" *ngIf=\"showApiKeyModal\">\r\n <div class=\"api-key-modal\" (click)=\"$event.stopPropagation()\">\r\n <div class=\"api-key-modal-header\">\r\n <div class=\"warning-icon\">\r\n <svg\r\n xmlns=\"http://www.w3.org/2000/svg\"\r\n width=\"48\"\r\n height=\"48\"\r\n viewBox=\"0 0 24 24\"\r\n fill=\"none\"\r\n stroke=\"currentColor\"\r\n stroke-width=\"2\"\r\n stroke-linecap=\"round\"\r\n stroke-linejoin=\"round\"\r\n >\r\n <path\r\n d=\"M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z\"\r\n ></path>\r\n <line x1=\"12\" y1=\"9\" x2=\"12\" y2=\"13\"></line>\r\n <line x1=\"12\" y1=\"17\" x2=\"12.01\" y2=\"17\"></line>\r\n </svg>\r\n </div>\r\n <h2>API Quota Limit Exceeded</h2>\r\n <button class=\"close-btn\" (click)=\"closeApiKeyModal()\" aria-label=\"Close\">\r\n <svg\r\n xmlns=\"http://www.w3.org/2000/svg\"\r\n width=\"24\"\r\n height=\"24\"\r\n viewBox=\"0 0 24 24\"\r\n fill=\"none\"\r\n stroke=\"currentColor\"\r\n stroke-width=\"2\"\r\n stroke-linecap=\"round\"\r\n stroke-linejoin=\"round\"\r\n >\r\n <line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\"></line>\r\n <line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\"></line>\r\n </svg>\r\n </button>\r\n </div>\r\n <!-- Face++ modal moved outside to top-level sibling overlay -->\r\n\r\n <div class=\"api-key-modal-body\">\r\n <p class=\"modal-message\">\r\n The default API Ninjas key has reached its free monthly quota limit of\r\n <strong>3,000 API calls</strong>.\r\n </p>\r\n <p class=\"modal-instruction\">\r\n Please enter your own API Ninjas key below to continue using the profile\r\n comparison features:\r\n </p>\r\n\r\n <div class=\"api-key-input-group\">\r\n <input\r\n type=\"text\"\r\n [(ngModel)]=\"apiKeyInputValue\"\r\n placeholder=\"Paste your API Ninjas key here\"\r\n class=\"api-key-input\"\r\n autofocus\r\n />\r\n <button class=\"api-key-load-btn\" (click)=\"updateApiKey()\">\r\n Apply Key\r\n </button>\r\n </div>\r\n\r\n <div class=\"modal-hint\">\r\n <p>\r\n <svg\r\n xmlns=\"http://www.w3.org/2000/svg\"\r\n width=\"16\"\r\n height=\"16\"\r\n viewBox=\"0 0 24 24\"\r\n fill=\"none\"\r\n stroke=\"currentColor\"\r\n stroke-width=\"2\"\r\n stroke-linecap=\"round\"\r\n stroke-linejoin=\"round\"\r\n >\r\n <circle cx=\"12\" cy=\"12\" r=\"10\"></circle>\r\n <line x1=\"12\" y1=\"16\" x2=\"12\" y2=\"12\"></line>\r\n <line x1=\"12\" y1=\"8\" x2=\"12.01\" y2=\"8\"></line>\r\n </svg>\r\n Don't have an API key? Get your free key at\r\n <a href=\"https://api-ninjas.com/\" target=\"_blank\" rel=\"noopener\"\r\n >api-ninjas.com</a\r\n >\r\n </p>\r\n </div>\r\n </div>\r\n </div>\r\n</div>\r\n\r\n<!-- Face++ API Credentials Modal (Independent) -->\r\n<div class=\"api-key-modal-overlay facepp-overlay\" *ngIf=\"showFaceppKeyModal\">\r\n <div class=\"api-key-modal\" (click)=\"$event.stopPropagation()\">\r\n <div class=\"api-key-modal-header\">\r\n <div class=\"warning-icon\">\r\n <svg\r\n xmlns=\"http://www.w3.org/2000/svg\"\r\n width=\"48\"\r\n height=\"48\"\r\n viewBox=\"0 0 24 24\"\r\n fill=\"none\"\r\n stroke=\"currentColor\"\r\n stroke-width=\"2\"\r\n stroke-linecap=\"round\"\r\n stroke-linejoin=\"round\"\r\n >\r\n <path\r\n d=\"M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z\"\r\n ></path>\r\n <line x1=\"12\" y1=\"9\" x2=\"12\" y2=\"13\"></line>\r\n <line x1=\"12\" y1=\"17\" x2=\"12.01\" y2=\"17\"></line>\r\n </svg>\r\n </div>\r\n <h2>Face++ Credentials Required</h2>\r\n <button\r\n class=\"close-btn\"\r\n (click)=\"closeFaceppKeyModal()\"\r\n aria-label=\"Close\"\r\n >\r\n <svg\r\n xmlns=\"http://www.w3.org/2000/svg\"\r\n width=\"24\"\r\n height=\"24\"\r\n viewBox=\"0 0 24 24\"\r\n fill=\"none\"\r\n stroke=\"currentColor\"\r\n stroke-width=\"2\"\r\n stroke-linecap=\"round\"\r\n stroke-linejoin=\"round\"\r\n >\r\n <line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\"></line>\r\n <line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\"></line>\r\n </svg>\r\n </button>\r\n </div>\r\n\r\n <div class=\"api-key-modal-body\">\r\n <p class=\"modal-message\">\r\n The Face++ API returned an authorization or rate-limit error. Please\r\n enter your Face++ API Key and API Secret.\r\n </p>\r\n\r\n <div\r\n class=\"api-key-input-group\"\r\n style=\"flex-direction: column; align-items: stretch; gap: 10px\"\r\n >\r\n <input\r\n type=\"text\"\r\n [(ngModel)]=\"faceppKeyInputValue\"\r\n placeholder=\"Face++ API Key\"\r\n class=\"api-key-input\"\r\n />\r\n <input\r\n type=\"text\"\r\n [(ngModel)]=\"faceppSecretInputValue\"\r\n placeholder=\"Face++ API Secret\"\r\n class=\"api-key-input\"\r\n />\r\n <button class=\"api-key-load-btn\" (click)=\"updateFaceppCredentials()\">\r\n Apply Face++ Credentials\r\n </button>\r\n </div>\r\n </div>\r\n </div>\r\n</div>\r\n\r\n<!-- OpenAI API Key Modal (Independent) -->\r\n<div class=\"api-key-modal-overlay openai-overlay\" *ngIf=\"showOpenAIKeyModal\">\r\n <div class=\"api-key-modal\" (click)=\"$event.stopPropagation()\">\r\n <div class=\"api-key-modal-header\">\r\n <div class=\"warning-icon\">\r\n <svg\r\n xmlns=\"http://www.w3.org/2000/svg\"\r\n width=\"48\"\r\n height=\"48\"\r\n viewBox=\"0 0 24 24\"\r\n fill=\"none\"\r\n stroke=\"currentColor\"\r\n stroke-width=\"2\"\r\n stroke-linecap=\"round\"\r\n stroke-linejoin=\"round\"\r\n >\r\n <path\r\n d=\"M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z\"\r\n ></path>\r\n <line x1=\"12\" y1=\"9\" x2=\"12\" y2=\"13\"></line>\r\n <line x1=\"12\" y1=\"17\" x2=\"12.01\" y2=\"17\"></line>\r\n </svg>\r\n </div>\r\n <h2>OpenAI API Key Required</h2>\r\n <button\r\n class=\"close-btn\"\r\n (click)=\"closeOpenAIKeyModal()\"\r\n aria-label=\"Close\"\r\n >\r\n <svg\r\n xmlns=\"http://www.w3.org/2000/svg\"\r\n width=\"24\"\r\n height=\"24\"\r\n viewBox=\"0 0 24 24\"\r\n fill=\"none\"\r\n stroke=\"currentColor\"\r\n stroke-width=\"2\"\r\n stroke-linecap=\"round\"\r\n stroke-linejoin=\"round\"\r\n >\r\n <line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\"></line>\r\n <line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\"></line>\r\n </svg>\r\n </button>\r\n </div>\r\n\r\n <div class=\"api-key-modal-body\">\r\n <p class=\"modal-message\">\r\n The OpenAI API returned an authorization or rate-limit error. Please\r\n enter your OpenAI API key to continue using the text similarity features.\r\n </p>\r\n <p class=\"modal-instruction\">\r\n Text matching now uses OpenAI embeddings (text-embedding-3-small) for improved semantic similarity.\r\n </p>\r\n\r\n <div class=\"api-key-input-group\">\r\n <input\r\n type=\"text\"\r\n [(ngModel)]=\"openaiKeyInputValue\"\r\n placeholder=\"Paste your OpenAI API key here\"\r\n class=\"api-key-input\"\r\n autofocus\r\n />\r\n <button class=\"api-key-load-btn\" (click)=\"updateOpenAIKey()\">\r\n Apply OpenAI Key\r\n </button>\r\n </div>\r\n\r\n <div class=\"modal-hint\">\r\n <p>\r\n <svg\r\n xmlns=\"http://www.w3.org/2000/svg\"\r\n width=\"16\"\r\n height=\"16\"\r\n viewBox=\"0 0 24 24\"\r\n fill=\"none\"\r\n stroke=\"currentColor\"\r\n stroke-width=\"2\"\r\n stroke-linecap=\"round\"\r\n stroke-linejoin=\"round\"\r\n >\r\n <circle cx=\"12\" cy=\"12\" r=\"10\"></circle>\r\n <line x1=\"12\" y1=\"16\" x2=\"12\" y2=\"12\"></line>\r\n <line x1=\"12\" y1=\"8\" x2=\"12.01\" y2=\"8\"></line>\r\n </svg>\r\n Don't have an API key? Get your free key at\r\n <a href=\"https://platform.openai.com/api-keys\" target=\"_blank\" rel=\"noopener\"\r\n >platform.openai.com/api-keys</a\r\n >\r\n </p>\r\n </div>\r\n </div>\r\n </div>\r\n</div>\r\n\r\n<div class=\"blank-box\"></div>\r\n\r\n<div class=\"profile-screen\">\r\n <div class=\"profile-flex\">\r\n <div class=\"profile-img left\" [class.fade-all]=\"fadeAllEdges\">\r\n <img\r\n [src]=\"user1Image\"\r\n alt=\"User 1\"\r\n [style.transform]=\"user1Transform\"\r\n [style.object-position]=\"user1ObjectPosition\"\r\n (load)=\"onUserImageLoad(1, $event)\"\r\n />\r\n </div>\r\n <div class=\"profile-img right\" [class.fade-all]=\"fadeAllEdges\">\r\n <img\r\n [src]=\"user2Image\"\r\n alt=\"User 2\"\r\n [style.transform]=\"user2Transform\"\r\n [style.object-position]=\"user2ObjectPosition\"\r\n (load)=\"onUserImageLoad(2, $event)\"\r\n />\r\n <!-- [style.object-fit]=\"'cover'\" -->\r\n </div>\r\n </div>\r\n\r\n <div class=\"shape\">\r\n <div class=\"shape-bg\">\r\n <img src=\"./assets/s1.svg\" alt=\"Shape\" height=\"350\" class=\"shape-bg1\" />\r\n <img src=\"./assets/s2.svg\" alt=\"Shape\" height=\"350\" class=\"shape-bg2\" />\r\n </div>\r\n\r\n <div class=\"shape-text\">\r\n <div class=\"shape-text-left\">\r\n <div class=\"scroll-container\" #leftContainer>\r\n <ng-container\r\n *ngFor=\"\r\n let interest of alignedPerson1Interests.length > 0\r\n ? alignedPerson1Interests\r\n : displayPerson1Interests.length > 0\r\n ? displayPerson1Interests\r\n : person1Interests;\r\n let i = index\r\n \"\r\n >\r\n <p\r\n class=\"shape-p\"\r\n *ngIf=\"!centerItem.includes(interest)\"\r\n [ngClass]=\"{'dash-item': interest === '-', 'spacer-item': interest === '----', 'aligned-item': interest !== '-' && interest !== '----'}\"\r\n >\r\n {{ interest === \"-\" ? \"\u2014\" : interest }}\r\n </p>\r\n </ng-container>\r\n </div>\r\n <h2 class=\"shape-h2\" (click)=\"onViewProfile()\">View Profile</h2>\r\n </div>\r\n\r\n <div class=\"shape-text-center\">\r\n <ng-container *ngFor=\"let item of centerItem\">\r\n <p class=\"shape-p-center\">{{ item }}</p>\r\n </ng-container>\r\n </div>\r\n <div class=\"shape-text-right\">\r\n <div class=\"scroll-container\" #rightContainer>\r\n <ng-container\r\n *ngFor=\"\r\n let interest of alignedPerson2Interests.length > 0\r\n ? alignedPerson2Interests\r\n : displayPerson2Interests.length > 0\r\n ? displayPerson2Interests\r\n : person2Interests;\r\n let i = index\r\n \"\r\n >\r\n <p\r\n class=\"shape-p-right\"\r\n *ngIf=\"!centerItem.includes(interest)\"\r\n [ngClass]=\"{'dash-item': interest === '-', 'spacer-item': interest === '----', 'aligned-item': interest !== '-' && interest !== '----'}\"\r\n >\r\n {{ interest === \"-\" ? \"\u2014\" : interest }}\r\n </p>\r\n </ng-container>\r\n </div>\r\n <h2 class=\"shape-h2-right\" (click)=\"onViewProfile()\">View Profile</h2>\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <!-- Loading indicator for alignment process -->\r\n <div *ngIf=\"isAligning\" class=\"loading-indicator\">\r\n <div class=\"loading-spinner\"></div>\r\n </div>\r\n</div>\r\n\r\n\r\n", styles: [".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%)}.profile-screen{width:320px;max-width:100%;margin:0 auto;overflow:hidden;position:relative}.blank-box{background-color:#000;height:20px;width:100%;position:absolute;left:0;top:26%;transform:translateY(-50%);z-index:-5}.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}.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 img{cursor:grab}.shape-bg img: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"] }]
|
|
2745
|
-
}], ctorParameters: () => [{ type:
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
}], faceplusSecret: [{
|
|
2752
|
-
type: Input
|
|
2753
|
-
}], openaiApiKey: [{
|
|
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"] }]
|
|
3026
|
+
}], ctorParameters: () => [{ type: ProfileComparisonBackendService }, { type: i0.Renderer2 }, { type: FileConversionService }, { type: ImageCompressionService }, { type: i0.ChangeDetectorRef }, { type: undefined, decorators: [{
|
|
3027
|
+
type: Optional
|
|
3028
|
+
}, {
|
|
3029
|
+
type: Inject,
|
|
3030
|
+
args: [PROFILE_COMPARISON_VERBOSE_LOGGING]
|
|
3031
|
+
}] }], propDecorators: { config: [{
|
|
2754
3032
|
type: Input
|
|
2755
3033
|
}], fadeAllEdges: [{
|
|
2756
3034
|
type: Input
|
|
2757
3035
|
}], matrixDataChange: [{
|
|
2758
3036
|
type: Output
|
|
3037
|
+
}], rawLLMOutputChange: [{
|
|
3038
|
+
type: Output
|
|
3039
|
+
}], viewProfileClick: [{
|
|
3040
|
+
type: Output
|
|
2759
3041
|
}], leftContainer: [{
|
|
2760
3042
|
type: ViewChild,
|
|
2761
3043
|
args: ['leftContainer']
|
|
2762
3044
|
}], rightContainer: [{
|
|
2763
3045
|
type: ViewChild,
|
|
2764
3046
|
args: ['rightContainer']
|
|
2765
|
-
}]
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
qualityStart: CustomInputComponent.QUALITY_START,
|
|
2796
|
-
qualityMin: CustomInputComponent.QUALITY_MIN,
|
|
2797
|
-
qualityStep: CustomInputComponent.QUALITY_STEP,
|
|
2798
|
-
maxBytes: CustomInputComponent.MAX_BYTES,
|
|
2799
|
-
downscaleStep: CustomInputComponent.DOWNSCALE_STEP,
|
|
2800
|
-
minWidth: CustomInputComponent.MIN_WIDTH,
|
|
2801
|
-
minHeight: CustomInputComponent.MIN_HEIGHT,
|
|
2802
|
-
};
|
|
2803
|
-
constructor(router) {
|
|
2804
|
-
this.router = router;
|
|
2805
|
-
}
|
|
2806
|
-
async onUser1FileChange(event) {
|
|
2807
|
-
const input = event.target;
|
|
2808
|
-
const file = input.files && input.files.length ? input.files[0] : null;
|
|
2809
|
-
if (!file) {
|
|
2810
|
-
this.user1File = null;
|
|
2811
|
-
this.user1Preview = null;
|
|
2812
|
-
return;
|
|
2813
|
-
}
|
|
2814
|
-
try {
|
|
2815
|
-
const { file: compressed, dataUrl } = await firstValueFrom(this.compressImageFile(file));
|
|
2816
|
-
this.user1File = compressed;
|
|
2817
|
-
this.user1Preview = dataUrl;
|
|
2818
|
-
}
|
|
2819
|
-
catch (e) {
|
|
2820
|
-
console.error('User1 image compression failed:', e);
|
|
2821
|
-
// Fallback to original if compression fails
|
|
2822
|
-
this.user1File = file;
|
|
2823
|
-
this.previewFile(file, (dataUrl) => (this.user1Preview = dataUrl));
|
|
2824
|
-
}
|
|
2825
|
-
}
|
|
2826
|
-
async onUser2FileChange(event) {
|
|
2827
|
-
const input = event.target;
|
|
2828
|
-
const file = input.files && input.files.length ? input.files[0] : null;
|
|
2829
|
-
if (!file) {
|
|
2830
|
-
this.user2File = null;
|
|
2831
|
-
this.user2Preview = null;
|
|
2832
|
-
return;
|
|
2833
|
-
}
|
|
2834
|
-
try {
|
|
2835
|
-
const { file: compressed, dataUrl } = await firstValueFrom(this.compressImageFile(file));
|
|
2836
|
-
this.user2File = compressed;
|
|
2837
|
-
this.user2Preview = dataUrl;
|
|
2838
|
-
}
|
|
2839
|
-
catch (e) {
|
|
2840
|
-
console.error('User2 image compression failed:', e);
|
|
2841
|
-
// Fallback to original if compression fails
|
|
2842
|
-
this.user2File = file;
|
|
2843
|
-
this.previewFile(file, (dataUrl) => (this.user2Preview = dataUrl));
|
|
2844
|
-
}
|
|
2845
|
-
}
|
|
2846
|
-
async saveAndView() {
|
|
2847
|
-
this.error = null;
|
|
2848
|
-
this.saving = true;
|
|
2849
|
-
try {
|
|
2850
|
-
const img1 = this.user1File
|
|
2851
|
-
? await firstValueFrom(this.fileToDataURL(this.user1File))
|
|
2852
|
-
: this.user1Preview || '';
|
|
2853
|
-
const img2 = this.user2File
|
|
2854
|
-
? await firstValueFrom(this.fileToDataURL(this.user2File))
|
|
2855
|
-
: this.user2Preview || '';
|
|
2856
|
-
const cfg = {
|
|
2857
|
-
person1Interests: this.toList(this.person1Text),
|
|
2858
|
-
person2Interests: this.toList(this.person2Text),
|
|
2859
|
-
person3Interests: this.toList(this.person3Text),
|
|
2860
|
-
user1Image: img1,
|
|
2861
|
-
user2Image: img2,
|
|
2862
|
-
};
|
|
2863
|
-
localStorage.setItem('customProfileConfig', JSON.stringify(cfg));
|
|
2864
|
-
// Navigate to home to render with the new config
|
|
2865
|
-
await this.router.navigateByUrl('/');
|
|
2866
|
-
window.location.reload();
|
|
2867
|
-
}
|
|
2868
|
-
catch (e) {
|
|
2869
|
-
this.error = e?.message || 'Failed to save configuration';
|
|
2870
|
-
}
|
|
2871
|
-
finally {
|
|
2872
|
-
this.saving = false;
|
|
2873
|
-
}
|
|
2874
|
-
}
|
|
2875
|
-
clearAll() {
|
|
2876
|
-
this.person1Text = '';
|
|
2877
|
-
this.person2Text = '';
|
|
2878
|
-
this.person3Text = '';
|
|
2879
|
-
this.user1File = null;
|
|
2880
|
-
this.user2File = null;
|
|
2881
|
-
this.user1Preview = null;
|
|
2882
|
-
this.user2Preview = null;
|
|
2883
|
-
try {
|
|
2884
|
-
localStorage.removeItem('customProfileConfig');
|
|
2885
|
-
window.location.reload();
|
|
2886
|
-
}
|
|
2887
|
-
catch { }
|
|
2888
|
-
}
|
|
2889
|
-
// Close the overlay and return to the main view
|
|
2890
|
-
close() {
|
|
2891
|
-
this.router.navigateByUrl('/');
|
|
2892
|
-
}
|
|
2893
|
-
toList(block) {
|
|
2894
|
-
return (block || '')
|
|
2895
|
-
.split(/[\n,]+/)
|
|
2896
|
-
.map((s) => s.trim())
|
|
2897
|
-
.filter((s) => !!s);
|
|
2898
|
-
}
|
|
2899
|
-
fileToDataURL(file) {
|
|
2900
|
-
return new Observable(observer => {
|
|
2901
|
-
const reader = new FileReader();
|
|
2902
|
-
reader.onload = () => {
|
|
2903
|
-
observer.next(String(reader.result));
|
|
2904
|
-
observer.complete();
|
|
2905
|
-
};
|
|
2906
|
-
reader.onerror = (err) => observer.error(err);
|
|
2907
|
-
reader.readAsDataURL(file);
|
|
2908
|
-
});
|
|
2909
|
-
}
|
|
2910
|
-
previewFile(file, cb) {
|
|
2911
|
-
if (!file) {
|
|
2912
|
-
cb(null);
|
|
2913
|
-
return;
|
|
2914
|
-
}
|
|
2915
|
-
const reader = new FileReader();
|
|
2916
|
-
reader.onload = () => cb(String(reader.result));
|
|
2917
|
-
reader.readAsDataURL(file);
|
|
2918
|
-
}
|
|
2919
|
-
// Compress an image file according to compressionConfig (or overrides)
|
|
2920
|
-
compressImageFile(file, overrides = {}) {
|
|
2921
|
-
const cfg = { ...this.compressionConfig, ...overrides };
|
|
2922
|
-
return this.loadImageFromFile(file).pipe(switchMap((img) => {
|
|
2923
|
-
let width = img.naturalWidth || img.width;
|
|
2924
|
-
let height = img.naturalHeight || img.height;
|
|
2925
|
-
// Initial fit within max dimensions
|
|
2926
|
-
const scale = Math.min(cfg.maxWidth / width, cfg.maxHeight / height, 1);
|
|
2927
|
-
width = Math.max(1, Math.round(width * scale));
|
|
2928
|
-
height = Math.max(1, Math.round(height * scale));
|
|
2929
|
-
const canvas = document.createElement('canvas');
|
|
2930
|
-
const ctx = canvas.getContext('2d');
|
|
2931
|
-
const render = (w, h) => {
|
|
2932
|
-
canvas.width = w;
|
|
2933
|
-
canvas.height = h;
|
|
2934
|
-
ctx.clearRect(0, 0, w, h);
|
|
2935
|
-
ctx.drawImage(img, 0, 0, w, h);
|
|
2936
|
-
};
|
|
2937
|
-
const process = (w, h, q, attempts) => {
|
|
2938
|
-
render(w, h);
|
|
2939
|
-
return this.canvasToBlob(canvas, cfg.format, q).pipe(switchMap((blob) => {
|
|
2940
|
-
if (blob.size > cfg.maxBytes && attempts < 25) {
|
|
2941
|
-
if (q > cfg.qualityMin + 0.005) {
|
|
2942
|
-
return process(w, h, Math.max(cfg.qualityMin, q - cfg.qualityStep), attempts + 1);
|
|
2943
|
-
}
|
|
2944
|
-
else {
|
|
2945
|
-
const nextW = Math.max(Math.round(w * cfg.downscaleStep), cfg.minWidth);
|
|
2946
|
-
const nextH = Math.max(Math.round(h * cfg.downscaleStep), cfg.minHeight);
|
|
2947
|
-
if (nextW === w && nextH === h) {
|
|
2948
|
-
return of(blob);
|
|
2949
|
-
}
|
|
2950
|
-
return process(nextW, nextH, cfg.qualityStart, attempts + 1);
|
|
2951
|
-
}
|
|
2952
|
-
}
|
|
2953
|
-
return of(blob);
|
|
2954
|
-
}));
|
|
2955
|
-
};
|
|
2956
|
-
return process(width, height, cfg.qualityStart, 0).pipe(switchMap((finalBlob) => {
|
|
2957
|
-
const newName = this.renameFileForFormat(file.name, cfg.format);
|
|
2958
|
-
const compressedFile = new File([finalBlob], newName, {
|
|
2959
|
-
type: cfg.format,
|
|
2960
|
-
lastModified: Date.now(),
|
|
2961
|
-
});
|
|
2962
|
-
return this.blobToDataURL(finalBlob).pipe(map((dataUrl) => ({ file: compressedFile, dataUrl })));
|
|
2963
|
-
}));
|
|
2964
|
-
}));
|
|
2965
|
-
}
|
|
2966
|
-
loadImageFromFile(file) {
|
|
2967
|
-
return new Observable(observer => {
|
|
2968
|
-
const url = URL.createObjectURL(file);
|
|
2969
|
-
const img = new Image();
|
|
2970
|
-
img.onload = () => {
|
|
2971
|
-
URL.revokeObjectURL(url);
|
|
2972
|
-
observer.next(img);
|
|
2973
|
-
observer.complete();
|
|
2974
|
-
};
|
|
2975
|
-
img.onerror = () => {
|
|
2976
|
-
URL.revokeObjectURL(url);
|
|
2977
|
-
observer.error(new Error('Failed to load image for compression'));
|
|
2978
|
-
};
|
|
2979
|
-
img.src = url;
|
|
2980
|
-
});
|
|
2981
|
-
}
|
|
2982
|
-
canvasToBlob(canvas, type, quality) {
|
|
2983
|
-
return new Observable(observer => {
|
|
2984
|
-
if (canvas.toBlob) {
|
|
2985
|
-
canvas.toBlob((blob) => {
|
|
2986
|
-
if (blob) {
|
|
2987
|
-
observer.next(blob);
|
|
2988
|
-
observer.complete();
|
|
2989
|
-
}
|
|
2990
|
-
else {
|
|
2991
|
-
observer.error(new Error('Canvas.toBlob returned null'));
|
|
2992
|
-
}
|
|
2993
|
-
}, type, quality);
|
|
2994
|
-
}
|
|
2995
|
-
else {
|
|
2996
|
-
// Fallback via DataURL
|
|
2997
|
-
try {
|
|
2998
|
-
const dataUrl = canvas.toDataURL(type, quality);
|
|
2999
|
-
const base64 = dataUrl.split(',')[1] || '';
|
|
3000
|
-
const binary = atob(base64);
|
|
3001
|
-
const len = binary.length;
|
|
3002
|
-
const u8 = new Uint8Array(len);
|
|
3003
|
-
for (let i = 0; i < len; i++)
|
|
3004
|
-
u8[i] = binary.charCodeAt(i);
|
|
3005
|
-
observer.next(new Blob([u8], { type }));
|
|
3006
|
-
observer.complete();
|
|
3007
|
-
}
|
|
3008
|
-
catch (e) {
|
|
3009
|
-
observer.error(e);
|
|
3010
|
-
}
|
|
3011
|
-
}
|
|
3012
|
-
});
|
|
3013
|
-
}
|
|
3014
|
-
blobToDataURL(blob) {
|
|
3015
|
-
return new Observable(observer => {
|
|
3016
|
-
const reader = new FileReader();
|
|
3017
|
-
reader.onload = () => {
|
|
3018
|
-
observer.next(String(reader.result));
|
|
3019
|
-
observer.complete();
|
|
3020
|
-
};
|
|
3021
|
-
reader.onerror = (err) => observer.error(err);
|
|
3022
|
-
reader.readAsDataURL(blob);
|
|
3023
|
-
});
|
|
3024
|
-
}
|
|
3025
|
-
renameFileForFormat(name, mime) {
|
|
3026
|
-
const ext = mime === 'image/webp' ? '.webp' : mime === 'image/png' ? '.png' : '.jpg';
|
|
3027
|
-
return name.replace(/\.[^/.]+$/, '') + ext;
|
|
3028
|
-
}
|
|
3029
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: CustomInputComponent, deps: [{ token: i1$1.Router }], target: i0.ɵɵFactoryTarget.Component });
|
|
3030
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.16", type: CustomInputComponent, isStandalone: true, selector: "lib-custom-input", ngImport: i0, template: "<div class=\"try-overlay\">\r\n <div class=\"try-container\" (click)=\"$event.stopPropagation()\">\r\n <header class=\"try-header\">\r\n <h1>Try Your Own Data</h1>\r\n <a class=\"close-btn\" (click)=\"close()\" aria-label=\"Close\">\u00D7</a>\r\n </header>\r\n <p class=\"lead\">\r\n Paste interests (one per line) and upload two images. Click \"Save & View\"\r\n to preview in the main comparison view.\r\n </p>\r\n\r\n <section class=\"try-grid\">\r\n <div class=\"card\">\r\n <h2>Person 1</h2>\r\n <div class=\"field\">\r\n <label>Image</label>\r\n <input\r\n type=\"file\"\r\n accept=\"image/*\"\r\n (change)=\"onUser1FileChange($event)\"\r\n />\r\n <div class=\"preview\" *ngIf=\"user1Preview\">\r\n <img [src]=\"user1Preview\" alt=\"User 1 preview\" />\r\n </div>\r\n </div>\r\n <div class=\"field\">\r\n <label>Interests (one per line)</label>\r\n <textarea\r\n [(ngModel)]=\"person1Text\"\r\n rows=\"10\"\r\n placeholder=\"e.g.Photography,Travel,Hiking\"\r\n ></textarea>\r\n </div>\r\n </div>\r\n\r\n <div class=\"card\">\r\n <h2>Person 2</h2>\r\n <div class=\"field\">\r\n <label>Image</label>\r\n <input\r\n type=\"file\"\r\n accept=\"image/*\"\r\n (change)=\"onUser2FileChange($event)\"\r\n />\r\n <div class=\"preview\" *ngIf=\"user2Preview\">\r\n <img [src]=\"user2Preview\" alt=\"User 2 preview\" />\r\n </div>\r\n </div>\r\n <div class=\"field\">\r\n <label>Interests (one per line)</label>\r\n <textarea\r\n [(ngModel)]=\"person2Text\"\r\n rows=\"10\"\r\n placeholder=\"e.g.Music,Movies,Soccer\"\r\n ></textarea>\r\n </div>\r\n </div>\r\n\r\n <!-- <div class=\"card\">\r\n <h2>Person 3 (Optional)</h2>\r\n <div class=\"field\">\r\n <label>Interests (one per line)</label>\r\n <textarea [(ngModel)]=\"person3Text\" rows=\"10\" placeholder=\"Optional third list\"></textarea>\r\n </div>\r\n <div class=\"hint\">\r\n <p>Tip: Leave this empty if you only want to compare two lists.</p>\r\n </div>\r\n </div> -->\r\n </section>\r\n\r\n <footer class=\"try-actions\">\r\n <button class=\"btn primary\" (click)=\"saveAndView()\" [disabled]=\"saving\">\r\n {{ saving ? \"Saving...\" : \"Save & View\" }}\r\n </button>\r\n <button class=\"btn\" (click)=\"clearAll()\" [disabled]=\"saving\">\r\n Clear\r\n </button>\r\n <a class=\"btn link\" (click)=\"close()\">Back to Home</a>\r\n </footer>\r\n\r\n <p class=\"error\" *ngIf=\"error\">{{ error }}</p>\r\n </div>\r\n</div>\r\n", styles: [".try-overlay{position:fixed;inset:0;background:#0a0e17bf;z-index:2000;overflow:auto}.try-container{box-sizing:border-box;max-width:900px;margin:40px auto;background:#0f172a;color:#e2e8f0;border-radius:12px;padding:24px 28px;box-shadow:0 12px 40px #00000059}.try-header{display:flex;align-items:center;justify-content:space-between;gap:16px;margin-bottom:8px}.try-header h1{margin:0;font-size:22px}.close-btn{color:#94a3b8;text-decoration:none;font-size:24px;line-height:1;border:1px solid #334155;border-radius:8px;padding:2px 10px;background:#0b1220;cursor:pointer}.lead{margin:0 0 12px;color:#cbd5e1}.try-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:16px}.card{background:#0b1220;border:1px solid #334155;border-radius:12px;padding:16px;box-shadow:0 4px 12px #0000000a}.field{margin:12px 0}.field label{font-weight:600;display:block;margin-bottom:6px}.field textarea{background:#0a1020;color:#e2e8f0;border:1px solid #334155;border-radius:8px;padding:8px 10px;resize:vertical;min-height:160px}.preview{margin-top:8px}.preview img{width:100%;max-height:220px;object-fit:cover;border-radius:8px;border:1px solid #334155}.try-actions{display:flex;gap:10px;margin-top:18px;align-items:center}.btn{background:#2563eb;color:#fff;padding:8px 14px;border-radius:8px;border:none;cursor:pointer;text-decoration:none}.btn.primary{background:#2563eb;color:#fff;border-color:transparent}.btn[disabled]{opacity:.6;cursor:not-allowed}.error{color:#b91c1c;font-weight:600;margin-top:12px}.hint{color:#6b7280;font-size:13px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i3.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i3.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }] });
|
|
3031
|
-
}
|
|
3032
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: CustomInputComponent, decorators: [{
|
|
3033
|
-
type: Component,
|
|
3034
|
-
args: [{ selector: 'lib-custom-input', standalone: true, imports: [CommonModule, FormsModule], template: "<div class=\"try-overlay\">\r\n <div class=\"try-container\" (click)=\"$event.stopPropagation()\">\r\n <header class=\"try-header\">\r\n <h1>Try Your Own Data</h1>\r\n <a class=\"close-btn\" (click)=\"close()\" aria-label=\"Close\">\u00D7</a>\r\n </header>\r\n <p class=\"lead\">\r\n Paste interests (one per line) and upload two images. Click \"Save & View\"\r\n to preview in the main comparison view.\r\n </p>\r\n\r\n <section class=\"try-grid\">\r\n <div class=\"card\">\r\n <h2>Person 1</h2>\r\n <div class=\"field\">\r\n <label>Image</label>\r\n <input\r\n type=\"file\"\r\n accept=\"image/*\"\r\n (change)=\"onUser1FileChange($event)\"\r\n />\r\n <div class=\"preview\" *ngIf=\"user1Preview\">\r\n <img [src]=\"user1Preview\" alt=\"User 1 preview\" />\r\n </div>\r\n </div>\r\n <div class=\"field\">\r\n <label>Interests (one per line)</label>\r\n <textarea\r\n [(ngModel)]=\"person1Text\"\r\n rows=\"10\"\r\n placeholder=\"e.g.Photography,Travel,Hiking\"\r\n ></textarea>\r\n </div>\r\n </div>\r\n\r\n <div class=\"card\">\r\n <h2>Person 2</h2>\r\n <div class=\"field\">\r\n <label>Image</label>\r\n <input\r\n type=\"file\"\r\n accept=\"image/*\"\r\n (change)=\"onUser2FileChange($event)\"\r\n />\r\n <div class=\"preview\" *ngIf=\"user2Preview\">\r\n <img [src]=\"user2Preview\" alt=\"User 2 preview\" />\r\n </div>\r\n </div>\r\n <div class=\"field\">\r\n <label>Interests (one per line)</label>\r\n <textarea\r\n [(ngModel)]=\"person2Text\"\r\n rows=\"10\"\r\n placeholder=\"e.g.Music,Movies,Soccer\"\r\n ></textarea>\r\n </div>\r\n </div>\r\n\r\n <!-- <div class=\"card\">\r\n <h2>Person 3 (Optional)</h2>\r\n <div class=\"field\">\r\n <label>Interests (one per line)</label>\r\n <textarea [(ngModel)]=\"person3Text\" rows=\"10\" placeholder=\"Optional third list\"></textarea>\r\n </div>\r\n <div class=\"hint\">\r\n <p>Tip: Leave this empty if you only want to compare two lists.</p>\r\n </div>\r\n </div> -->\r\n </section>\r\n\r\n <footer class=\"try-actions\">\r\n <button class=\"btn primary\" (click)=\"saveAndView()\" [disabled]=\"saving\">\r\n {{ saving ? \"Saving...\" : \"Save & View\" }}\r\n </button>\r\n <button class=\"btn\" (click)=\"clearAll()\" [disabled]=\"saving\">\r\n Clear\r\n </button>\r\n <a class=\"btn link\" (click)=\"close()\">Back to Home</a>\r\n </footer>\r\n\r\n <p class=\"error\" *ngIf=\"error\">{{ error }}</p>\r\n </div>\r\n</div>\r\n", styles: [".try-overlay{position:fixed;inset:0;background:#0a0e17bf;z-index:2000;overflow:auto}.try-container{box-sizing:border-box;max-width:900px;margin:40px auto;background:#0f172a;color:#e2e8f0;border-radius:12px;padding:24px 28px;box-shadow:0 12px 40px #00000059}.try-header{display:flex;align-items:center;justify-content:space-between;gap:16px;margin-bottom:8px}.try-header h1{margin:0;font-size:22px}.close-btn{color:#94a3b8;text-decoration:none;font-size:24px;line-height:1;border:1px solid #334155;border-radius:8px;padding:2px 10px;background:#0b1220;cursor:pointer}.lead{margin:0 0 12px;color:#cbd5e1}.try-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:16px}.card{background:#0b1220;border:1px solid #334155;border-radius:12px;padding:16px;box-shadow:0 4px 12px #0000000a}.field{margin:12px 0}.field label{font-weight:600;display:block;margin-bottom:6px}.field textarea{background:#0a1020;color:#e2e8f0;border:1px solid #334155;border-radius:8px;padding:8px 10px;resize:vertical;min-height:160px}.preview{margin-top:8px}.preview img{width:100%;max-height:220px;object-fit:cover;border-radius:8px;border:1px solid #334155}.try-actions{display:flex;gap:10px;margin-top:18px;align-items:center}.btn{background:#2563eb;color:#fff;padding:8px 14px;border-radius:8px;border:none;cursor:pointer;text-decoration:none}.btn.primary{background:#2563eb;color:#fff;border-color:transparent}.btn[disabled]{opacity:.6;cursor:not-allowed}.error{color:#b91c1c;font-weight:600;margin-top:12px}.hint{color:#6b7280;font-size:13px}\n"] }]
|
|
3035
|
-
}], ctorParameters: () => [{ type: i1$1.Router }] });
|
|
3036
|
-
|
|
3037
|
-
class DocumentationComponent {
|
|
3038
|
-
openaiEmbeddingService;
|
|
3039
|
-
profileService;
|
|
3040
|
-
router;
|
|
3041
|
-
// API key state
|
|
3042
|
-
apiKeyInput = '';
|
|
3043
|
-
// Embedding similarity demo
|
|
3044
|
-
textA = 'football';
|
|
3045
|
-
textB = 'soccer';
|
|
3046
|
-
embeddingScore = null;
|
|
3047
|
-
embeddingLoading = false;
|
|
3048
|
-
embeddingError = null;
|
|
3049
|
-
// Best matching pairs demo
|
|
3050
|
-
listAInput = 'music\nphotography\ntravel\nrock climbing';
|
|
3051
|
-
listBInput = 'hiking\nsoccer\nphotograph\nmovies';
|
|
3052
|
-
matchingPairs = [];
|
|
3053
|
-
pairsLoading = false;
|
|
3054
|
-
pairsError = null;
|
|
3055
|
-
// Face detect demo
|
|
3056
|
-
faceFile = null;
|
|
3057
|
-
faceResult = null;
|
|
3058
|
-
faceLoading = false;
|
|
3059
|
-
faceError = null;
|
|
3060
|
-
// Text similarity via API Ninjas
|
|
3061
|
-
compareA = 'football';
|
|
3062
|
-
compareB = 'soccer';
|
|
3063
|
-
apiSimilarity = null;
|
|
3064
|
-
apiLoading = false;
|
|
3065
|
-
apiError = null;
|
|
3066
|
-
// OpenAI key state
|
|
3067
|
-
openaiKeyInput = '';
|
|
3068
|
-
defaultApiNinjasKey = '';
|
|
3069
|
-
defaultOpenaiApiKey = '';
|
|
3070
|
-
constructor(openaiEmbeddingService, profileService, router) {
|
|
3071
|
-
this.openaiEmbeddingService = openaiEmbeddingService;
|
|
3072
|
-
this.profileService = profileService;
|
|
3073
|
-
this.router = router;
|
|
3074
|
-
}
|
|
3075
|
-
ngOnInit() {
|
|
3076
|
-
// Prefill API key from env or localStorage
|
|
3077
|
-
const saved = this.getStoredKey();
|
|
3078
|
-
if (saved) {
|
|
3079
|
-
this.apiKeyInput = saved;
|
|
3080
|
-
}
|
|
3081
|
-
else {
|
|
3082
|
-
this.apiKeyInput = this.defaultApiNinjasKey || '';
|
|
3083
|
-
}
|
|
3084
|
-
}
|
|
3085
|
-
// --- API Key helpers ---
|
|
3086
|
-
getActiveApiKey() {
|
|
3087
|
-
return this.apiKeyInput?.trim() || this.defaultApiNinjasKey;
|
|
3088
|
-
}
|
|
3089
|
-
saveKey() {
|
|
3090
|
-
try {
|
|
3091
|
-
localStorage.setItem('apiNinjasCustomKey', this.apiKeyInput.trim());
|
|
3092
|
-
}
|
|
3093
|
-
catch (e) {
|
|
3094
|
-
console.warn('Could not persist API key', e);
|
|
3095
|
-
}
|
|
3096
|
-
}
|
|
3097
|
-
getStoredKey() {
|
|
3098
|
-
try {
|
|
3099
|
-
return localStorage.getItem('apiNinjasCustomKey');
|
|
3100
|
-
}
|
|
3101
|
-
catch {
|
|
3102
|
-
return null;
|
|
3103
|
-
}
|
|
3104
|
-
}
|
|
3105
|
-
// --- OpenAI Key helpers ---
|
|
3106
|
-
getActiveOpenAIKey() {
|
|
3107
|
-
return this.openaiKeyInput?.trim() || this.defaultOpenaiApiKey;
|
|
3108
|
-
}
|
|
3109
|
-
saveOpenAIKey() {
|
|
3110
|
-
try {
|
|
3111
|
-
localStorage.setItem('openaiCustomKey', this.openaiKeyInput.trim());
|
|
3112
|
-
}
|
|
3113
|
-
catch (e) {
|
|
3114
|
-
console.warn('Could not persist OpenAI key', e);
|
|
3115
|
-
}
|
|
3116
|
-
}
|
|
3117
|
-
// --- Embedding similarity demo (now uses OpenAI) ---
|
|
3118
|
-
runEmbeddingSimilarity() {
|
|
3119
|
-
this.embeddingLoading = true;
|
|
3120
|
-
this.embeddingError = null;
|
|
3121
|
-
this.embeddingScore = null;
|
|
3122
|
-
this.openaiEmbeddingService
|
|
3123
|
-
.calculateSimilarity(this.textA, this.textB, this.getActiveOpenAIKey())
|
|
3124
|
-
.subscribe({
|
|
3125
|
-
next: (score) => {
|
|
3126
|
-
this.embeddingScore = score;
|
|
3127
|
-
this.embeddingLoading = false;
|
|
3128
|
-
},
|
|
3129
|
-
error: (err) => {
|
|
3130
|
-
this.embeddingError = err?.message || 'Failed to compute similarity';
|
|
3131
|
-
this.embeddingLoading = false;
|
|
3132
|
-
},
|
|
3133
|
-
});
|
|
3134
|
-
}
|
|
3135
|
-
// --- Best matching pairs demo ---
|
|
3136
|
-
runBestPairs() {
|
|
3137
|
-
const listA = this.listAInput
|
|
3138
|
-
.split('\n')
|
|
3139
|
-
.map((s) => s.trim())
|
|
3140
|
-
.filter(Boolean);
|
|
3141
|
-
const listB = this.listBInput
|
|
3142
|
-
.split('\n')
|
|
3143
|
-
.map((s) => s.trim())
|
|
3144
|
-
.filter(Boolean);
|
|
3145
|
-
if (listA.length === 0 || listB.length === 0) {
|
|
3146
|
-
this.pairsError = 'Both lists must contain at least one item.';
|
|
3147
|
-
return;
|
|
3148
|
-
}
|
|
3149
|
-
this.pairsLoading = true;
|
|
3150
|
-
this.pairsError = null;
|
|
3151
|
-
this.matchingPairs = [];
|
|
3152
|
-
// Note: Best matching pairs demo is currently disabled as it's not needed for showcase functionality
|
|
3153
|
-
// The OpenAI service focuses on core text similarity for the main alignment algorithm
|
|
3154
|
-
this.pairsError = 'This demo is currently disabled. The showcase uses OpenAI embeddings for core text similarity.';
|
|
3155
|
-
this.pairsLoading = false;
|
|
3156
|
-
/* Original TensorFlow implementation:
|
|
3157
|
-
this.embeddingService
|
|
3158
|
-
.findBestMatchingPairs(listA, listB, 0.15)
|
|
3159
|
-
.subscribe({
|
|
3160
|
-
next: (pairs: IWordPair[]) => {
|
|
3161
|
-
this.matchingPairs = pairs;
|
|
3162
|
-
this.pairsLoading = false;
|
|
3163
|
-
},
|
|
3164
|
-
error: (err: Error | HttpErrorResponse) => {
|
|
3165
|
-
this.pairsError = (err as Error)?.message || 'Failed to compute pairs';
|
|
3166
|
-
this.pairsLoading = false;
|
|
3167
|
-
},
|
|
3168
|
-
});
|
|
3169
|
-
*/
|
|
3170
|
-
}
|
|
3171
|
-
// --- Face detection demo ---
|
|
3172
|
-
onFaceFileChange(event) {
|
|
3173
|
-
const input = event.target;
|
|
3174
|
-
const file = input.files && input.files.length ? input.files[0] : null;
|
|
3175
|
-
this.faceFile = file;
|
|
3176
|
-
}
|
|
3177
|
-
runFaceDetect() {
|
|
3178
|
-
this.faceResult = null;
|
|
3179
|
-
this.faceError = null;
|
|
3180
|
-
if (!this.faceFile) {
|
|
3181
|
-
this.faceError = 'Please choose an image file.';
|
|
3182
|
-
return;
|
|
3183
|
-
}
|
|
3184
|
-
this.faceLoading = true;
|
|
3185
|
-
this.profileService.detectFace(this.faceFile).subscribe({
|
|
3186
|
-
next: (res) => {
|
|
3187
|
-
this.faceResult = res;
|
|
3188
|
-
this.faceLoading = false;
|
|
3189
|
-
},
|
|
3190
|
-
error: (err) => {
|
|
3191
|
-
this.faceError = err?.error?.message || err?.message || 'Face detection failed';
|
|
3192
|
-
this.faceLoading = false;
|
|
3193
|
-
}
|
|
3194
|
-
});
|
|
3195
|
-
}
|
|
3196
|
-
// --- API Ninjas text similarity demo ---
|
|
3197
|
-
runApiSimilarity() {
|
|
3198
|
-
this.apiSimilarity = null;
|
|
3199
|
-
this.apiError = null;
|
|
3200
|
-
this.apiLoading = true;
|
|
3201
|
-
this.profileService
|
|
3202
|
-
.compareInterests(this.compareA, this.compareB, this.getActiveApiKey())
|
|
3203
|
-
.subscribe({
|
|
3204
|
-
next: (res) => {
|
|
3205
|
-
this.apiSimilarity = res.similarity;
|
|
3206
|
-
this.apiLoading = false;
|
|
3207
|
-
},
|
|
3208
|
-
error: (err) => {
|
|
3209
|
-
this.apiError = err?.error?.message || err?.message || 'API call failed';
|
|
3210
|
-
this.apiLoading = false;
|
|
3211
|
-
},
|
|
3212
|
-
});
|
|
3213
|
-
}
|
|
3214
|
-
// --- Helper label/format methods to simplify template expressions ---
|
|
3215
|
-
similarityLabel() {
|
|
3216
|
-
if (this.embeddingScore === null)
|
|
3217
|
-
return '';
|
|
3218
|
-
return `Similarity: ${this.formatNumber(this.embeddingScore, 2, 4)}`;
|
|
3219
|
-
}
|
|
3220
|
-
apiSimilarityLabel() {
|
|
3221
|
-
if (this.apiSimilarity === null)
|
|
3222
|
-
return '';
|
|
3223
|
-
return `Similarity: ${this.formatNumber(this.apiSimilarity, 2, 4)}`;
|
|
3224
|
-
}
|
|
3225
|
-
pairLabel(p) {
|
|
3226
|
-
return `${p.wordA} ⇄ ${p.wordB} — ${this.formatNumber(p.score * 100, 1, 1)}%`;
|
|
3227
|
-
}
|
|
3228
|
-
faceResultJson() {
|
|
3229
|
-
try {
|
|
3230
|
-
return JSON.stringify(this.faceResult, null, 2);
|
|
3231
|
-
}
|
|
3232
|
-
catch {
|
|
3233
|
-
return String(this.faceResult);
|
|
3234
|
-
}
|
|
3235
|
-
}
|
|
3236
|
-
formatNumber(value, min = 2, max = 2) {
|
|
3237
|
-
try {
|
|
3238
|
-
return new Intl.NumberFormat('en-US', { minimumFractionDigits: min, maximumFractionDigits: max }).format(value);
|
|
3239
|
-
}
|
|
3240
|
-
catch {
|
|
3241
|
-
return value.toFixed(max);
|
|
3242
|
-
}
|
|
3243
|
-
}
|
|
3244
|
-
// --- Navigation ---
|
|
3245
|
-
goHome(event) {
|
|
3246
|
-
event.preventDefault();
|
|
3247
|
-
this.router.navigateByUrl('/');
|
|
3248
|
-
}
|
|
3249
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: DocumentationComponent, deps: [{ token: OpenAIEmbeddingService }, { token: ProfileService }, { token: i1$1.Router }], target: i0.ɵɵFactoryTarget.Component });
|
|
3250
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.16", type: DocumentationComponent, isStandalone: true, selector: "lib-documentation", inputs: { defaultApiNinjasKey: "defaultApiNinjasKey", defaultOpenaiApiKey: "defaultOpenaiApiKey" }, ngImport: i0, template: "<div class=\"docs-overlay\">\r\n <div class=\"docs-container\">\r\n <div class=\"docs-header\">\r\n <h1>Profile Comparison Library \u2014 Developer Docs</h1>\r\n <a class=\"close-btn\" href=\"/\" (click)=\"goHome($event)\" aria-label=\"Close\">\u00D7</a>\r\n </div>\r\n\r\n <p class=\"lead\">\r\n This page documents how to use the library services and lets you test them live inside the showcase.\r\n Existing showcase UI remains unchanged \u2014 this is a standalone docs screen reachable from the \"Docs\" link.\r\n </p>\r\n\r\n <section class=\"section\">\r\n <h2>Quickstart</h2>\r\n <ol>\r\n <li><strong>Build the library</strong>: run <code>ng build profile-comparison-lib</code>.</li>\r\n <li><strong>Import</strong>: <code>import { ProfileComparisonLibModule, EmbeddingService, ProfileService } from 'profile-comparison-lib';</code></li>\r\n <li><strong>Provide HttpClient</strong>: ensure <code>HttpClientModule</code> is imported in your app module.</li>\r\n <li><strong>API Key</strong>: for text similarity/face detect APIs, pass an API Ninjas key.</li>\r\n <li><strong>Run Showcase</strong>: <code>ng serve showcase</code> (or <code>npm run serve:showcase</code>), then open <code>/docs</code>.</li>\r\n </ol>\r\n <p>\r\n Full README is in <code>README.md</code> at the repository root.\r\n </p>\r\n </section>\r\n\r\n <section class=\"section\">\r\n <h2>Set API Key</h2>\r\n <div class=\"row\">\r\n <input type=\"text\" [(ngModel)]=\"apiKeyInput\" placeholder=\"API Ninjas Key\" />\r\n <button (click)=\"saveKey()\">Save</button>\r\n </div>\r\n <p class=\"hint\">Your key is stored in localStorage. Provide a valid key; rate limits apply.</p>\r\n </section>\r\n\r\n <section class=\"section\">\r\n <h2>Embedding Similarity (TensorFlow.js)</h2>\r\n <div class=\"row\">\r\n <input type=\"text\" [(ngModel)]=\"textA\" placeholder=\"Text A\" />\r\n <input type=\"text\" [(ngModel)]=\"textB\" placeholder=\"Text B\" />\r\n <button (click)=\"runEmbeddingSimilarity()\" [disabled]=\"embeddingLoading\">Run</button>\r\n </div>\r\n <div class=\"result\" *ngIf=\"embeddingLoading\">Computing\u2026</div>\r\n <div class=\"result\" *ngIf=\"embeddingError\" [textContent]=\"'Error: ' + embeddingError\"></div>\r\n <div class=\"result\" *ngIf=\"embeddingScore !== null\" [textContent]=\"similarityLabel()\"></div>\r\n <details>\r\n <summary>Usage</summary>\r\n <pre ngNonBindable><code>constructor(private embedding: EmbeddingService) {}\r\nthis.embedding.calculateSimilarity('football','soccer')\r\n .subscribe(score => console.log(score));</code></pre>\r\n </details>\r\n </section>\r\n\r\n <section class=\"section\">\r\n <h2>Best Matching Pairs</h2>\r\n <div class=\"cols\">\r\n <textarea [(ngModel)]=\"listAInput\" rows=\"6\" spellcheck=\"false\"></textarea>\r\n <textarea [(ngModel)]=\"listBInput\" rows=\"6\" spellcheck=\"false\"></textarea>\r\n </div>\r\n <button class=\"spaced-btn\" (click)=\"runBestPairs()\" [disabled]=\"pairsLoading\">Find Pairs</button>\r\n <div class=\"result\" *ngIf=\"pairsLoading\">Processing\u2026</div>\r\n <div class=\"result\" *ngIf=\"pairsError\" [textContent]=\"'Error: ' + pairsError\"></div>\r\n <ul class=\"pairs\" *ngIf=\"!pairsLoading && matchingPairs.length\">\r\n <li *ngFor=\"let p of matchingPairs\" [textContent]=\"pairLabel(p)\"></li>\r\n </ul>\r\n <details>\r\n <summary>Usage</summary>\r\n <pre ngNonBindable><code>this.embedding.findBestMatchingPairs(listA, listB, 0.15)\r\n .subscribe(pairs => console.log(pairs));</code></pre>\r\n </details>\r\n </section>\r\n\r\n <section class=\"section\">\r\n <h2>API Ninjas: Text Similarity</h2>\r\n <div class=\"row\">\r\n <input type=\"text\" [(ngModel)]=\"compareA\" placeholder=\"Text A\" />\r\n <input type=\"text\" [(ngModel)]=\"compareB\" placeholder=\"Text B\" />\r\n <button (click)=\"runApiSimilarity()\" [disabled]=\"apiLoading\">Compare</button>\r\n </div>\r\n <div class=\"result\" *ngIf=\"apiLoading\">Calling API\u2026</div>\r\n <div class=\"result\" *ngIf=\"apiError\" [textContent]=\"'Error: ' + apiError\"></div>\r\n <div class=\"result\" *ngIf=\"apiSimilarity !== null\" [textContent]=\"apiSimilarityLabel()\"></div>\r\n <details>\r\n <summary>Usage</summary>\r\n <pre><code>this.profileService.compareInterests(a, b, apiKey)\r\n .subscribe((res: { similarity: number }) => console.log(res.similarity));</code></pre>\r\n </details>\r\n </section>\r\n\r\n <section class=\"section\">\r\n <h2>Face Detection</h2>\r\n <div class=\"row\">\r\n <input type=\"file\" (change)=\"onFaceFileChange($event)\" accept=\"image/*\" />\r\n <button (click)=\"runFaceDetect()\" [disabled]=\"faceLoading\">Detect Face</button>\r\n </div>\r\n <div class=\"result\" *ngIf=\"faceLoading\">Calling API\u2026</div>\r\n <div class=\"result\" *ngIf=\"faceError\" [textContent]=\"'Error: ' + faceError\"></div>\r\n <pre class=\"result\" *ngIf=\"!faceLoading && faceResult\" [textContent]=\"faceResultJson()\"></pre>\r\n <details>\r\n <summary>Usage</summary>\r\n <pre ngNonBindable><code>this.profileService.detectFace(file, apiKey)\r\n .subscribe(res => console.log(res));</code></pre>\r\n </details>\r\n </section>\r\n\r\n <section class=\"section\">\r\n <h2>Module Import</h2>\r\n <pre ngNonBindable><code>import { HttpClientModule } from '@angular/common/http';\r\nimport { ProfileComparisonLibModule } from 'profile-comparison-lib';\r\n\r\n@NgModule({\r\n imports: [HttpClientModule, ProfileComparisonLibModule]\r\n})\r\nexport class AppModule {}</code></pre>\r\n </section>\r\n\r\n <section class=\"section\">\r\n <h2>Services Overview</h2>\r\n <ul>\r\n <li><strong>EmbeddingService</strong>: <code>calculateSimilarity(textA, textB)</code>, <code>findBestMatchingPairs(listA, listB, threshold?)</code></li>\r\n <li><strong>ProfileService</strong>: <code>compareInterests(a, b, apiKey)</code>, <code>detectFace(file, apiKey)</code></li>\r\n </ul>\r\n </section>\r\n\r\n <section class=\"section\">\r\n <h2>Exports</h2>\r\n <ul>\r\n <li><code>ProfileComparisonLibModule</code></li>\r\n <li><code>EmbeddingService</code></li>\r\n <li><code>ProfileService</code></li>\r\n <li><code>WordPair</code> (interface)</li>\r\n <li><code>WordAlignment</code> (interface)</li>\r\n </ul>\r\n </section>\r\n\r\n <section class=\"section\">\r\n <h2>Troubleshooting</h2>\r\n <ul>\r\n <li><strong>API key/Quota</strong>: If calls fail, verify the API key and check rate limits.</li>\r\n <li><strong>Network</strong>: Ensure CORS and network connectivity are OK.</li>\r\n <li><strong>Build</strong>: Run <code>ng build profile-comparison-lib</code> after library changes.</li>\r\n </ul>\r\n </section>\r\n </div>\r\n</div>\r\n", styles: [".docs-overlay{position:fixed;inset:0;background:#0a0e17bf;z-index:2000;overflow:auto}.docs-container{box-sizing:border-box;max-width:900px;margin:40px auto;background:#0f172a;color:#e2e8f0;border-radius:12px;padding:24px 28px;box-shadow:0 12px 40px #00000059}.docs-header{display:flex;align-items:center;justify-content:space-between;gap:16px;margin-bottom:8px}.docs-header h1{margin:0;font-size:22px}.close-btn{color:#94a3b8;text-decoration:none;font-size:24px;line-height:1;border:1px solid #334155;border-radius:8px;padding:2px 10px;background:#0b1220}.lead{margin:0 0 12px;color:#cbd5e1}.section{border:1px solid #334155;border-radius:10px;padding:16px;margin:14px 0;background:#0b1220}.section h2{margin:0 0 12px;font-size:18px}.row{display:flex;gap:10px;align-items:center;flex-wrap:wrap}.row input[type=text],.row input[type=file],textarea{flex:1 1 280px;background:#0a1020;color:#e2e8f0;border:1px solid #334155;border-radius:8px;padding:8px 10px}textarea{min-height:100px}button{background:#2563eb;color:#fff;border:none;border-radius:8px;padding:8px 12px;cursor:pointer}.result{margin-top:8px;color:#cbd5e1}.cols{display:grid;grid-template-columns:1fr 1fr;gap:10px}.pairs{margin:10px 0 0;padding-left:18px}.hint{color:#94a3b8;margin-top:4px;font-size:12px}.spaced-btn{margin-top:10px;margin-bottom:10px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i3.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i3.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }] });
|
|
3251
|
-
}
|
|
3252
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: DocumentationComponent, decorators: [{
|
|
3253
|
-
type: Component,
|
|
3254
|
-
args: [{ selector: 'lib-documentation', standalone: true, imports: [CommonModule, FormsModule], template: "<div class=\"docs-overlay\">\r\n <div class=\"docs-container\">\r\n <div class=\"docs-header\">\r\n <h1>Profile Comparison Library \u2014 Developer Docs</h1>\r\n <a class=\"close-btn\" href=\"/\" (click)=\"goHome($event)\" aria-label=\"Close\">\u00D7</a>\r\n </div>\r\n\r\n <p class=\"lead\">\r\n This page documents how to use the library services and lets you test them live inside the showcase.\r\n Existing showcase UI remains unchanged \u2014 this is a standalone docs screen reachable from the \"Docs\" link.\r\n </p>\r\n\r\n <section class=\"section\">\r\n <h2>Quickstart</h2>\r\n <ol>\r\n <li><strong>Build the library</strong>: run <code>ng build profile-comparison-lib</code>.</li>\r\n <li><strong>Import</strong>: <code>import { ProfileComparisonLibModule, EmbeddingService, ProfileService } from 'profile-comparison-lib';</code></li>\r\n <li><strong>Provide HttpClient</strong>: ensure <code>HttpClientModule</code> is imported in your app module.</li>\r\n <li><strong>API Key</strong>: for text similarity/face detect APIs, pass an API Ninjas key.</li>\r\n <li><strong>Run Showcase</strong>: <code>ng serve showcase</code> (or <code>npm run serve:showcase</code>), then open <code>/docs</code>.</li>\r\n </ol>\r\n <p>\r\n Full README is in <code>README.md</code> at the repository root.\r\n </p>\r\n </section>\r\n\r\n <section class=\"section\">\r\n <h2>Set API Key</h2>\r\n <div class=\"row\">\r\n <input type=\"text\" [(ngModel)]=\"apiKeyInput\" placeholder=\"API Ninjas Key\" />\r\n <button (click)=\"saveKey()\">Save</button>\r\n </div>\r\n <p class=\"hint\">Your key is stored in localStorage. Provide a valid key; rate limits apply.</p>\r\n </section>\r\n\r\n <section class=\"section\">\r\n <h2>Embedding Similarity (TensorFlow.js)</h2>\r\n <div class=\"row\">\r\n <input type=\"text\" [(ngModel)]=\"textA\" placeholder=\"Text A\" />\r\n <input type=\"text\" [(ngModel)]=\"textB\" placeholder=\"Text B\" />\r\n <button (click)=\"runEmbeddingSimilarity()\" [disabled]=\"embeddingLoading\">Run</button>\r\n </div>\r\n <div class=\"result\" *ngIf=\"embeddingLoading\">Computing\u2026</div>\r\n <div class=\"result\" *ngIf=\"embeddingError\" [textContent]=\"'Error: ' + embeddingError\"></div>\r\n <div class=\"result\" *ngIf=\"embeddingScore !== null\" [textContent]=\"similarityLabel()\"></div>\r\n <details>\r\n <summary>Usage</summary>\r\n <pre ngNonBindable><code>constructor(private embedding: EmbeddingService) {}\r\nthis.embedding.calculateSimilarity('football','soccer')\r\n .subscribe(score => console.log(score));</code></pre>\r\n </details>\r\n </section>\r\n\r\n <section class=\"section\">\r\n <h2>Best Matching Pairs</h2>\r\n <div class=\"cols\">\r\n <textarea [(ngModel)]=\"listAInput\" rows=\"6\" spellcheck=\"false\"></textarea>\r\n <textarea [(ngModel)]=\"listBInput\" rows=\"6\" spellcheck=\"false\"></textarea>\r\n </div>\r\n <button class=\"spaced-btn\" (click)=\"runBestPairs()\" [disabled]=\"pairsLoading\">Find Pairs</button>\r\n <div class=\"result\" *ngIf=\"pairsLoading\">Processing\u2026</div>\r\n <div class=\"result\" *ngIf=\"pairsError\" [textContent]=\"'Error: ' + pairsError\"></div>\r\n <ul class=\"pairs\" *ngIf=\"!pairsLoading && matchingPairs.length\">\r\n <li *ngFor=\"let p of matchingPairs\" [textContent]=\"pairLabel(p)\"></li>\r\n </ul>\r\n <details>\r\n <summary>Usage</summary>\r\n <pre ngNonBindable><code>this.embedding.findBestMatchingPairs(listA, listB, 0.15)\r\n .subscribe(pairs => console.log(pairs));</code></pre>\r\n </details>\r\n </section>\r\n\r\n <section class=\"section\">\r\n <h2>API Ninjas: Text Similarity</h2>\r\n <div class=\"row\">\r\n <input type=\"text\" [(ngModel)]=\"compareA\" placeholder=\"Text A\" />\r\n <input type=\"text\" [(ngModel)]=\"compareB\" placeholder=\"Text B\" />\r\n <button (click)=\"runApiSimilarity()\" [disabled]=\"apiLoading\">Compare</button>\r\n </div>\r\n <div class=\"result\" *ngIf=\"apiLoading\">Calling API\u2026</div>\r\n <div class=\"result\" *ngIf=\"apiError\" [textContent]=\"'Error: ' + apiError\"></div>\r\n <div class=\"result\" *ngIf=\"apiSimilarity !== null\" [textContent]=\"apiSimilarityLabel()\"></div>\r\n <details>\r\n <summary>Usage</summary>\r\n <pre><code>this.profileService.compareInterests(a, b, apiKey)\r\n .subscribe((res: { similarity: number }) => console.log(res.similarity));</code></pre>\r\n </details>\r\n </section>\r\n\r\n <section class=\"section\">\r\n <h2>Face Detection</h2>\r\n <div class=\"row\">\r\n <input type=\"file\" (change)=\"onFaceFileChange($event)\" accept=\"image/*\" />\r\n <button (click)=\"runFaceDetect()\" [disabled]=\"faceLoading\">Detect Face</button>\r\n </div>\r\n <div class=\"result\" *ngIf=\"faceLoading\">Calling API\u2026</div>\r\n <div class=\"result\" *ngIf=\"faceError\" [textContent]=\"'Error: ' + faceError\"></div>\r\n <pre class=\"result\" *ngIf=\"!faceLoading && faceResult\" [textContent]=\"faceResultJson()\"></pre>\r\n <details>\r\n <summary>Usage</summary>\r\n <pre ngNonBindable><code>this.profileService.detectFace(file, apiKey)\r\n .subscribe(res => console.log(res));</code></pre>\r\n </details>\r\n </section>\r\n\r\n <section class=\"section\">\r\n <h2>Module Import</h2>\r\n <pre ngNonBindable><code>import { HttpClientModule } from '@angular/common/http';\r\nimport { ProfileComparisonLibModule } from 'profile-comparison-lib';\r\n\r\n@NgModule({\r\n imports: [HttpClientModule, ProfileComparisonLibModule]\r\n})\r\nexport class AppModule {}</code></pre>\r\n </section>\r\n\r\n <section class=\"section\">\r\n <h2>Services Overview</h2>\r\n <ul>\r\n <li><strong>EmbeddingService</strong>: <code>calculateSimilarity(textA, textB)</code>, <code>findBestMatchingPairs(listA, listB, threshold?)</code></li>\r\n <li><strong>ProfileService</strong>: <code>compareInterests(a, b, apiKey)</code>, <code>detectFace(file, apiKey)</code></li>\r\n </ul>\r\n </section>\r\n\r\n <section class=\"section\">\r\n <h2>Exports</h2>\r\n <ul>\r\n <li><code>ProfileComparisonLibModule</code></li>\r\n <li><code>EmbeddingService</code></li>\r\n <li><code>ProfileService</code></li>\r\n <li><code>WordPair</code> (interface)</li>\r\n <li><code>WordAlignment</code> (interface)</li>\r\n </ul>\r\n </section>\r\n\r\n <section class=\"section\">\r\n <h2>Troubleshooting</h2>\r\n <ul>\r\n <li><strong>API key/Quota</strong>: If calls fail, verify the API key and check rate limits.</li>\r\n <li><strong>Network</strong>: Ensure CORS and network connectivity are OK.</li>\r\n <li><strong>Build</strong>: Run <code>ng build profile-comparison-lib</code> after library changes.</li>\r\n </ul>\r\n </section>\r\n </div>\r\n</div>\r\n", styles: [".docs-overlay{position:fixed;inset:0;background:#0a0e17bf;z-index:2000;overflow:auto}.docs-container{box-sizing:border-box;max-width:900px;margin:40px auto;background:#0f172a;color:#e2e8f0;border-radius:12px;padding:24px 28px;box-shadow:0 12px 40px #00000059}.docs-header{display:flex;align-items:center;justify-content:space-between;gap:16px;margin-bottom:8px}.docs-header h1{margin:0;font-size:22px}.close-btn{color:#94a3b8;text-decoration:none;font-size:24px;line-height:1;border:1px solid #334155;border-radius:8px;padding:2px 10px;background:#0b1220}.lead{margin:0 0 12px;color:#cbd5e1}.section{border:1px solid #334155;border-radius:10px;padding:16px;margin:14px 0;background:#0b1220}.section h2{margin:0 0 12px;font-size:18px}.row{display:flex;gap:10px;align-items:center;flex-wrap:wrap}.row input[type=text],.row input[type=file],textarea{flex:1 1 280px;background:#0a1020;color:#e2e8f0;border:1px solid #334155;border-radius:8px;padding:8px 10px}textarea{min-height:100px}button{background:#2563eb;color:#fff;border:none;border-radius:8px;padding:8px 12px;cursor:pointer}.result{margin-top:8px;color:#cbd5e1}.cols{display:grid;grid-template-columns:1fr 1fr;gap:10px}.pairs{margin:10px 0 0;padding-left:18px}.hint{color:#94a3b8;margin-top:4px;font-size:12px}.spaced-btn{margin-top:10px;margin-bottom:10px}\n"] }]
|
|
3255
|
-
}], ctorParameters: () => [{ type: OpenAIEmbeddingService }, { type: ProfileService }, { type: i1$1.Router }], propDecorators: { defaultApiNinjasKey: [{
|
|
3256
|
-
type: Input
|
|
3257
|
-
}], defaultOpenaiApiKey: [{
|
|
3258
|
-
type: Input
|
|
3047
|
+
}], profileFlex: [{
|
|
3048
|
+
type: ViewChild,
|
|
3049
|
+
args: ['profileFlex']
|
|
3050
|
+
}], profileImgLeft: [{
|
|
3051
|
+
type: ViewChild,
|
|
3052
|
+
args: ['profileImgLeft']
|
|
3053
|
+
}], profileImgRight: [{
|
|
3054
|
+
type: ViewChild,
|
|
3055
|
+
args: ['profileImgRight']
|
|
3056
|
+
}], shapeContainer: [{
|
|
3057
|
+
type: ViewChild,
|
|
3058
|
+
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
|
+
}], shapeTextCenter: [{
|
|
3075
|
+
type: ViewChild,
|
|
3076
|
+
args: ['shapeTextCenter']
|
|
3259
3077
|
}] } });
|
|
3260
3078
|
|
|
3261
3079
|
class ProfileComparisonLibModule {
|
|
@@ -3263,23 +3081,15 @@ class ProfileComparisonLibModule {
|
|
|
3263
3081
|
static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "20.3.16", ngImport: i0, type: ProfileComparisonLibModule, declarations: [ProfileComparisonLibComponent], imports: [CommonModule,
|
|
3264
3082
|
HttpClientModule,
|
|
3265
3083
|
FormsModule,
|
|
3266
|
-
ReactiveFormsModule,
|
|
3267
|
-
CustomInputComponent,
|
|
3268
|
-
DocumentationComponent,
|
|
3269
|
-
RouterModule], exports: [ProfileComparisonLibComponent,
|
|
3270
|
-
CustomInputComponent,
|
|
3271
|
-
DocumentationComponent] });
|
|
3084
|
+
ReactiveFormsModule], exports: [ProfileComparisonLibComponent] });
|
|
3272
3085
|
static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: ProfileComparisonLibModule, providers: [
|
|
3273
|
-
|
|
3086
|
+
ProfileComparisonBackendService,
|
|
3274
3087
|
EmbeddingService,
|
|
3275
3088
|
OpenAIEmbeddingService
|
|
3276
3089
|
], imports: [CommonModule,
|
|
3277
3090
|
HttpClientModule,
|
|
3278
3091
|
FormsModule,
|
|
3279
|
-
ReactiveFormsModule
|
|
3280
|
-
CustomInputComponent,
|
|
3281
|
-
DocumentationComponent,
|
|
3282
|
-
RouterModule] });
|
|
3092
|
+
ReactiveFormsModule] });
|
|
3283
3093
|
}
|
|
3284
3094
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: ProfileComparisonLibModule, decorators: [{
|
|
3285
3095
|
type: NgModule,
|
|
@@ -3291,18 +3101,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
|
|
|
3291
3101
|
CommonModule,
|
|
3292
3102
|
HttpClientModule,
|
|
3293
3103
|
FormsModule,
|
|
3294
|
-
ReactiveFormsModule
|
|
3295
|
-
CustomInputComponent,
|
|
3296
|
-
DocumentationComponent,
|
|
3297
|
-
RouterModule
|
|
3104
|
+
ReactiveFormsModule
|
|
3298
3105
|
],
|
|
3299
3106
|
exports: [
|
|
3300
|
-
ProfileComparisonLibComponent
|
|
3301
|
-
CustomInputComponent,
|
|
3302
|
-
DocumentationComponent
|
|
3107
|
+
ProfileComparisonLibComponent
|
|
3303
3108
|
],
|
|
3304
3109
|
providers: [
|
|
3305
|
-
|
|
3110
|
+
ProfileComparisonBackendService,
|
|
3306
3111
|
EmbeddingService,
|
|
3307
3112
|
OpenAIEmbeddingService
|
|
3308
3113
|
]
|
|
@@ -3317,5 +3122,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
|
|
|
3317
3122
|
* Generated bundle index. Do not edit.
|
|
3318
3123
|
*/
|
|
3319
3124
|
|
|
3320
|
-
export {
|
|
3125
|
+
export { CachePersistenceService, EmbeddingService, FileConversionService, ImageCompressionService, OpenAIEmbeddingService, PROFILE_COMPARISON_API_BASE_URL, PROFILE_COMPARISON_VERBOSE_LOGGING, ProfileComparisonBackendService, ProfileComparisonLibComponent, ProfileComparisonLibModule, ProfileComparisonLibService, ProfileService };
|
|
3321
3126
|
//# sourceMappingURL=naniteninja-profile-comparison-lib.mjs.map
|