@naniteninja/profile-comparison-lib 1.0.26 → 1.0.28
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.
|
@@ -17,9 +17,23 @@ var BackendMode;
|
|
|
17
17
|
/** Use a local mock server at localhost:44101. */
|
|
18
18
|
BackendMode[BackendMode["Mock"] = 1] = "Mock";
|
|
19
19
|
})(BackendMode || (BackendMode = {}));
|
|
20
|
+
const PROFILE_COMPARISON_PROD_API_BASE_URL = 'https://api.test.thecyrano.app/api/profile-comparison/api';
|
|
21
|
+
const PROFILE_COMPARISON_LOCAL_API_BASE_URL = 'http://localhost:44101/api';
|
|
22
|
+
function isLocalBrowserRuntime() {
|
|
23
|
+
if (typeof window === 'undefined') {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
const hostname = window.location?.hostname?.toLowerCase() ?? '';
|
|
27
|
+
return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '0.0.0.0';
|
|
28
|
+
}
|
|
29
|
+
function resolveDefaultRealBackendUrl() {
|
|
30
|
+
return isLocalBrowserRuntime()
|
|
31
|
+
? PROFILE_COMPARISON_LOCAL_API_BASE_URL
|
|
32
|
+
: PROFILE_COMPARISON_PROD_API_BASE_URL;
|
|
33
|
+
}
|
|
20
34
|
const BACKEND_MODE_URLS = {
|
|
21
|
-
[BackendMode.Real]:
|
|
22
|
-
[BackendMode.Mock]:
|
|
35
|
+
[BackendMode.Real]: resolveDefaultRealBackendUrl(),
|
|
36
|
+
[BackendMode.Mock]: PROFILE_COMPARISON_LOCAL_API_BASE_URL,
|
|
23
37
|
};
|
|
24
38
|
/**
|
|
25
39
|
* Legacy injection token for the backend base URL.
|
|
@@ -203,10 +217,10 @@ class CachePersistenceService {
|
|
|
203
217
|
static STORE_OPENAI_EMBEDDINGS = STORE_OPENAI_EMBEDDINGS;
|
|
204
218
|
static STORE_PROFILE_FACE = STORE_PROFILE_FACE;
|
|
205
219
|
static STORE_PROFILE_COMPARE = STORE_PROFILE_COMPARE;
|
|
206
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.
|
|
207
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.
|
|
220
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.26", ngImport: i0, type: CachePersistenceService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
221
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.26", ngImport: i0, type: CachePersistenceService, providedIn: 'root' });
|
|
208
222
|
}
|
|
209
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.
|
|
223
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.26", ngImport: i0, type: CachePersistenceService, decorators: [{
|
|
210
224
|
type: Injectable,
|
|
211
225
|
args: [{ providedIn: 'root' }]
|
|
212
226
|
}] });
|
|
@@ -314,10 +328,10 @@ class FileConversionService {
|
|
|
314
328
|
const mime = this.getMimeFromFilename(imageStr);
|
|
315
329
|
return this.urlToFile(imageStr, fileBaseName, mime);
|
|
316
330
|
}
|
|
317
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.
|
|
318
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.
|
|
331
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.26", ngImport: i0, type: FileConversionService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
332
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.26", ngImport: i0, type: FileConversionService, providedIn: 'root' });
|
|
319
333
|
}
|
|
320
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.
|
|
334
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.26", ngImport: i0, type: FileConversionService, decorators: [{
|
|
321
335
|
type: Injectable,
|
|
322
336
|
args: [{
|
|
323
337
|
providedIn: 'root'
|
|
@@ -433,10 +447,10 @@ class ImageCompressionService {
|
|
|
433
447
|
return `${base}.webp`;
|
|
434
448
|
return `${base}.jpg`;
|
|
435
449
|
}
|
|
436
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.
|
|
437
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.
|
|
450
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.26", ngImport: i0, type: ImageCompressionService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
451
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.26", ngImport: i0, type: ImageCompressionService, providedIn: 'root' });
|
|
438
452
|
}
|
|
439
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.
|
|
453
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.26", ngImport: i0, type: ImageCompressionService, decorators: [{
|
|
440
454
|
type: Injectable,
|
|
441
455
|
args: [{
|
|
442
456
|
providedIn: 'root'
|
|
@@ -580,96 +594,96 @@ class OpenAIEmbeddingService {
|
|
|
580
594
|
[OpenAIEmbeddingService.HEADER_CONTENT_TYPE]: OpenAIEmbeddingService.CONTENT_TYPE_JSON,
|
|
581
595
|
[OpenAIEmbeddingService.HEADER_AUTHORIZATION]: `${OpenAIEmbeddingService.BEARER_PREFIX}${apiKey}`
|
|
582
596
|
});
|
|
583
|
-
const prompt = `
|
|
584
|
-
You are an expert semantic alignment engine.
|
|
585
|
-
Task: Align the items from List A to List B based on semantic similarity.
|
|
586
|
-
Context: This is a heuristic, human-semantic alignment (use embeddings later if you want it algorithmically strict).
|
|
587
|
-
|
|
588
|
-
List A: ${JSON.stringify(validA)}
|
|
589
|
-
List B: ${JSON.stringify(validB)}
|
|
590
|
-
|
|
591
|
-
SEMANTIC STRATEGY (3-PASS PROCESS):
|
|
592
|
-
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.
|
|
593
|
-
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.
|
|
594
|
-
3. **Pass 3: Contextual & Loose Matches**: Look for looser associations (e.g., "Reading" vs "Library").
|
|
595
|
-
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.
|
|
596
|
-
|
|
597
|
-
Rules:
|
|
598
|
-
1. Align items into clusters with variable dash-gaps to show semantic distance.
|
|
599
|
-
2. Use "-" for a small hop (related but distinct).
|
|
600
|
-
3. Use "----" for a hard theme switch (different category).
|
|
601
|
-
4. You MUST reorder items to create the best semantic clusters.
|
|
602
|
-
5. Ensure the output arrays "listA" and "listB" have the SAME length.
|
|
603
|
-
6. If an item has no match, align it with "-" or "----" as appropriate, or just a gap.
|
|
604
|
-
7. Output MUST be a JSON object with "listA" and "listB".
|
|
605
|
-
8. **COMPLETENESS**: You MUST include EVERY item from the input List A and List B in the output. Do not omit any items.
|
|
606
|
-
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.
|
|
607
|
-
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.
|
|
608
|
-
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.
|
|
609
|
-
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.
|
|
610
|
-
13. **NO TRAILING SPACER**: The last item in both lists MUST be a word. Do NOT put a spacer at the very end.
|
|
611
|
-
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.
|
|
612
|
-
15. **HIGH SIMILARITY CONSOLIDATION (>= 0.8)**: This is CRITICAL for center display:
|
|
613
|
-
- 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.
|
|
614
|
-
- **Key principle**: If the items are synonyms, very similar activities, or one is a specific type of the other, consolidate them!
|
|
615
|
-
- **STRICT CONSTRAINT**: You can ONLY consolidate if BOTH items actually exist in the input lists and are semantically related.
|
|
616
|
-
- **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.
|
|
617
|
-
- **STRICT SYMMETRY**: If you use the "×" symbol to consolidate, you MUST output the EXACT SAME string in BOTH "listA" and "listB" at that index.
|
|
618
|
-
- This allows the system to automatically move these items to the center display!
|
|
619
|
-
- **NEVER** use "×" on one side and a spacer on the other. That is a critical failure.
|
|
620
|
-
- **NEVER** include similarity scores or any other numbers in the output strings unless they are part of the original item text.
|
|
621
|
-
- **ONLY** use "×" as the separator for consolidated items. Do not use "/", "&", or other symbols for this purpose.
|
|
622
|
-
- Example: "Jogging" (List A) + "Running" (List B) -> "Jogging × Running" in BOTH listA and listB.
|
|
623
|
-
- **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.
|
|
624
|
-
- **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.
|
|
625
|
-
- **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.
|
|
626
|
-
- **DETERMINISM**: Process the list from top to bottom. Do not change your output between runs.
|
|
627
|
-
- This allows the system to automatically move these items to the center display!
|
|
628
|
-
16. Do not insert any new, inferred, or related words. Each pair must be created strictly from the existing list items only.
|
|
629
|
-
|
|
630
|
-
NEGATIVE CONSTRAINTS:
|
|
631
|
-
- NEVER put an item from List A into List B.
|
|
632
|
-
- NEVER put an item from List B into List A.
|
|
633
|
-
- NEVER invent new items. Only use items from the input lists.
|
|
634
|
-
- **NEVER DUPLICATE ITEMS**. Each item from the input list must appear EXACTLY ONCE in the output.
|
|
635
|
-
|
|
636
|
-
VERIFICATION STEP:
|
|
637
|
-
Before outputting, verify every single item:
|
|
638
|
-
- "Is this item in Input List A?" -> If yes, put it in Output List A.
|
|
639
|
-
- "Is this item in Input List B?" -> If yes, put it in Output List B.
|
|
640
|
-
- "Is this item in neither?" -> It is a hallucination. DELETE IT.
|
|
641
|
-
|
|
642
|
-
CRITICAL: OUTPUT MUST CONTAIN EXACTLY THE SAME ITEMS AS INPUT. NO DELETIONS ALLOWED.
|
|
643
|
-
|
|
644
|
-
FINAL SANITY CHECK:
|
|
645
|
-
- **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.
|
|
646
|
-
- **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.
|
|
647
|
-
- **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.
|
|
648
|
-
- **COUNT CHECK**: Do the same for List B.
|
|
649
|
-
- Look at your generated "listA". Are there any items that belong to Input List B? REMOVE THEM immediately.
|
|
650
|
-
- Look at your generated "listB". Are there any items that belong to Input List A? REMOVE THEM immediately.
|
|
651
|
-
- **Did you miss any items from Input List A? ADD THEM NOW.**
|
|
652
|
-
- **Did you miss any items from Input List B? ADD THEM NOW.**
|
|
653
|
-
- If you are unsure where an item belongs, check the Input Lists provided above. Do NOT guess.
|
|
654
|
-
|
|
655
|
-
Example of Desired Output Logic:
|
|
656
|
-
- "Volunteering at animal shelter" vs "Dog training" -> Related? Yes. Similarity >= 0.8? No (different activities)
|
|
657
|
-
- NEXT PAIR: "Trail biking" vs "Rock climbing" -> Is this related to Animals? No. -> INSERT "----"
|
|
658
|
-
- "Trail biking" vs "Rock climbing" -> Related? Yes (Outdoors).
|
|
659
|
-
- NEXT PAIR: "Jogging" vs "Running" -> Similarity >= 0.8? YES! -> Output "Jogging × Running" in BOTH lists
|
|
660
|
-
- "Jogging" vs "Running" -> Related? Yes (Exercise).
|
|
661
|
-
- NEXT PAIR: "Beach volleyball" vs "Salsa dancing" -> Is this related to Exercise? Yes. -> INSERT "-" (Small hop)
|
|
662
|
-
- "Beach volleyball" vs "Salsa dancing" -> Related? Yes (Hobbies). Similarity >= 0.8? No (different hobbies)
|
|
663
|
-
- NEXT PAIR: "Fermenting kimchi" vs "Korean cooking" -> Is this related to Hobbies? No. -> INSERT "----"
|
|
664
|
-
|
|
665
|
-
JSON Output Structure:
|
|
666
|
-
{
|
|
667
|
-
"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 ],
|
|
668
|
-
"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, "----" , - ]
|
|
669
|
-
}
|
|
670
|
-
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.
|
|
671
|
-
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.
|
|
672
|
-
VARIATION & SIMILARITY CONTROL: Maintain strict consistency. If the same semantic relationship appears multiple times, align it identically every time.
|
|
597
|
+
const prompt = `
|
|
598
|
+
You are an expert semantic alignment engine.
|
|
599
|
+
Task: Align the items from List A to List B based on semantic similarity.
|
|
600
|
+
Context: This is a heuristic, human-semantic alignment (use embeddings later if you want it algorithmically strict).
|
|
601
|
+
|
|
602
|
+
List A: ${JSON.stringify(validA)}
|
|
603
|
+
List B: ${JSON.stringify(validB)}
|
|
604
|
+
|
|
605
|
+
SEMANTIC STRATEGY (3-PASS PROCESS):
|
|
606
|
+
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.
|
|
607
|
+
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.
|
|
608
|
+
3. **Pass 3: Contextual & Loose Matches**: Look for looser associations (e.g., "Reading" vs "Library").
|
|
609
|
+
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.
|
|
610
|
+
|
|
611
|
+
Rules:
|
|
612
|
+
1. Align items into clusters with variable dash-gaps to show semantic distance.
|
|
613
|
+
2. Use "-" for a small hop (related but distinct).
|
|
614
|
+
3. Use "----" for a hard theme switch (different category).
|
|
615
|
+
4. You MUST reorder items to create the best semantic clusters.
|
|
616
|
+
5. Ensure the output arrays "listA" and "listB" have the SAME length.
|
|
617
|
+
6. If an item has no match, align it with "-" or "----" as appropriate, or just a gap.
|
|
618
|
+
7. Output MUST be a JSON object with "listA" and "listB".
|
|
619
|
+
8. **COMPLETENESS**: You MUST include EVERY item from the input List A and List B in the output. Do not omit any items.
|
|
620
|
+
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.
|
|
621
|
+
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.
|
|
622
|
+
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.
|
|
623
|
+
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.
|
|
624
|
+
13. **NO TRAILING SPACER**: The last item in both lists MUST be a word. Do NOT put a spacer at the very end.
|
|
625
|
+
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.
|
|
626
|
+
15. **HIGH SIMILARITY CONSOLIDATION (>= 0.8)**: This is CRITICAL for center display:
|
|
627
|
+
- 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.
|
|
628
|
+
- **Key principle**: If the items are synonyms, very similar activities, or one is a specific type of the other, consolidate them!
|
|
629
|
+
- **STRICT CONSTRAINT**: You can ONLY consolidate if BOTH items actually exist in the input lists and are semantically related.
|
|
630
|
+
- **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.
|
|
631
|
+
- **STRICT SYMMETRY**: If you use the "×" symbol to consolidate, you MUST output the EXACT SAME string in BOTH "listA" and "listB" at that index.
|
|
632
|
+
- This allows the system to automatically move these items to the center display!
|
|
633
|
+
- **NEVER** use "×" on one side and a spacer on the other. That is a critical failure.
|
|
634
|
+
- **NEVER** include similarity scores or any other numbers in the output strings unless they are part of the original item text.
|
|
635
|
+
- **ONLY** use "×" as the separator for consolidated items. Do not use "/", "&", or other symbols for this purpose.
|
|
636
|
+
- Example: "Jogging" (List A) + "Running" (List B) -> "Jogging × Running" in BOTH listA and listB.
|
|
637
|
+
- **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.
|
|
638
|
+
- **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.
|
|
639
|
+
- **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.
|
|
640
|
+
- **DETERMINISM**: Process the list from top to bottom. Do not change your output between runs.
|
|
641
|
+
- This allows the system to automatically move these items to the center display!
|
|
642
|
+
16. Do not insert any new, inferred, or related words. Each pair must be created strictly from the existing list items only.
|
|
643
|
+
|
|
644
|
+
NEGATIVE CONSTRAINTS:
|
|
645
|
+
- NEVER put an item from List A into List B.
|
|
646
|
+
- NEVER put an item from List B into List A.
|
|
647
|
+
- NEVER invent new items. Only use items from the input lists.
|
|
648
|
+
- **NEVER DUPLICATE ITEMS**. Each item from the input list must appear EXACTLY ONCE in the output.
|
|
649
|
+
|
|
650
|
+
VERIFICATION STEP:
|
|
651
|
+
Before outputting, verify every single item:
|
|
652
|
+
- "Is this item in Input List A?" -> If yes, put it in Output List A.
|
|
653
|
+
- "Is this item in Input List B?" -> If yes, put it in Output List B.
|
|
654
|
+
- "Is this item in neither?" -> It is a hallucination. DELETE IT.
|
|
655
|
+
|
|
656
|
+
CRITICAL: OUTPUT MUST CONTAIN EXACTLY THE SAME ITEMS AS INPUT. NO DELETIONS ALLOWED.
|
|
657
|
+
|
|
658
|
+
FINAL SANITY CHECK:
|
|
659
|
+
- **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.
|
|
660
|
+
- **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.
|
|
661
|
+
- **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.
|
|
662
|
+
- **COUNT CHECK**: Do the same for List B.
|
|
663
|
+
- Look at your generated "listA". Are there any items that belong to Input List B? REMOVE THEM immediately.
|
|
664
|
+
- Look at your generated "listB". Are there any items that belong to Input List A? REMOVE THEM immediately.
|
|
665
|
+
- **Did you miss any items from Input List A? ADD THEM NOW.**
|
|
666
|
+
- **Did you miss any items from Input List B? ADD THEM NOW.**
|
|
667
|
+
- If you are unsure where an item belongs, check the Input Lists provided above. Do NOT guess.
|
|
668
|
+
|
|
669
|
+
Example of Desired Output Logic:
|
|
670
|
+
- "Volunteering at animal shelter" vs "Dog training" -> Related? Yes. Similarity >= 0.8? No (different activities)
|
|
671
|
+
- NEXT PAIR: "Trail biking" vs "Rock climbing" -> Is this related to Animals? No. -> INSERT "----"
|
|
672
|
+
- "Trail biking" vs "Rock climbing" -> Related? Yes (Outdoors).
|
|
673
|
+
- NEXT PAIR: "Jogging" vs "Running" -> Similarity >= 0.8? YES! -> Output "Jogging × Running" in BOTH lists
|
|
674
|
+
- "Jogging" vs "Running" -> Related? Yes (Exercise).
|
|
675
|
+
- NEXT PAIR: "Beach volleyball" vs "Salsa dancing" -> Is this related to Exercise? Yes. -> INSERT "-" (Small hop)
|
|
676
|
+
- "Beach volleyball" vs "Salsa dancing" -> Related? Yes (Hobbies). Similarity >= 0.8? No (different hobbies)
|
|
677
|
+
- NEXT PAIR: "Fermenting kimchi" vs "Korean cooking" -> Is this related to Hobbies? No. -> INSERT "----"
|
|
678
|
+
|
|
679
|
+
JSON Output Structure:
|
|
680
|
+
{
|
|
681
|
+
"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 ],
|
|
682
|
+
"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, "----" , - ]
|
|
683
|
+
}
|
|
684
|
+
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.
|
|
685
|
+
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.
|
|
686
|
+
VARIATION & SIMILARITY CONTROL: Maintain strict consistency. If the same semantic relationship appears multiple times, align it identically every time.
|
|
673
687
|
`;
|
|
674
688
|
const body = {
|
|
675
689
|
model: this.MODEL,
|
|
@@ -863,40 +877,40 @@ class OpenAIEmbeddingService {
|
|
|
863
877
|
[OpenAIEmbeddingService.HEADER_AUTHORIZATION]: `${OpenAIEmbeddingService.BEARER_PREFIX}${apiKey}`
|
|
864
878
|
});
|
|
865
879
|
// Construct the prompt
|
|
866
|
-
const prompt = `
|
|
867
|
-
You are an expert semantic alignment engine.
|
|
868
|
-
Task: Align the items from List A to List B based on semantic similarity.
|
|
869
|
-
|
|
870
|
-
List A: ${JSON.stringify(validA)}
|
|
871
|
-
List B: ${JSON.stringify(validB)}
|
|
872
|
-
|
|
873
|
-
Rules:
|
|
874
|
-
1. Match items that are semantically similar.
|
|
875
|
-
2. Support One-to-One, One-to-Many, and Many-to-Many matches.
|
|
876
|
-
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.
|
|
877
|
-
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.
|
|
878
|
-
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).
|
|
879
|
-
6. If an item in List A has no match in List B, map it to "-".
|
|
880
|
-
7. If an item in List B is not matched to anything in List A, add a row with "-" on the left.
|
|
881
|
-
8. Output MUST be a JSON object with a single key "alignment" containing an array of objects.
|
|
882
|
-
9. Each object in the array must have:
|
|
883
|
-
- "left": string (item from List A or "-")
|
|
884
|
-
- "right": string (item from List B or "-")
|
|
885
|
-
- "score": number (similarity score between 0 and 1)
|
|
886
|
-
|
|
887
|
-
Example Output Format:
|
|
888
|
-
{
|
|
889
|
-
"alignment": [
|
|
890
|
-
{ "left": "Seafood", "right": "Fish", "score": 0.9 },
|
|
891
|
-
{ "left": "Seafood", "right": "Clams", "score": 0.85 },
|
|
892
|
-
{ "left": "Seafood", "right": "Crab", "score": 0.88 },
|
|
893
|
-
{ "left": "Chicken", "right": "Meat", "score": 0.8 },
|
|
894
|
-
{ "left": "Chicken", "right": "Poultry", "score": 0.9 },
|
|
895
|
-
{ "left": "Chicken", "right": "Mutton", "score": 0.85 },
|
|
896
|
-
{ "left": "Coffee", "right": "Cafés", "score": 0.9 },
|
|
897
|
-
{ "left": "Coding", "right": "-", "score": 0 }
|
|
898
|
-
]
|
|
899
|
-
}
|
|
880
|
+
const prompt = `
|
|
881
|
+
You are an expert semantic alignment engine.
|
|
882
|
+
Task: Align the items from List A to List B based on semantic similarity.
|
|
883
|
+
|
|
884
|
+
List A: ${JSON.stringify(validA)}
|
|
885
|
+
List B: ${JSON.stringify(validB)}
|
|
886
|
+
|
|
887
|
+
Rules:
|
|
888
|
+
1. Match items that are semantically similar.
|
|
889
|
+
2. Support One-to-One, One-to-Many, and Many-to-Many matches.
|
|
890
|
+
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.
|
|
891
|
+
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.
|
|
892
|
+
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).
|
|
893
|
+
6. If an item in List A has no match in List B, map it to "-".
|
|
894
|
+
7. If an item in List B is not matched to anything in List A, add a row with "-" on the left.
|
|
895
|
+
8. Output MUST be a JSON object with a single key "alignment" containing an array of objects.
|
|
896
|
+
9. Each object in the array must have:
|
|
897
|
+
- "left": string (item from List A or "-")
|
|
898
|
+
- "right": string (item from List B or "-")
|
|
899
|
+
- "score": number (similarity score between 0 and 1)
|
|
900
|
+
|
|
901
|
+
Example Output Format:
|
|
902
|
+
{
|
|
903
|
+
"alignment": [
|
|
904
|
+
{ "left": "Seafood", "right": "Fish", "score": 0.9 },
|
|
905
|
+
{ "left": "Seafood", "right": "Clams", "score": 0.85 },
|
|
906
|
+
{ "left": "Seafood", "right": "Crab", "score": 0.88 },
|
|
907
|
+
{ "left": "Chicken", "right": "Meat", "score": 0.8 },
|
|
908
|
+
{ "left": "Chicken", "right": "Poultry", "score": 0.9 },
|
|
909
|
+
{ "left": "Chicken", "right": "Mutton", "score": 0.85 },
|
|
910
|
+
{ "left": "Coffee", "right": "Cafés", "score": 0.9 },
|
|
911
|
+
{ "left": "Coding", "right": "-", "score": 0 }
|
|
912
|
+
]
|
|
913
|
+
}
|
|
900
914
|
`;
|
|
901
915
|
const body = {
|
|
902
916
|
model: this.MODEL,
|
|
@@ -1215,10 +1229,10 @@ class OpenAIEmbeddingService {
|
|
|
1215
1229
|
this.persistence.clear(CachePersistenceService.STORE_OPENAI_SIMILARITY);
|
|
1216
1230
|
this.persistence.clear(CachePersistenceService.STORE_OPENAI_EMBEDDINGS);
|
|
1217
1231
|
}
|
|
1218
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.
|
|
1219
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.
|
|
1232
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.26", ngImport: i0, type: OpenAIEmbeddingService, deps: [{ token: i1.HttpClient }, { token: PROFILE_COMPARISON_API_BASE_URL, optional: true }, { token: CachePersistenceService }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1233
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.26", ngImport: i0, type: OpenAIEmbeddingService, providedIn: 'root' });
|
|
1220
1234
|
}
|
|
1221
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.
|
|
1235
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.26", ngImport: i0, type: OpenAIEmbeddingService, decorators: [{
|
|
1222
1236
|
type: Injectable,
|
|
1223
1237
|
args: [{
|
|
1224
1238
|
providedIn: 'root'
|
|
@@ -1250,10 +1264,10 @@ class ProfileComparisonLibService {
|
|
|
1250
1264
|
getVersion() {
|
|
1251
1265
|
return '0.0.0';
|
|
1252
1266
|
}
|
|
1253
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.
|
|
1254
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.
|
|
1267
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.26", ngImport: i0, type: ProfileComparisonLibService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1268
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.26", ngImport: i0, type: ProfileComparisonLibService, providedIn: 'root' });
|
|
1255
1269
|
}
|
|
1256
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.
|
|
1270
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.26", ngImport: i0, type: ProfileComparisonLibService, decorators: [{
|
|
1257
1271
|
type: Injectable,
|
|
1258
1272
|
args: [{
|
|
1259
1273
|
providedIn: 'root'
|
|
@@ -1507,10 +1521,10 @@ class ProfileService {
|
|
|
1507
1521
|
this.compareInterestsCache.clear();
|
|
1508
1522
|
this.persistence.clear(CachePersistenceService.STORE_PROFILE_COMPARE);
|
|
1509
1523
|
}
|
|
1510
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.
|
|
1511
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.
|
|
1524
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.26", ngImport: i0, type: ProfileService, deps: [{ token: i1.HttpClient }, { token: PROFILE_COMPARISON_API_BASE_URL, optional: true }, { token: CachePersistenceService }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1525
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.26", ngImport: i0, type: ProfileService, providedIn: 'root' });
|
|
1512
1526
|
}
|
|
1513
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.
|
|
1527
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.26", ngImport: i0, type: ProfileService, decorators: [{
|
|
1514
1528
|
type: Injectable,
|
|
1515
1529
|
args: [{ providedIn: 'root' }]
|
|
1516
1530
|
}], ctorParameters: () => [{ type: i1.HttpClient }, { type: undefined, decorators: [{
|
|
@@ -1566,10 +1580,10 @@ class ProfileComparisonBackendService {
|
|
|
1566
1580
|
return throwError(() => err);
|
|
1567
1581
|
}));
|
|
1568
1582
|
}
|
|
1569
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.
|
|
1570
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.
|
|
1583
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.26", ngImport: i0, type: ProfileComparisonBackendService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1584
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.26", ngImport: i0, type: ProfileComparisonBackendService, providedIn: 'root' });
|
|
1571
1585
|
}
|
|
1572
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.
|
|
1586
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.26", ngImport: i0, type: ProfileComparisonBackendService, decorators: [{
|
|
1573
1587
|
type: Injectable,
|
|
1574
1588
|
args: [{ providedIn: 'root' }]
|
|
1575
1589
|
}], ctorParameters: () => [] });
|
|
@@ -1718,6 +1732,39 @@ class ProfileComparisonLibComponent {
|
|
|
1718
1732
|
static MIN_CROSS_LINKS_PER_NODE = 0;
|
|
1719
1733
|
static MAX_INTRA_LINKS_PER_NODE = 2;
|
|
1720
1734
|
static MAX_RELEVANT_INTERESTS_PER_SIDE = 6;
|
|
1735
|
+
static COLUMN_CORE_SEED_PIXELS = 9;
|
|
1736
|
+
static IDLE_ALPHA_TARGET = 0.012;
|
|
1737
|
+
static CLICK_REHEAT_ALPHA = 0.065;
|
|
1738
|
+
static SEMANTIC_STABILITY_ALPHA_FLOOR = 0.012;
|
|
1739
|
+
static RELATIVE_MEDIAN_ALPHA_FLOOR = 0.01;
|
|
1740
|
+
static RELATIVE_MEDIAN_ATTRACT_STRENGTH = 0.44;
|
|
1741
|
+
static RELATIVE_MEDIAN_REPEL_STRENGTH = 0.58;
|
|
1742
|
+
static RELATIVE_MEDIAN_ATTRACT_RADIUS = 72;
|
|
1743
|
+
static RELATIVE_MEDIAN_REPEL_RADIUS = 194;
|
|
1744
|
+
static STRONG_SIMILARITY_BOOST_EXPONENT = 3.2;
|
|
1745
|
+
static STRONG_SIMILARITY_BOOST_SCALE = 0.5;
|
|
1746
|
+
static PRIORITY_PAIR_MIN_SIMILARITY = 0.62;
|
|
1747
|
+
static PRIORITY_PAIR_ATTRACTION_STRENGTH = 1.02;
|
|
1748
|
+
static PRIORITY_PAIR_CORRIDOR_RADIUS = 42;
|
|
1749
|
+
static PRIORITY_PAIR_CORRIDOR_REPEL_STRENGTH = 0.64;
|
|
1750
|
+
static MUTUAL_TOP_PAIR_MIN_SIMILARITY = 0.58;
|
|
1751
|
+
static MUTUAL_TOP_PAIR_MARGIN = 0.12;
|
|
1752
|
+
static MUTUAL_TOP_PAIR_ATTRACTION_STRENGTH = 1.12;
|
|
1753
|
+
static MUTUAL_TOP_PAIR_BARRIER_RADIUS = 56;
|
|
1754
|
+
static MUTUAL_TOP_PAIR_REPEL_STRENGTH = 1.06;
|
|
1755
|
+
static DETERMINISTIC_SEED_PASSES = 6;
|
|
1756
|
+
static DETERMINISTIC_SEED_BLEND = 0.86;
|
|
1757
|
+
static OBJECTIVE_OPTIMIZER_ITERATIONS = 56;
|
|
1758
|
+
static OBJECTIVE_STEP_START = 0.34;
|
|
1759
|
+
static OBJECTIVE_STEP_END = 0.09;
|
|
1760
|
+
static OBJECTIVE_ATTRACTION_WEIGHT = 0.74;
|
|
1761
|
+
static OBJECTIVE_REPULSION_WEIGHT = 0.5;
|
|
1762
|
+
static OBJECTIVE_ANCHOR_WEIGHT = 0.34;
|
|
1763
|
+
static OBJECTIVE_BOUNDARY_WEIGHT = 0.92;
|
|
1764
|
+
static DISSIMILAR_REPEL_THRESHOLD = 0.52;
|
|
1765
|
+
static DISSIMILAR_REPEL_STRENGTH = 0.34;
|
|
1766
|
+
static DISSIMILAR_REPEL_MAX_DISTANCE = 220;
|
|
1767
|
+
static DISSIMILAR_REPEL_DECAY_DISTANCE = 130;
|
|
1721
1768
|
static CHIP_RADIUS = 18; // half chip height for collision
|
|
1722
1769
|
static CHIP_COLLISION_BORDER_PADDING = 6;
|
|
1723
1770
|
static MAX_LINK_DISTANCE = 120;
|
|
@@ -1968,11 +2015,15 @@ class ProfileComparisonLibComponent {
|
|
|
1968
2015
|
this.log('fetchComparison', 'ignoring stale error', { requestId, current: this.fetchRequestId, status: err?.status });
|
|
1969
2016
|
return;
|
|
1970
2017
|
}
|
|
1971
|
-
this.log('fetchComparison', 'backend
|
|
1972
|
-
const msg = err?.error?.message ?? err?.message ?? err?.statusText ?? (typeof err?.error === 'string' ? err.error : null) ?? 'Request failed';
|
|
1973
|
-
this.backendError = msg;
|
|
1974
|
-
console.error('[ProfileComparisonLib] fetchComparison error', err?.status ?? '', msg);
|
|
2018
|
+
this.log('fetchComparison', 'backend unavailable, using raw interests as fallback', err);
|
|
1975
2019
|
this.isAligning = false;
|
|
2020
|
+
this.backendError = null;
|
|
2021
|
+
this.displayPerson1Interests = [...this.person1Interests];
|
|
2022
|
+
this.displayPerson2Interests = [...this.person2Interests];
|
|
2023
|
+
this.alignedPerson1Interests = [];
|
|
2024
|
+
this.alignedPerson2Interests = [];
|
|
2025
|
+
this.centerItem = this.findNaiveOverlap(this.person1Interests, this.person2Interests);
|
|
2026
|
+
this.buildForceGraph();
|
|
1976
2027
|
this.cdr.detectChanges();
|
|
1977
2028
|
}
|
|
1978
2029
|
});
|
|
@@ -2526,6 +2577,11 @@ class ProfileComparisonLibComponent {
|
|
|
2526
2577
|
}
|
|
2527
2578
|
return selected;
|
|
2528
2579
|
}
|
|
2580
|
+
findNaiveOverlap(list1, list2) {
|
|
2581
|
+
const norm = (s) => s.trim().toLowerCase();
|
|
2582
|
+
const set2 = new Set((list2 ?? []).map(norm));
|
|
2583
|
+
return (list1 ?? []).filter(item => set2.has(norm(item)));
|
|
2584
|
+
}
|
|
2529
2585
|
filterCenterItemsByThreshold(centerItems, p1Items, p2Items) {
|
|
2530
2586
|
if (!centerItems?.length)
|
|
2531
2587
|
return [];
|
|
@@ -2731,17 +2787,33 @@ class ProfileComparisonLibComponent {
|
|
|
2731
2787
|
if (matrixMax <= springThreshold)
|
|
2732
2788
|
matrixMax = 1.0;
|
|
2733
2789
|
const simRange = matrixMax - springThreshold;
|
|
2734
|
-
const
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2790
|
+
const columnCoreY = minCenterY + verticalSpan * 0.5;
|
|
2791
|
+
const getMedian = (values) => {
|
|
2792
|
+
if (!values.length)
|
|
2793
|
+
return 0;
|
|
2794
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
2795
|
+
const mid = Math.floor(sorted.length / 2);
|
|
2796
|
+
if (sorted.length % 2 === 1)
|
|
2797
|
+
return sorted[mid];
|
|
2798
|
+
return (sorted[mid - 1] + sorted[mid]) / 2;
|
|
2799
|
+
};
|
|
2800
|
+
const getTopSixMedian = (values) => {
|
|
2801
|
+
if (!values.length)
|
|
2802
|
+
return 0;
|
|
2803
|
+
const top = [...values].sort((a, b) => b - a).slice(0, Math.min(6, values.length));
|
|
2804
|
+
return getMedian(top);
|
|
2805
|
+
};
|
|
2806
|
+
const hashToken = (value) => {
|
|
2807
|
+
let hash = 0;
|
|
2808
|
+
for (let i = 0; i < value.length; i++) {
|
|
2809
|
+
hash = ((hash << 5) - hash + value.charCodeAt(i)) | 0;
|
|
2810
|
+
}
|
|
2811
|
+
return Math.abs(hash);
|
|
2739
2812
|
};
|
|
2740
2813
|
const nodes = [];
|
|
2741
2814
|
const addNodes = (items, group, zoneX) => {
|
|
2742
|
-
const total = items.length;
|
|
2743
2815
|
items.forEach((label, i) => {
|
|
2744
|
-
const targetY =
|
|
2816
|
+
const targetY = columnCoreY;
|
|
2745
2817
|
nodes.push({
|
|
2746
2818
|
id: `${group}-${i}-${label}`,
|
|
2747
2819
|
label,
|
|
@@ -2782,8 +2854,10 @@ class ProfileComparisonLibComponent {
|
|
|
2782
2854
|
: zoneP2 + (zoneShared - zoneP2) * centerPull;
|
|
2783
2855
|
}
|
|
2784
2856
|
});
|
|
2785
|
-
//
|
|
2786
|
-
//
|
|
2857
|
+
// Build a relative attractiveness score (median of top-six similarities)
|
|
2858
|
+
// and seed chips from each column core with only a tiny 9x9 perturbation.
|
|
2859
|
+
// This lets forces "emanate" from a shared origin while preserving
|
|
2860
|
+
// deterministic reset behavior.
|
|
2787
2861
|
const semanticPool = (node) => {
|
|
2788
2862
|
if (node.group === 'p1')
|
|
2789
2863
|
return nodes.filter(n => n.group === 'p2' || n.group === 'shared');
|
|
@@ -2791,35 +2865,50 @@ class ProfileComparisonLibComponent {
|
|
|
2791
2865
|
return nodes.filter(n => n.group === 'p1' || n.group === 'shared');
|
|
2792
2866
|
return nodes.filter(n => n.group === 'p1' || n.group === 'p2');
|
|
2793
2867
|
};
|
|
2794
|
-
const
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2868
|
+
const getSemanticAttractiveness = (node) => {
|
|
2869
|
+
const pool = semanticPool(node);
|
|
2870
|
+
if (!pool.length)
|
|
2871
|
+
return 0;
|
|
2872
|
+
const similarities = pool.map(other => Math.max(0, this.getSimilarity(node.label, other.label)));
|
|
2873
|
+
return getTopSixMedian(similarities);
|
|
2874
|
+
};
|
|
2875
|
+
const microSeedSpan = ProfileComparisonLibComponent.COLUMN_CORE_SEED_PIXELS;
|
|
2876
|
+
const microSeedHalf = microSeedSpan / 2;
|
|
2877
|
+
const deterministicPasses = ProfileComparisonLibComponent.DETERMINISTIC_SEED_PASSES;
|
|
2878
|
+
const deterministicBlend = ProfileComparisonLibComponent.DETERMINISTIC_SEED_BLEND;
|
|
2879
|
+
for (let pass = 0; pass < deterministicPasses; pass++) {
|
|
2880
|
+
['p1', 'shared', 'p2'].forEach(group => {
|
|
2881
|
+
const groupNodes = nodes.filter(node => node.group === group);
|
|
2882
|
+
if (groupNodes.length <= 1)
|
|
2803
2883
|
return;
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2884
|
+
const ranked = groupNodes
|
|
2885
|
+
.map(node => ({
|
|
2886
|
+
node,
|
|
2887
|
+
attractiveness: getSemanticAttractiveness(node),
|
|
2888
|
+
tie: this.normalizeSimilarityToken(node.label),
|
|
2889
|
+
}))
|
|
2890
|
+
.sort((a, b) => (b.attractiveness - a.attractiveness) || a.tie.localeCompare(b.tie));
|
|
2891
|
+
ranked.forEach((entry, index) => {
|
|
2892
|
+
const rankT = ranked.length <= 1 ? 0.5 : (index / Math.max(1, ranked.length - 1));
|
|
2893
|
+
const yOffset = (rankT - 0.5) * microSeedSpan;
|
|
2894
|
+
const desiredY = Math.max(minCenterY, Math.min(maxY, columnCoreY + yOffset));
|
|
2895
|
+
const currentY = entry.node.targetY ?? columnCoreY;
|
|
2896
|
+
entry.node.targetY = Math.max(minCenterY, Math.min(maxY, currentY + (desiredY - currentY) * deterministicBlend));
|
|
2897
|
+
const xSeedRaw = hashToken(`${entry.node.id}:${pass}`) % (microSeedSpan + 1);
|
|
2898
|
+
const xOffset = xSeedRaw - microSeedHalf;
|
|
2899
|
+
const baseTargetX = entry.node.targetX ?? (entry.node.group === 'p1'
|
|
2900
|
+
? zoneP1
|
|
2901
|
+
: (entry.node.group === 'p2' ? zoneP2 : zoneShared));
|
|
2902
|
+
entry.node.x = Math.max(globalMinX, Math.min(globalMaxX, baseTargetX + xOffset));
|
|
2903
|
+
entry.node.y = entry.node.targetY;
|
|
2814
2904
|
});
|
|
2815
|
-
if (totalWeight <= 0)
|
|
2816
|
-
return;
|
|
2817
|
-
const semanticY = weightedY / totalWeight;
|
|
2818
|
-
const currentY = snapshotY.get(node.id) ?? semanticY;
|
|
2819
|
-
const blend = node.group === 'shared' ? 0.84 : 0.78;
|
|
2820
|
-
node.targetY = Math.max(minCenterY, Math.min(maxY, currentY + (semanticY - currentY) * blend));
|
|
2821
2905
|
});
|
|
2822
2906
|
}
|
|
2907
|
+
nodes.forEach(node => {
|
|
2908
|
+
const fallbackX = node.targetX ?? (node.group === 'p1' ? zoneP1 : (node.group === 'p2' ? zoneP2 : zoneShared));
|
|
2909
|
+
node.x = Math.max(globalMinX, Math.min(globalMaxX, node.x ?? fallbackX));
|
|
2910
|
+
node.y = Math.max(minCenterY, Math.min(maxY, node.targetY ?? columnCoreY));
|
|
2911
|
+
});
|
|
2823
2912
|
const buildNodeBounds = (node) => {
|
|
2824
2913
|
let minX = globalMinX;
|
|
2825
2914
|
let maxX = globalMaxX;
|
|
@@ -2999,10 +3088,96 @@ class ProfileComparisonLibComponent {
|
|
|
2999
3088
|
};
|
|
3000
3089
|
})
|
|
3001
3090
|
.filter((record) => !!record);
|
|
3091
|
+
const dissimilarThreshold = ProfileComparisonLibComponent.DISSIMILAR_REPEL_THRESHOLD;
|
|
3092
|
+
const dissimilarPairs = [];
|
|
3093
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
3094
|
+
for (let j = i + 1; j < nodes.length; j++) {
|
|
3095
|
+
const source = nodes[i];
|
|
3096
|
+
const target = nodes[j];
|
|
3097
|
+
const similarity = Math.max(0, this.getSimilarity(source.label, target.label));
|
|
3098
|
+
if (similarity >= dissimilarThreshold)
|
|
3099
|
+
continue;
|
|
3100
|
+
const laneFactor = source.group === target.group
|
|
3101
|
+
? 1
|
|
3102
|
+
: (source.group === 'shared' || target.group === 'shared' ? 0.62 : 0.44);
|
|
3103
|
+
const repulsion = laneFactor * (dissimilarThreshold - similarity) / Math.max(0.001, dissimilarThreshold);
|
|
3104
|
+
dissimilarPairs.push({
|
|
3105
|
+
source,
|
|
3106
|
+
target,
|
|
3107
|
+
repulsion,
|
|
3108
|
+
});
|
|
3109
|
+
}
|
|
3110
|
+
}
|
|
3111
|
+
const relativeMedianByNode = new Map();
|
|
3112
|
+
const relativeMedianPairs = [];
|
|
3113
|
+
['p1', 'shared', 'p2'].forEach(group => {
|
|
3114
|
+
const groupNodes = nodes.filter(node => node.group === group);
|
|
3115
|
+
if (groupNodes.length <= 1)
|
|
3116
|
+
return;
|
|
3117
|
+
groupNodes.forEach(node => {
|
|
3118
|
+
const similarities = groupNodes
|
|
3119
|
+
.filter(other => other.id !== node.id)
|
|
3120
|
+
.map(other => Math.max(0, this.getSimilarity(node.label, other.label)));
|
|
3121
|
+
relativeMedianByNode.set(node.id, getTopSixMedian(similarities));
|
|
3122
|
+
});
|
|
3123
|
+
for (let i = 0; i < groupNodes.length; i++) {
|
|
3124
|
+
for (let j = i + 1; j < groupNodes.length; j++) {
|
|
3125
|
+
const source = groupNodes[i];
|
|
3126
|
+
const target = groupNodes[j];
|
|
3127
|
+
const similarity = Math.max(0, this.getSimilarity(source.label, target.label));
|
|
3128
|
+
const sourceMedian = relativeMedianByNode.get(source.id) ?? 0;
|
|
3129
|
+
const targetMedian = relativeMedianByNode.get(target.id) ?? 0;
|
|
3130
|
+
const median = (sourceMedian + targetMedian) / 2;
|
|
3131
|
+
relativeMedianPairs.push({ source, target, similarity, median });
|
|
3132
|
+
}
|
|
3133
|
+
}
|
|
3134
|
+
});
|
|
3002
3135
|
const maxLinkSimilarity = Math.max(0.001, ...linkRecords.map(record => Math.max(0, record.similarity)));
|
|
3003
3136
|
const MIN_PAIR_DISTANCE = 34;
|
|
3004
3137
|
const MAX_PAIR_DISTANCE = 88;
|
|
3138
|
+
const semanticAlphaFloor = ProfileComparisonLibComponent.SEMANTIC_STABILITY_ALPHA_FLOOR;
|
|
3139
|
+
const relativeMedianColumnForce = (alpha) => {
|
|
3140
|
+
const effectiveAlpha = Math.max(alpha, ProfileComparisonLibComponent.RELATIVE_MEDIAN_ALPHA_FLOOR);
|
|
3141
|
+
const attractStrength = ProfileComparisonLibComponent.RELATIVE_MEDIAN_ATTRACT_STRENGTH;
|
|
3142
|
+
const repelStrength = ProfileComparisonLibComponent.RELATIVE_MEDIAN_REPEL_STRENGTH;
|
|
3143
|
+
const attractRadius = ProfileComparisonLibComponent.RELATIVE_MEDIAN_ATTRACT_RADIUS;
|
|
3144
|
+
const repelRadius = ProfileComparisonLibComponent.RELATIVE_MEDIAN_REPEL_RADIUS;
|
|
3145
|
+
const deltaDeadband = 0.025;
|
|
3146
|
+
relativeMedianPairs.forEach(pair => {
|
|
3147
|
+
const dx = (pair.target.x ?? 0) - (pair.source.x ?? 0);
|
|
3148
|
+
const dy = (pair.target.y ?? 0) - (pair.source.y ?? 0);
|
|
3149
|
+
const dist = Math.sqrt(dx * dx + dy * dy) || 1;
|
|
3150
|
+
const delta = pair.similarity - pair.median;
|
|
3151
|
+
if (Math.abs(delta) <= deltaDeadband)
|
|
3152
|
+
return;
|
|
3153
|
+
if (delta >= 0) {
|
|
3154
|
+
const closenessBias = Math.max(0.08, Math.min(1, delta + 0.08));
|
|
3155
|
+
const targetDistance = Math.max(24, attractRadius - closenessBias * 46);
|
|
3156
|
+
if (dist <= targetDistance + 2)
|
|
3157
|
+
return;
|
|
3158
|
+
const pull = attractStrength * effectiveAlpha * closenessBias * ((dist - targetDistance) / dist);
|
|
3159
|
+
pair.source.vx = (pair.source.vx ?? 0) + dx * pull;
|
|
3160
|
+
pair.source.vy = (pair.source.vy ?? 0) + dy * pull;
|
|
3161
|
+
pair.target.vx = (pair.target.vx ?? 0) - dx * pull;
|
|
3162
|
+
pair.target.vy = (pair.target.vy ?? 0) - dy * pull;
|
|
3163
|
+
return;
|
|
3164
|
+
}
|
|
3165
|
+
if (dist >= repelRadius || dist <= 28)
|
|
3166
|
+
return;
|
|
3167
|
+
const underMedian = Math.min(1, Math.abs(delta) / Math.max(0.001, pair.median + 0.05));
|
|
3168
|
+
const push = repelStrength * effectiveAlpha * underMedian * (1 - (dist / repelRadius));
|
|
3169
|
+
if (push <= 0)
|
|
3170
|
+
return;
|
|
3171
|
+
const unitX = dx / dist;
|
|
3172
|
+
const unitY = dy / dist;
|
|
3173
|
+
pair.source.vx = (pair.source.vx ?? 0) - unitX * push;
|
|
3174
|
+
pair.source.vy = (pair.source.vy ?? 0) - unitY * push;
|
|
3175
|
+
pair.target.vx = (pair.target.vx ?? 0) + unitX * push;
|
|
3176
|
+
pair.target.vy = (pair.target.vy ?? 0) + unitY * push;
|
|
3177
|
+
});
|
|
3178
|
+
};
|
|
3005
3179
|
const similarityAttractionForce = (alpha) => {
|
|
3180
|
+
const effectiveAlpha = Math.max(alpha, semanticAlphaFloor);
|
|
3006
3181
|
linkRecords.forEach(record => {
|
|
3007
3182
|
const dx = (record.target.x ?? 0) - (record.source.x ?? 0);
|
|
3008
3183
|
const dy = (record.target.y ?? 0) - (record.source.y ?? 0);
|
|
@@ -3012,14 +3187,348 @@ class ProfileComparisonLibComponent {
|
|
|
3012
3187
|
if (dist <= targetDistance)
|
|
3013
3188
|
return;
|
|
3014
3189
|
const pullGap = dist - targetDistance;
|
|
3015
|
-
const pullBase = record.kind === 'cross' ? 0.
|
|
3016
|
-
const
|
|
3190
|
+
const pullBase = record.kind === 'cross' ? 0.86 : 0.78;
|
|
3191
|
+
const nonlinearBoost = 1
|
|
3192
|
+
+ Math.pow(normalizedSimilarity, ProfileComparisonLibComponent.STRONG_SIMILARITY_BOOST_EXPONENT)
|
|
3193
|
+
* ProfileComparisonLibComponent.STRONG_SIMILARITY_BOOST_SCALE;
|
|
3194
|
+
const pullFactor = pullBase * nonlinearBoost * effectiveAlpha * (pullGap / dist);
|
|
3017
3195
|
record.source.vx = (record.source.vx ?? 0) + dx * pullFactor;
|
|
3018
3196
|
record.source.vy = (record.source.vy ?? 0) + dy * pullFactor;
|
|
3019
3197
|
record.target.vx = (record.target.vx ?? 0) - dx * pullFactor;
|
|
3020
3198
|
record.target.vy = (record.target.vy ?? 0) - dy * pullFactor;
|
|
3021
3199
|
});
|
|
3022
3200
|
};
|
|
3201
|
+
const dissimilarRepulsionForce = (alpha) => {
|
|
3202
|
+
const effectiveAlpha = Math.max(alpha, semanticAlphaFloor * 0.9);
|
|
3203
|
+
const maxDistance = ProfileComparisonLibComponent.DISSIMILAR_REPEL_MAX_DISTANCE;
|
|
3204
|
+
const decayDistance = ProfileComparisonLibComponent.DISSIMILAR_REPEL_DECAY_DISTANCE;
|
|
3205
|
+
const baseStrength = ProfileComparisonLibComponent.DISSIMILAR_REPEL_STRENGTH;
|
|
3206
|
+
dissimilarPairs.forEach(record => {
|
|
3207
|
+
const dx = (record.target.x ?? 0) - (record.source.x ?? 0);
|
|
3208
|
+
const dy = (record.target.y ?? 0) - (record.source.y ?? 0);
|
|
3209
|
+
const rawDistance = Math.sqrt(dx * dx + dy * dy) || 1;
|
|
3210
|
+
if (rawDistance >= maxDistance)
|
|
3211
|
+
return;
|
|
3212
|
+
const distance = Math.max(8, rawDistance);
|
|
3213
|
+
const closeness = 1 - Math.min(1, distance / maxDistance);
|
|
3214
|
+
const decay = Math.exp(-Math.pow(distance / Math.max(1, decayDistance), 2));
|
|
3215
|
+
const push = baseStrength * effectiveAlpha * record.repulsion * closeness * decay;
|
|
3216
|
+
if (push <= 0)
|
|
3217
|
+
return;
|
|
3218
|
+
const unitX = dx / distance;
|
|
3219
|
+
const unitY = dy / distance;
|
|
3220
|
+
record.source.vx = (record.source.vx ?? 0) - unitX * push;
|
|
3221
|
+
record.source.vy = (record.source.vy ?? 0) - unitY * push;
|
|
3222
|
+
record.target.vx = (record.target.vx ?? 0) + unitX * push;
|
|
3223
|
+
record.target.vy = (record.target.vy ?? 0) + unitY * push;
|
|
3224
|
+
});
|
|
3225
|
+
};
|
|
3226
|
+
const priorityMinSimilarity = ProfileComparisonLibComponent.PRIORITY_PAIR_MIN_SIMILARITY;
|
|
3227
|
+
const priorityPairCandidates = [...crossCandidates, ...intraCandidates]
|
|
3228
|
+
.filter(link => link.similarity >= priorityMinSimilarity)
|
|
3229
|
+
.sort((a, b) => (b.similarity - a.similarity) || (b.strength - a.strength));
|
|
3230
|
+
const priorityPairKeys = new Set();
|
|
3231
|
+
const endpointClaims = new Map();
|
|
3232
|
+
const maxPairsPerNode = 2;
|
|
3233
|
+
const getClaimCount = (id) => endpointClaims.get(id) ?? 0;
|
|
3234
|
+
const priorityPairs = priorityPairCandidates
|
|
3235
|
+
.filter(candidate => {
|
|
3236
|
+
const sourceId = this.getLinkNodeId(candidate.source);
|
|
3237
|
+
const targetId = this.getLinkNodeId(candidate.target);
|
|
3238
|
+
const key = this.getLinkPairKey(sourceId, targetId);
|
|
3239
|
+
if (priorityPairKeys.has(key))
|
|
3240
|
+
return false;
|
|
3241
|
+
if (getClaimCount(sourceId) >= maxPairsPerNode || getClaimCount(targetId) >= maxPairsPerNode)
|
|
3242
|
+
return false;
|
|
3243
|
+
priorityPairKeys.add(key);
|
|
3244
|
+
endpointClaims.set(sourceId, getClaimCount(sourceId) + 1);
|
|
3245
|
+
endpointClaims.set(targetId, getClaimCount(targetId) + 1);
|
|
3246
|
+
return true;
|
|
3247
|
+
})
|
|
3248
|
+
.map(link => {
|
|
3249
|
+
const source = typeof link.source === 'object' ? link.source : nodeById.get(String(link.source));
|
|
3250
|
+
const target = typeof link.target === 'object' ? link.target : nodeById.get(String(link.target));
|
|
3251
|
+
if (!source || !target)
|
|
3252
|
+
return null;
|
|
3253
|
+
return {
|
|
3254
|
+
source,
|
|
3255
|
+
target,
|
|
3256
|
+
similarity: Math.max(priorityMinSimilarity, Number(link.similarity ?? 0)),
|
|
3257
|
+
};
|
|
3258
|
+
})
|
|
3259
|
+
.filter((pair) => !!pair);
|
|
3260
|
+
const maxPrioritySimilarity = Math.max(priorityMinSimilarity, ...priorityPairs.map(pair => pair.similarity));
|
|
3261
|
+
const priorityPairForce = (alpha) => {
|
|
3262
|
+
const effectiveAlpha = Math.max(alpha, semanticAlphaFloor);
|
|
3263
|
+
const pairStrength = ProfileComparisonLibComponent.PRIORITY_PAIR_ATTRACTION_STRENGTH;
|
|
3264
|
+
const corridorRadius = ProfileComparisonLibComponent.PRIORITY_PAIR_CORRIDOR_RADIUS;
|
|
3265
|
+
const corridorRepel = ProfileComparisonLibComponent.PRIORITY_PAIR_CORRIDOR_REPEL_STRENGTH;
|
|
3266
|
+
priorityPairs.forEach(pair => {
|
|
3267
|
+
const ax = pair.source.x ?? 0;
|
|
3268
|
+
const ay = pair.source.y ?? 0;
|
|
3269
|
+
const bx = pair.target.x ?? 0;
|
|
3270
|
+
const by = pair.target.y ?? 0;
|
|
3271
|
+
const dx = bx - ax;
|
|
3272
|
+
const dy = by - ay;
|
|
3273
|
+
const dist = Math.sqrt(dx * dx + dy * dy) || 1;
|
|
3274
|
+
const similarityNorm = Math.max(0, Math.min(1, pair.similarity / Math.max(0.001, maxPrioritySimilarity)));
|
|
3275
|
+
const targetDistance = 26 + (1 - similarityNorm) * 20;
|
|
3276
|
+
if (dist > targetDistance) {
|
|
3277
|
+
const pull = pairStrength * effectiveAlpha * ((dist - targetDistance) / dist);
|
|
3278
|
+
pair.source.vx = (pair.source.vx ?? 0) + dx * pull;
|
|
3279
|
+
pair.source.vy = (pair.source.vy ?? 0) + dy * pull;
|
|
3280
|
+
pair.target.vx = (pair.target.vx ?? 0) - dx * pull;
|
|
3281
|
+
pair.target.vy = (pair.target.vy ?? 0) - dy * pull;
|
|
3282
|
+
}
|
|
3283
|
+
const segmentLenSq = dx * dx + dy * dy;
|
|
3284
|
+
if (segmentLenSq <= 1)
|
|
3285
|
+
return;
|
|
3286
|
+
nodes.forEach(candidate => {
|
|
3287
|
+
if (candidate.id === pair.source.id || candidate.id === pair.target.id)
|
|
3288
|
+
return;
|
|
3289
|
+
const candidateSimilarity = Math.max(this.getSimilarity(candidate.label, pair.source.label), this.getSimilarity(candidate.label, pair.target.label));
|
|
3290
|
+
if (candidateSimilarity >= pair.similarity * 0.74)
|
|
3291
|
+
return;
|
|
3292
|
+
const px = candidate.x ?? 0;
|
|
3293
|
+
const py = candidate.y ?? 0;
|
|
3294
|
+
const tRaw = ((px - ax) * dx + (py - ay) * dy) / segmentLenSq;
|
|
3295
|
+
const t = Math.max(0, Math.min(1, tRaw));
|
|
3296
|
+
if (t <= 0.12 || t >= 0.88)
|
|
3297
|
+
return;
|
|
3298
|
+
const closestX = ax + dx * t;
|
|
3299
|
+
const closestY = ay + dy * t;
|
|
3300
|
+
const offX = px - closestX;
|
|
3301
|
+
const offY = py - closestY;
|
|
3302
|
+
const offDist = Math.sqrt(offX * offX + offY * offY) || 1;
|
|
3303
|
+
if (offDist >= corridorRadius)
|
|
3304
|
+
return;
|
|
3305
|
+
const proximity = 1 - (offDist / corridorRadius);
|
|
3306
|
+
const mismatch = 1 - Math.max(0, Math.min(1, candidateSimilarity / Math.max(0.001, pair.similarity)));
|
|
3307
|
+
const push = corridorRepel * effectiveAlpha * proximity * mismatch;
|
|
3308
|
+
if (push <= 0)
|
|
3309
|
+
return;
|
|
3310
|
+
const unitOffX = offX / offDist;
|
|
3311
|
+
const unitOffY = offY / offDist;
|
|
3312
|
+
candidate.vx = (candidate.vx ?? 0) + unitOffX * push;
|
|
3313
|
+
candidate.vy = (candidate.vy ?? 0) + unitOffY * push;
|
|
3314
|
+
// Counter-force keeps the pair stable while opening a semantic corridor.
|
|
3315
|
+
const damp = push * 0.24;
|
|
3316
|
+
pair.source.vx = (pair.source.vx ?? 0) - unitOffX * damp;
|
|
3317
|
+
pair.source.vy = (pair.source.vy ?? 0) - unitOffY * damp;
|
|
3318
|
+
pair.target.vx = (pair.target.vx ?? 0) - unitOffX * damp;
|
|
3319
|
+
pair.target.vy = (pair.target.vy ?? 0) - unitOffY * damp;
|
|
3320
|
+
});
|
|
3321
|
+
});
|
|
3322
|
+
};
|
|
3323
|
+
const bestByNode = new Map();
|
|
3324
|
+
nodes.forEach(node => {
|
|
3325
|
+
let bestNode = null;
|
|
3326
|
+
let best = -1;
|
|
3327
|
+
let second = -1;
|
|
3328
|
+
nodes.forEach(other => {
|
|
3329
|
+
if (other.id === node.id)
|
|
3330
|
+
return;
|
|
3331
|
+
const similarity = Math.max(0, this.getSimilarity(node.label, other.label));
|
|
3332
|
+
if (similarity > best) {
|
|
3333
|
+
second = best;
|
|
3334
|
+
best = similarity;
|
|
3335
|
+
bestNode = other;
|
|
3336
|
+
}
|
|
3337
|
+
else if (similarity > second) {
|
|
3338
|
+
second = similarity;
|
|
3339
|
+
}
|
|
3340
|
+
});
|
|
3341
|
+
bestByNode.set(node.id, { node: bestNode, best: Math.max(0, best), second: Math.max(0, second) });
|
|
3342
|
+
});
|
|
3343
|
+
const mutualPairMinSimilarity = ProfileComparisonLibComponent.MUTUAL_TOP_PAIR_MIN_SIMILARITY;
|
|
3344
|
+
const mutualPairMargin = ProfileComparisonLibComponent.MUTUAL_TOP_PAIR_MARGIN;
|
|
3345
|
+
const mutualPairKeys = new Set();
|
|
3346
|
+
const mutualTopPairs = nodes
|
|
3347
|
+
.map(node => {
|
|
3348
|
+
const best = bestByNode.get(node.id);
|
|
3349
|
+
if (!best?.node)
|
|
3350
|
+
return null;
|
|
3351
|
+
if (best.best < mutualPairMinSimilarity)
|
|
3352
|
+
return null;
|
|
3353
|
+
if (best.best - best.second < mutualPairMargin)
|
|
3354
|
+
return null;
|
|
3355
|
+
const reverse = bestByNode.get(best.node.id);
|
|
3356
|
+
if (!reverse?.node || reverse.node.id !== node.id)
|
|
3357
|
+
return null;
|
|
3358
|
+
if (reverse.best < mutualPairMinSimilarity || reverse.best - reverse.second < mutualPairMargin)
|
|
3359
|
+
return null;
|
|
3360
|
+
const key = this.getLinkPairKey(node.id, best.node.id);
|
|
3361
|
+
if (mutualPairKeys.has(key))
|
|
3362
|
+
return null;
|
|
3363
|
+
mutualPairKeys.add(key);
|
|
3364
|
+
return {
|
|
3365
|
+
source: node,
|
|
3366
|
+
target: best.node,
|
|
3367
|
+
similarity: Math.min(best.best, reverse.best),
|
|
3368
|
+
};
|
|
3369
|
+
})
|
|
3370
|
+
.filter((pair) => !!pair);
|
|
3371
|
+
const mutualTopPairForce = (alpha) => {
|
|
3372
|
+
const effectiveAlpha = Math.max(alpha, semanticAlphaFloor * 1.15);
|
|
3373
|
+
const attractionStrength = ProfileComparisonLibComponent.MUTUAL_TOP_PAIR_ATTRACTION_STRENGTH;
|
|
3374
|
+
const baseBarrierRadius = ProfileComparisonLibComponent.MUTUAL_TOP_PAIR_BARRIER_RADIUS;
|
|
3375
|
+
const repelStrength = ProfileComparisonLibComponent.MUTUAL_TOP_PAIR_REPEL_STRENGTH;
|
|
3376
|
+
mutualTopPairs.forEach(pair => {
|
|
3377
|
+
const ax = pair.source.x ?? 0;
|
|
3378
|
+
const ay = pair.source.y ?? 0;
|
|
3379
|
+
const bx = pair.target.x ?? 0;
|
|
3380
|
+
const by = pair.target.y ?? 0;
|
|
3381
|
+
const dx = bx - ax;
|
|
3382
|
+
const dy = by - ay;
|
|
3383
|
+
const dist = Math.sqrt(dx * dx + dy * dy) || 1;
|
|
3384
|
+
const norm = Math.max(0, Math.min(1, pair.similarity));
|
|
3385
|
+
const targetDistance = 24 + (1 - norm) * 26;
|
|
3386
|
+
if (dist > targetDistance) {
|
|
3387
|
+
const pull = attractionStrength * effectiveAlpha * ((dist - targetDistance) / dist);
|
|
3388
|
+
pair.source.vx = (pair.source.vx ?? 0) + dx * pull;
|
|
3389
|
+
pair.source.vy = (pair.source.vy ?? 0) + dy * pull;
|
|
3390
|
+
pair.target.vx = (pair.target.vx ?? 0) - dx * pull;
|
|
3391
|
+
pair.target.vy = (pair.target.vy ?? 0) - dy * pull;
|
|
3392
|
+
}
|
|
3393
|
+
const segmentLenSq = dx * dx + dy * dy;
|
|
3394
|
+
if (segmentLenSq <= 1)
|
|
3395
|
+
return;
|
|
3396
|
+
const barrierRadius = baseBarrierRadius + (1 - norm) * 10;
|
|
3397
|
+
nodes.forEach(candidate => {
|
|
3398
|
+
if (candidate.id === pair.source.id || candidate.id === pair.target.id)
|
|
3399
|
+
return;
|
|
3400
|
+
const candidateSimilarity = Math.max(this.getSimilarity(candidate.label, pair.source.label), this.getSimilarity(candidate.label, pair.target.label));
|
|
3401
|
+
if (candidateSimilarity >= pair.similarity * 0.76)
|
|
3402
|
+
return;
|
|
3403
|
+
const px = candidate.x ?? 0;
|
|
3404
|
+
const py = candidate.y ?? 0;
|
|
3405
|
+
const tRaw = ((px - ax) * dx + (py - ay) * dy) / segmentLenSq;
|
|
3406
|
+
const t = Math.max(0, Math.min(1, tRaw));
|
|
3407
|
+
if (t <= 0.08 || t >= 0.92)
|
|
3408
|
+
return;
|
|
3409
|
+
const closestX = ax + dx * t;
|
|
3410
|
+
const closestY = ay + dy * t;
|
|
3411
|
+
const offX = px - closestX;
|
|
3412
|
+
const offY = py - closestY;
|
|
3413
|
+
const offDist = Math.sqrt(offX * offX + offY * offY) || 1;
|
|
3414
|
+
if (offDist >= barrierRadius)
|
|
3415
|
+
return;
|
|
3416
|
+
const proximity = 1 - (offDist / barrierRadius);
|
|
3417
|
+
const mismatch = 1 - Math.max(0, Math.min(1, candidateSimilarity / Math.max(0.001, pair.similarity)));
|
|
3418
|
+
const push = repelStrength * effectiveAlpha * proximity * mismatch;
|
|
3419
|
+
if (push <= 0)
|
|
3420
|
+
return;
|
|
3421
|
+
const unitOffX = offX / offDist;
|
|
3422
|
+
const unitOffY = offY / offDist;
|
|
3423
|
+
candidate.vx = (candidate.vx ?? 0) + unitOffX * push;
|
|
3424
|
+
candidate.vy = (candidate.vy ?? 0) + unitOffY * push;
|
|
3425
|
+
const damp = push * 0.26;
|
|
3426
|
+
pair.source.vx = (pair.source.vx ?? 0) - unitOffX * damp;
|
|
3427
|
+
pair.source.vy = (pair.source.vy ?? 0) - unitOffY * damp;
|
|
3428
|
+
pair.target.vx = (pair.target.vx ?? 0) - unitOffX * damp;
|
|
3429
|
+
pair.target.vy = (pair.target.vy ?? 0) - unitOffY * damp;
|
|
3430
|
+
});
|
|
3431
|
+
});
|
|
3432
|
+
};
|
|
3433
|
+
const runObjectiveOptimizer = () => {
|
|
3434
|
+
if (!nodes.length)
|
|
3435
|
+
return;
|
|
3436
|
+
const attractionFloor = springThreshold;
|
|
3437
|
+
const dissimilarFloor = dissimilarThreshold;
|
|
3438
|
+
const pairRecords = [];
|
|
3439
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
3440
|
+
for (let j = i + 1; j < nodes.length; j++) {
|
|
3441
|
+
const similarity = Math.max(0, this.getSimilarity(nodes[i].label, nodes[j].label));
|
|
3442
|
+
if (similarity <= dissimilarFloor || similarity >= attractionFloor) {
|
|
3443
|
+
pairRecords.push({ a: nodes[i], b: nodes[j], similarity });
|
|
3444
|
+
}
|
|
3445
|
+
}
|
|
3446
|
+
}
|
|
3447
|
+
const iterations = ProfileComparisonLibComponent.OBJECTIVE_OPTIMIZER_ITERATIONS;
|
|
3448
|
+
const stepStart = ProfileComparisonLibComponent.OBJECTIVE_STEP_START;
|
|
3449
|
+
const stepEnd = ProfileComparisonLibComponent.OBJECTIVE_STEP_END;
|
|
3450
|
+
const attractionWeight = ProfileComparisonLibComponent.OBJECTIVE_ATTRACTION_WEIGHT;
|
|
3451
|
+
const repulsionWeight = ProfileComparisonLibComponent.OBJECTIVE_REPULSION_WEIGHT;
|
|
3452
|
+
const anchorWeight = ProfileComparisonLibComponent.OBJECTIVE_ANCHOR_WEIGHT;
|
|
3453
|
+
const boundaryWeight = ProfileComparisonLibComponent.OBJECTIVE_BOUNDARY_WEIGHT;
|
|
3454
|
+
for (let iteration = 0; iteration < iterations; iteration++) {
|
|
3455
|
+
const progress = iteration / Math.max(1, iterations - 1);
|
|
3456
|
+
const step = stepStart + (stepEnd - stepStart) * progress;
|
|
3457
|
+
const gradient = new Map();
|
|
3458
|
+
nodes.forEach(node => gradient.set(node.id, { x: 0, y: 0 }));
|
|
3459
|
+
pairRecords.forEach(record => {
|
|
3460
|
+
const ax = record.a.x ?? record.a.targetX ?? 0;
|
|
3461
|
+
const ay = record.a.y ?? record.a.targetY ?? 0;
|
|
3462
|
+
const bx = record.b.x ?? record.b.targetX ?? 0;
|
|
3463
|
+
const by = record.b.y ?? record.b.targetY ?? 0;
|
|
3464
|
+
const dx = bx - ax;
|
|
3465
|
+
const dy = by - ay;
|
|
3466
|
+
const dist = Math.sqrt(dx * dx + dy * dy) || 1;
|
|
3467
|
+
const unitX = dx / dist;
|
|
3468
|
+
const unitY = dy / dist;
|
|
3469
|
+
if (record.similarity >= attractionFloor) {
|
|
3470
|
+
const normalized = Math.max(0, Math.min(1, (record.similarity - attractionFloor) / Math.max(0.001, 1 - attractionFloor)));
|
|
3471
|
+
const targetDistance = 24 + (1 - normalized) * 66;
|
|
3472
|
+
if (dist > targetDistance) {
|
|
3473
|
+
const pull = attractionWeight * Math.pow(normalized, 1.4) * ((dist - targetDistance) / dist);
|
|
3474
|
+
const gradA = gradient.get(record.a.id);
|
|
3475
|
+
const gradB = gradient.get(record.b.id);
|
|
3476
|
+
if (gradA && gradB) {
|
|
3477
|
+
gradA.x += dx * pull;
|
|
3478
|
+
gradA.y += dy * pull;
|
|
3479
|
+
gradB.x -= dx * pull;
|
|
3480
|
+
gradB.y -= dy * pull;
|
|
3481
|
+
}
|
|
3482
|
+
}
|
|
3483
|
+
return;
|
|
3484
|
+
}
|
|
3485
|
+
if (record.similarity > dissimilarFloor)
|
|
3486
|
+
return;
|
|
3487
|
+
const normalized = (dissimilarFloor - record.similarity) / Math.max(0.001, dissimilarFloor);
|
|
3488
|
+
const maxDistance = 210;
|
|
3489
|
+
if (dist >= maxDistance)
|
|
3490
|
+
return;
|
|
3491
|
+
const push = repulsionWeight * normalized * (1 - (dist / maxDistance));
|
|
3492
|
+
const gradA = gradient.get(record.a.id);
|
|
3493
|
+
const gradB = gradient.get(record.b.id);
|
|
3494
|
+
if (gradA && gradB) {
|
|
3495
|
+
gradA.x -= unitX * push;
|
|
3496
|
+
gradA.y -= unitY * push;
|
|
3497
|
+
gradB.x += unitX * push;
|
|
3498
|
+
gradB.y += unitY * push;
|
|
3499
|
+
}
|
|
3500
|
+
});
|
|
3501
|
+
nodes.forEach(node => {
|
|
3502
|
+
const grad = gradient.get(node.id);
|
|
3503
|
+
if (!grad)
|
|
3504
|
+
return;
|
|
3505
|
+
const currentX = node.x ?? node.targetX ?? zoneShared;
|
|
3506
|
+
const currentY = node.y ?? node.targetY ?? minCenterY;
|
|
3507
|
+
const targetX = node.targetX ?? currentX;
|
|
3508
|
+
const targetY = node.targetY ?? currentY;
|
|
3509
|
+
grad.x += (targetX - currentX) * anchorWeight;
|
|
3510
|
+
grad.y += (targetY - currentY) * anchorWeight;
|
|
3511
|
+
const bounds = nodeBounds.get(node.id) ?? buildNodeBounds(node);
|
|
3512
|
+
if (currentX < bounds.minX)
|
|
3513
|
+
grad.x += (bounds.minX - currentX) * boundaryWeight;
|
|
3514
|
+
if (currentX > bounds.maxX)
|
|
3515
|
+
grad.x -= (currentX - bounds.maxX) * boundaryWeight;
|
|
3516
|
+
if (currentY < bounds.minY)
|
|
3517
|
+
grad.y += (bounds.minY - currentY) * boundaryWeight;
|
|
3518
|
+
if (currentY > bounds.maxY)
|
|
3519
|
+
grad.y -= (currentY - bounds.maxY) * boundaryWeight;
|
|
3520
|
+
});
|
|
3521
|
+
nodes.forEach(node => {
|
|
3522
|
+
const grad = gradient.get(node.id);
|
|
3523
|
+
if (!grad)
|
|
3524
|
+
return;
|
|
3525
|
+
node.x = (node.x ?? node.targetX ?? zoneShared) + grad.x * step;
|
|
3526
|
+
node.y = (node.y ?? node.targetY ?? minCenterY) + grad.y * step;
|
|
3527
|
+
clampNodeToBounds(node);
|
|
3528
|
+
});
|
|
3529
|
+
}
|
|
3530
|
+
};
|
|
3531
|
+
runObjectiveOptimizer();
|
|
3023
3532
|
this.ngZone.runOutsideAngular(() => {
|
|
3024
3533
|
const sim = forceSimulation(nodes)
|
|
3025
3534
|
.alphaDecay(0.014)
|
|
@@ -3038,7 +3547,11 @@ class ProfileComparisonLibComponent {
|
|
|
3038
3547
|
.strength(d => d.group === 'shared' ? 1.15 : 0.85))
|
|
3039
3548
|
.force('yTargetSeed', forceY(d => d.targetY ?? minCenterY).strength(0.1))
|
|
3040
3549
|
.force('crossAlign', crossAlignForce)
|
|
3041
|
-
.force('
|
|
3550
|
+
.force('relativeMedianColumn', relativeMedianColumnForce)
|
|
3551
|
+
.force('similarityAttraction', similarityAttractionForce)
|
|
3552
|
+
.force('dissimilarRepulsion', dissimilarRepulsionForce)
|
|
3553
|
+
.force('priorityPair', priorityPairForce)
|
|
3554
|
+
.force('mutualTopPair', mutualTopPairForce);
|
|
3042
3555
|
for (let i = 0; i < 420 && sim.alpha() > 0.0035; i++) {
|
|
3043
3556
|
sim.tick();
|
|
3044
3557
|
nodes.forEach(n => {
|
|
@@ -3054,8 +3567,9 @@ class ProfileComparisonLibComponent {
|
|
|
3054
3567
|
sim.force('yTargetSeed', null);
|
|
3055
3568
|
sim.force('crossAlign', null);
|
|
3056
3569
|
sim.force('xTarget', forceX(d => d.originalFx ?? d.x ?? 0)
|
|
3057
|
-
.strength(d => d.group === 'shared' ? 0.
|
|
3058
|
-
sim.force('yTarget', forceY(d => d.originalFy ?? d.y ?? 0).strength(0.
|
|
3570
|
+
.strength(d => d.group === 'shared' ? 0.085 : 0.03));
|
|
3571
|
+
sim.force('yTarget', forceY(d => d.originalFy ?? d.y ?? 0).strength(0.03));
|
|
3572
|
+
sim.alphaTarget(ProfileComparisonLibComponent.IDLE_ALPHA_TARGET);
|
|
3059
3573
|
this.simulation = sim;
|
|
3060
3574
|
this.ngZone.run(() => {
|
|
3061
3575
|
this.forceNodes = [...nodes];
|
|
@@ -3493,10 +4007,14 @@ class ProfileComparisonLibComponent {
|
|
|
3493
4007
|
return;
|
|
3494
4008
|
this.simulation.force('attraction', null);
|
|
3495
4009
|
if (anchorNode) {
|
|
3496
|
-
|
|
4010
|
+
const minClickAlpha = ProfileComparisonLibComponent.CLICK_REHEAT_ALPHA;
|
|
4011
|
+
if (this.simulation.alpha() < minClickAlpha) {
|
|
4012
|
+
this.simulation.alpha(minClickAlpha);
|
|
4013
|
+
}
|
|
4014
|
+
this.simulation.alphaTarget(ProfileComparisonLibComponent.IDLE_ALPHA_TARGET).restart();
|
|
3497
4015
|
}
|
|
3498
4016
|
else if (!this.isDragging) {
|
|
3499
|
-
this.simulation.alphaTarget(
|
|
4017
|
+
this.simulation.alphaTarget(ProfileComparisonLibComponent.IDLE_ALPHA_TARGET);
|
|
3500
4018
|
}
|
|
3501
4019
|
}
|
|
3502
4020
|
isNodeConnected(node) {
|
|
@@ -3595,10 +4113,10 @@ class ProfileComparisonLibComponent {
|
|
|
3595
4113
|
collideForce.strength(1.0);
|
|
3596
4114
|
const xTarget = this.simulation.force('xTarget');
|
|
3597
4115
|
if (xTarget)
|
|
3598
|
-
xTarget.strength((d) => d.group === 'shared' ? 0.
|
|
4116
|
+
xTarget.strength((d) => d.group === 'shared' ? 0.1 : 0.045);
|
|
3599
4117
|
const yTarget = this.simulation.force('yTarget');
|
|
3600
4118
|
if (yTarget)
|
|
3601
|
-
yTarget.strength(0.
|
|
4119
|
+
yTarget.strength(0.042);
|
|
3602
4120
|
}
|
|
3603
4121
|
// Pull connected chips toward the dragged node
|
|
3604
4122
|
this.updateAttractionForce(node);
|
|
@@ -3657,21 +4175,21 @@ class ProfileComparisonLibComponent {
|
|
|
3657
4175
|
collideForce.strength(1.0);
|
|
3658
4176
|
const xTarget = this.simulation.force('xTarget');
|
|
3659
4177
|
if (xTarget)
|
|
3660
|
-
xTarget.strength((d) => d.group === 'shared' ? 0.
|
|
4178
|
+
xTarget.strength((d) => d.group === 'shared' ? 0.1 : 0.045);
|
|
3661
4179
|
const yTarget = this.simulation.force('yTarget');
|
|
3662
4180
|
if (yTarget)
|
|
3663
|
-
yTarget.strength(0.
|
|
4181
|
+
yTarget.strength(0.042);
|
|
3664
4182
|
}
|
|
3665
4183
|
// Cool down simulation targets
|
|
3666
|
-
this.simulation.alphaTarget(
|
|
3667
|
-
this.simulation.alpha(0.
|
|
4184
|
+
this.simulation.alphaTarget(ProfileComparisonLibComponent.IDLE_ALPHA_TARGET);
|
|
4185
|
+
this.simulation.alpha(0.2).restart();
|
|
3668
4186
|
this.draggedNode = null;
|
|
3669
4187
|
this.isDragging = false;
|
|
3670
4188
|
}
|
|
3671
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.
|
|
3672
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.24", type: ProfileComparisonLibComponent, isStandalone: false, selector: "lib-profile-comparison", inputs: { config: "config", user1File: "user1File", user2File: "user2File", backendMode: "backendMode", backendUrl: "backendUrl", fadeAllEdges: "fadeAllEdges", person1Name: "person1Name", person2Name: "person2Name", imageProxyPath: "imageProxyPath" }, outputs: { matrixDataChange: "matrixDataChange", rawLLMOutputChange: "rawLLMOutputChange", viewProfileClick: "viewProfileClick" }, host: { listeners: { "window:resize": "onWindowResize()", "document:click": "onDocumentClick()", "document:mousemove": "onDocumentDragMove($event)", "document:touchmove": "onDocumentDragMove($event)", "document:mouseup": "onDocumentDragEnd()", "document:touchend": "onDocumentDragEnd()" } }, viewQueries: [{ propertyName: "leftContainer", first: true, predicate: ["leftContainer"], descendants: true }, { propertyName: "rightContainer", first: true, predicate: ["rightContainer"], descendants: true }, { propertyName: "profileFlex", first: true, predicate: ["profileFlex"], descendants: true }, { propertyName: "profileImgLeft", first: true, predicate: ["profileImgLeft"], descendants: true }, { propertyName: "profileImgRight", first: true, predicate: ["profileImgRight"], descendants: true }, { propertyName: "shapeContainer", first: true, predicate: ["shapeContainer"], descendants: true }, { propertyName: "shapeBg", first: true, predicate: ["shapeBg"], descendants: true }, { propertyName: "shapeBg1", first: true, predicate: ["shapeBg1"], descendants: true }, { propertyName: "shapeBg2", first: true, predicate: ["shapeBg2"], descendants: true }, { propertyName: "shapeTextLeft", first: true, predicate: ["shapeTextLeft"], descendants: true }, { propertyName: "shapeTextRight", first: true, predicate: ["shapeTextRight"], descendants: true }, { propertyName: "shapeTextCenter", first: true, predicate: ["shapeTextCenter"], descendants: true }, { propertyName: "chipsGrid", first: true, predicate: ["chipsGrid"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "<div class=\"configure-backend-message\" *ngIf=\"!backendConfigured\">\r\n Configure backend\r\n</div>\r\n\r\n<div class=\"profile-screen\" *ngIf=\"backendConfigured\">\r\n <div #profileFlex class=\"profile-flex\" [class.fade-all]=\"fadeAllEdges\">\r\n <svg class=\"image-noise-overlay\" style=\"position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; overflow: hidden; z-index: 0;\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 320 320\" fill=\"none\" preserveAspectRatio=\"none\">\r\n <g filter=\"url(#filter0_n_1_1753)\">\r\n <rect width=\"100%\" height=\"100%\" fill=\"url(#paint0_radial_1_1753)\" fill-opacity=\"0.33\"/>\r\n </g>\r\n <defs>\r\n <filter id=\"filter0_n_1_1753\" x=\"0%\" y=\"0%\" width=\"100%\" height=\"100%\" filterUnits=\"objectBoundingBox\" color-interpolation-filters=\"sRGB\">\r\n <feFlood flood-opacity=\"0\" result=\"BackgroundImageFix\"/>\r\n <feBlend mode=\"normal\" in=\"SourceGraphic\" in2=\"BackgroundImageFix\" result=\"shape\"/>\r\n <feTurbulence type=\"fractalNoise\" baseFrequency=\"0.10869565606117249 0.10869565606117249\" stitchTiles=\"stitch\" numOctaves=\"3\" result=\"noise\" seed=\"7339\"/>\r\n <feColorMatrix in=\"noise\" type=\"luminanceToAlpha\" result=\"alphaNoise\"/>\r\n <feComponentTransfer in=\"alphaNoise\" result=\"coloredNoise1\">\r\n <feFuncA type=\"discrete\" tableValues=\"0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 \"/>\r\n </feComponentTransfer>\r\n <feComposite operator=\"in\" in2=\"shape\" in=\"coloredNoise1\" result=\"noise1Clipped\"/>\r\n <feFlood flood-color=\"rgba(0, 0, 0, 0.25)\" result=\"color1Flood\"/>\r\n <feComposite operator=\"in\" in2=\"noise1Clipped\" in=\"color1Flood\" result=\"color1\"/>\r\n <feMerge result=\"effect1_noise_1_1753\">\r\n <feMergeNode in=\"shape\"/>\r\n <feMergeNode in=\"color1\"/>\r\n </feMerge>\r\n </filter>\r\n <radialGradient id=\"paint0_radial_1_1753\" cx=\"50%\" cy=\"50%\" r=\"50%\" fx=\"50%\" fy=\"50%\">\r\n <stop offset=\"0%\" stop-opacity=\"0\"/>\r\n <stop offset=\"100%\" stop-opacity=\"0.25\"/>\r\n </radialGradient>\r\n </defs>\r\n </svg>\r\n <div #profileImgLeft class=\"profile-img left\" [class.fade-all]=\"fadeAllEdges\">\r\n <img\r\n [src]=\"user1Image\"\r\n alt=\"User 1\"\r\n [style.transform]=\"user1Transform\"\r\n [style.top]=\"user1Top\"\r\n [style.height]=\"user1Height\"\r\n [style.object-position]=\"user1ObjectPosition\"\r\n (load)=\"onUserImageLoad(1, $event)\"\r\n />\r\n </div>\r\n <div #profileImgRight class=\"profile-img right\" [class.fade-all]=\"fadeAllEdges\">\r\n <img\r\n [src]=\"user2Image\"\r\n alt=\"User 2\"\r\n [style.transform]=\"user2Transform\"\r\n [style.top]=\"user2Top\"\r\n [style.height]=\"user2Height\"\r\n [style.object-position]=\"user2ObjectPosition\"\r\n (load)=\"onUserImageLoad(2, $event)\"\r\n />\r\n </div>\r\n <div class=\"image-shade-overlay\"></div>\r\n\r\n <!-- Frosted Glass Overlay Layer -->\r\n <div class=\"overlay-container\">\r\n <!-- Top Headers \u2014 absolutely positioned overlays, exactly as original -->\r\n <div class=\"header-label left\">{{ person1Name }}</div>\r\n <div class=\"header-label center\">Shared</div>\r\n <div class=\"header-label right\">{{ person2Name }}</div>\r\n\r\n <!-- Force-directed chip canvas (scrollable) -->\r\n <div class=\"chips-grid\" #chipsGrid>\r\n <div class=\"force-canvas\" [style.height.px]=\"forceCanvasHeight\">\r\n <!-- SVG Connections Layer -->\r\n <svg class=\"connections-svg\" [style.height.px]=\"forceCanvasHeight\">\r\n <line\r\n *ngFor=\"let link of sortedForceLinks\"\r\n [attr.x1]=\"getLinkSourceX(link)\"\r\n [attr.y1]=\"getLinkSourceY(link)\"\r\n [attr.x2]=\"getLinkTargetX(link)\"\r\n [attr.y2]=\"getLinkTargetY(link)\"\r\n class=\"connection-line\"\r\n [class.active]=\"isLinkConnectedToActive(link)\"\r\n [class.cross-group]=\"isCrossGroupLink(link)\"\r\n ></line>\r\n </svg>\r\n\r\n <div\r\n *ngFor=\"let node of forceNodes\"\r\n class=\"frosted-chip\"\r\n [class.person-a]=\"node.group === 'p1'\"\r\n [class.shared]=\"node.group === 'shared'\"\r\n [class.person-b]=\"node.group === 'p2'\"\r\n [class.active]=\"activeNode === node\"\r\n [class.dimmed]=\"activeNode && !isNodeConnected(node)\"\r\n [style.left.px]=\"node.x\"\r\n [style.top.px]=\"node.y\"\r\n [style.padding]=\"chipPaddingYPx + 'px ' + chipPaddingXPx + 'px'\"\r\n (mouseenter)=\"onNodeMouseEnter(node)\"\r\n (mouseleave)=\"onNodeMouseLeave(node)\"\r\n (click)=\"onNodeClick(node, $event)\"\r\n (mousedown)=\"onNodeDragStart(node, $event)\"\r\n (touchstart)=\"onNodeDragStart(node, $event)\"\r\n >\r\n <span\r\n class=\"chip-label\"\r\n [style.font-size.px]=\"chipFontSizePx\"\r\n [style.line-height]=\"chipLineHeight\"\r\n >{{ node.label }}</span>\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <!-- Bottom Actions -->\r\n <div class=\"view-profile left\" (click)=\"onViewProfile('left')\">View Profile</div>\r\n <div class=\"view-profile right\" (click)=\"onViewProfile('right')\">View Profile</div>\r\n </div>\r\n\r\n <!-- Loading indicator for alignment process -->\r\n <div *ngIf=\"isAligning\" class=\"loading-indicator\">\r\n <div class=\"loading-spinner\"></div>\r\n </div>\r\n</div>\r\n\r\n\r\n", styles: ["@charset \"UTF-8\";:host{display:block;width:100%;max-width:100%;overflow-x:hidden!important;box-sizing:border-box!important}.profile-img.left{position:absolute;top:0;bottom:0;left:0;width:58%;height:100%;margin:0;z-index:1;-webkit-mask-image:linear-gradient(to right,black 0%,black 45%,transparent 92%);mask-image:linear-gradient(to right,black 0%,black 45%,transparent 92%)}.profile-img.right{position:absolute;inset:0 0 0 auto;width:58%;height:100%;margin:0;z-index:2;-webkit-mask-image:linear-gradient(to right,transparent 8%,black 55%,black 100%);mask-image:linear-gradient(to right,transparent 8%,black 55%,black 100%)}@media(min-width:768px){.profile-img.left{width:65%;-webkit-mask-image:linear-gradient(to right,black 0%,black 40%,transparent 90%);mask-image:linear-gradient(to right,black 0%,black 40%,transparent 90%)}.profile-img.right{width:65%;-webkit-mask-image:linear-gradient(to right,transparent 10%,black 60%,black 100%);mask-image:linear-gradient(to right,transparent 10%,black 60%,black 100%)}}.configure-backend-message{padding:1rem;text-align:center;color:#bdc3c7;background:#34495e;border-radius:8px}.backend-error-message{padding:.75rem 1rem;text-align:center;color:#fef3c7;background:#b4530940;border:1px solid rgba(245,158,11,.5);border-radius:8px;margin-bottom:.5rem}.profile-screen{width:100%;max-width:768px;margin:0 auto;overflow:hidden;position:relative;border-radius:12px}.profile-flex{display:flex;width:100%;height:440px;overflow:hidden;background:transparent;gap:0;margin:0;padding:0;position:relative;box-shadow:none;border:none;--overlap-size: 40px}.profile-flex.fade-all{-webkit-mask-image:linear-gradient(to right,transparent 0%,black 18%,black 82%,transparent 100%),linear-gradient(to bottom,transparent 0%,black 15%,black 85%,transparent 100%);-webkit-mask-composite:intersect;mask-image:linear-gradient(to right,transparent 0%,black 18%,black 82%,transparent 100%),linear-gradient(to bottom,transparent 0%,black 15%,black 85%,transparent 100%);mask-composite:intersect}.profile-flex .image-shade-overlay{position:absolute;inset:0;width:100%;height:100%;pointer-events:none;z-index:3;background:radial-gradient(ellipse at center,transparent 40%,rgba(41,37,45,.25) 100%)}.profile-flex .image-noise-overlay{position:absolute;inset:0;width:100%;height:100%;max-width:100%;max-height:100%;pointer-events:none;overflow:hidden;z-index:0}.shape-bg1,.shape-bg2{position:absolute;max-width:250px;opacity:1;transition:transform .2s ease-out}.shape-bg1{z-index:1;left:0}.shape-bg2{right:0}.shape-bg.fade-all .shape-bg1{-webkit-mask-image:linear-gradient(to right,transparent 0%,black 20%,black 100%);mask-image:linear-gradient(to right,transparent 0%,black 20%,black 100%)}.shape-bg.fade-all .shape-bg2{-webkit-mask-image:linear-gradient(to left,transparent 0%,black 20%,black 100%);mask-image:linear-gradient(to left,transparent 0%,black 20%,black 100%)}.shape-bg svg{cursor:grab}.shape-bg svg:active{cursor:grabbing}.profile-img{height:100%;position:relative;flex-shrink:0;background:transparent;margin:0;padding:0;left:0;top:0;border:none;box-sizing:border-box;overflow:hidden;box-shadow:none;outline:none}.profile-img img{transition:transform .3s ease;width:100%;height:100%;object-fit:cover;display:block;object-position:center center;opacity:1;transform-origin:center center;border:none;outline:none;box-shadow:none;position:relative;top:0;left:0;filter:contrast(1.2) brightness(1.1) saturate(1.1);image-rendering:auto}.overlay-container{position:absolute;inset:0;width:100%;height:100%;pointer-events:none;z-index:10;-webkit-user-select:none;user-select:none;overflow:hidden;background:#29252da6;backdrop-filter:blur(.5px);-webkit-backdrop-filter:blur(.5px);--name-label-font-size: 14px}.header-label{position:absolute;top:20px;font-family:Gilroy,sans-serif;text-shadow:0 1px 8px rgba(0,0,0,.8);pointer-events:none}.header-label.left{left:25%;transform:translate(-50%);font-size:var(--name-label-font-size);font-weight:400;color:#fffffff2;text-align:center}.header-label.center{left:50%;transform:translate(-50%);font-size:12px;font-weight:400;color:#fffc;letter-spacing:0}.header-label.right{left:75%;transform:translate(-50%);font-size:var(--name-label-font-size);font-weight:400;color:#fffffff2;text-align:center}.chips-grid{position:absolute;inset:45px 0;overflow-y:auto;overscroll-behavior:none;scrollbar-width:none;box-sizing:border-box}.chips-grid::-webkit-scrollbar{display:none}.force-canvas{position:relative;width:100%;box-sizing:border-box;min-height:120px}.connections-svg{position:absolute;top:0;left:0;width:100%;pointer-events:none;z-index:1}@keyframes lineGlow{0%{opacity:0;stroke-dashoffset:8}30%{opacity:.15}to{opacity:.6;stroke-dashoffset:0}}.connection-line{stroke:#fe3c72d1;stroke-width:1.5px;stroke-dasharray:4;opacity:0;pointer-events:none;transition:opacity .3s ease}.connection-line.active{animation:lineGlow .7s cubic-bezier(.4,0,.2,1) forwards}.frosted-chip{position:absolute;cursor:pointer;pointer-events:auto;transition:transform .2s ease,box-shadow .2s ease,background .2s ease,opacity .3s ease;flex-shrink:0;padding:6px 18px;border-radius:999px;display:inline-flex;align-items:center;justify-content:center;transform:translate(-50%,-50%);z-index:2;border:1px solid rgba(255,255,255,.24);background:#ffffff30;box-shadow:inset 0 1.5px 2px #ffffff59,0 0 0 1px #0f172a38;backdrop-filter:blur(.5px);-webkit-backdrop-filter:blur(.5px)}.frosted-chip:hover{transform:translate(-50%,-50%) scale(1.04);background:#ffffff29;cursor:grab}.frosted-chip.active{background:#ffffff4d;box-shadow:0 0 15px #fe3c7261,inset 0 1.5px 2px #ffffff80,0 0 0 1px #ffffff52;transform:translate(-50%,-50%) scale(1.06);z-index:10;cursor:grabbing}.frosted-chip.active .chip-label{color:#fff!important}.frosted-chip.dimmed{opacity:.35}.frosted-chip .chip-label{position:relative;color:#a8a8a8;text-align:center;font-family:Gilroy,sans-serif;font-size:14px;font-style:normal;font-weight:400;line-height:140%;letter-spacing:-.01em;text-shadow:none;white-space:nowrap;pointer-events:none;z-index:1}.ghost-chip{visibility:hidden!important;pointer-events:none!important}.view-profile{position:absolute;bottom:8px;color:#ffffff80;text-shadow:0 2px 4.2px #000;font-family:Calistoga,serif;font-size:var(--name-label-font-size);font-style:normal;font-weight:400;line-height:normal;cursor:pointer;pointer-events:auto;transition:opacity .2s ease,transform .2s ease}.view-profile:hover{opacity:.85}.view-profile.left{left:8%;width:34%;text-align:left;transform:none}.view-profile.left:hover{transform:translateY(-2px)}.view-profile.right{right:8%;width:34%;text-align:right;left:auto;transform:none}.view-profile.right:hover{transform:translateY(-2px)}#drag-preview{position:absolute;pointer-events:none;display:none;background:#000000b3;color:#fff;padding:5px 10px;border-radius:5px;z-index:9999;max-width:200px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.shape-text-container{width:70px;position:relative;overflow:hidden}.draggable{width:70px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;cursor:grab;-webkit-user-select:none;user-select:none;position:relative;display:inline-block}.dragging{width:auto;text-overflow:clip;cursor:grabbing;position:absolute;z-index:1000}.p-wrapper{width:70px;overflow-x:hidden}.shape-text-center{display:flex;justify-content:space-between;flex-direction:column;gap:10px;height:100%;margin-top:55px}.loading-indicator{position:absolute;inset:0;width:100%;height:100%;background:#00000040;backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px);color:#fff;z-index:100;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12px;pointer-events:auto}.loading-spinner{width:44px;height:44px;border:3.5px solid rgba(255,255,255,.25);border-top:3.5px solid #ffffff;border-radius:50%;animation:spin .8s linear infinite;box-shadow:0 0 15px #fff3}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.loading-indicator p{margin:0;font-size:14px;font-weight:500;color:#ffffffd9;letter-spacing:.02em}.api-key-modal-overlay{position:fixed;inset:0;background:#0f172a99;-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px);display:flex;align-items:center;justify-content:center;z-index:2000}.api-key-modal{width:min(640px,92vw);background:#0f172a;color:#e2e8f0;border:1px solid #334155;border-radius:12px;box-shadow:0 20px 50px #00000073;padding:16px 18px}.api-key-modal-header{display:flex;align-items:center;justify-content:space-between;gap:12px}.api-key-modal-header h2{margin:0;font-size:20px;font-weight:700;color:#e2e8f0}.warning-icon{color:#fbbf24}.api-key-modal .close-btn{background:#0b1220;border:1px solid #334155;color:#94a3b8;border-radius:8px;padding:6px;cursor:pointer;line-height:0}.api-key-modal-body{margin-top:12px;display:flex;flex-direction:column;gap:10px}.modal-message,.modal-instruction{margin:0;color:#cbd5e1}.modal-message strong{color:#e5e7eb}.api-key-input-group{display:flex;align-items:center;gap:10px}.api-key-input{flex:1 1 auto;background:#0a1020;color:#e2e8f0;border:1px solid #334155;border-radius:8px;padding:10px 12px}.api-key-input:focus{outline:none;border-color:#2563eb;box-shadow:0 0 0 3px #2563eb33}.api-key-load-btn{background:#2563eb;color:#fff;border:none;border-radius:8px;padding:10px 12px;cursor:pointer;font-weight:600}.api-key-load-btn:hover{background:#1d4ed8}.modal-hint{display:flex;align-items:center;gap:8px;color:#94a3b8}.modal-hint a{color:#93c5fd}@media(max-width:1023px){.profile-screen{max-width:640px}.profile-flex{height:400px}.profile-img{height:550px}}@media(max-width:767px){.profile-screen,.profile-flex,.overlay-container{width:100%;max-width:100%;overflow-x:hidden;box-sizing:border-box}.profile-screen{border-radius:0}.profile-flex{height:360px}.profile-img{height:510px}.chips-grid{width:100%;max-width:100%;padding:0;top:45px;bottom:50px;box-sizing:border-box}.frosted-chip{padding:4px 10px;max-width:100%;box-sizing:border-box}.frosted-chip .chip-label{font-size:11px;max-width:100%;overflow:hidden;text-overflow:ellipsis}.view-profile{bottom:10px}}@media(max-width:480px){.overlay-container{--name-label-font-size: 12px}.profile-flex{height:340px}.profile-img{height:480px}.frosted-chip{padding:3px 8px}.frosted-chip .chip-label,.header-label.center{font-size:10px}}\n"], dependencies: [{ kind: "directive", type: i2_1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i2_1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] });
|
|
4189
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.26", ngImport: i0, type: ProfileComparisonLibComponent, deps: [{ token: ProfileComparisonBackendService }, { token: i0.Renderer2 }, { token: FileConversionService }, { token: ImageCompressionService }, { token: i0.ChangeDetectorRef }, { token: i0.NgZone }, { token: i0.ElementRef }, { token: PROFILE_COMPARISON_VERBOSE_LOGGING, optional: true }], target: i0.ɵɵFactoryTarget.Component });
|
|
4190
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.26", type: ProfileComparisonLibComponent, isStandalone: false, selector: "lib-profile-comparison", inputs: { config: "config", user1File: "user1File", user2File: "user2File", backendMode: "backendMode", backendUrl: "backendUrl", fadeAllEdges: "fadeAllEdges", person1Name: "person1Name", person2Name: "person2Name", imageProxyPath: "imageProxyPath" }, outputs: { matrixDataChange: "matrixDataChange", rawLLMOutputChange: "rawLLMOutputChange", viewProfileClick: "viewProfileClick" }, host: { listeners: { "window:resize": "onWindowResize()", "document:click": "onDocumentClick()", "document:mousemove": "onDocumentDragMove($event)", "document:touchmove": "onDocumentDragMove($event)", "document:mouseup": "onDocumentDragEnd()", "document:touchend": "onDocumentDragEnd()" } }, viewQueries: [{ propertyName: "leftContainer", first: true, predicate: ["leftContainer"], descendants: true }, { propertyName: "rightContainer", first: true, predicate: ["rightContainer"], descendants: true }, { propertyName: "profileFlex", first: true, predicate: ["profileFlex"], descendants: true }, { propertyName: "profileImgLeft", first: true, predicate: ["profileImgLeft"], descendants: true }, { propertyName: "profileImgRight", first: true, predicate: ["profileImgRight"], descendants: true }, { propertyName: "shapeContainer", first: true, predicate: ["shapeContainer"], descendants: true }, { propertyName: "shapeBg", first: true, predicate: ["shapeBg"], descendants: true }, { propertyName: "shapeBg1", first: true, predicate: ["shapeBg1"], descendants: true }, { propertyName: "shapeBg2", first: true, predicate: ["shapeBg2"], descendants: true }, { propertyName: "shapeTextLeft", first: true, predicate: ["shapeTextLeft"], descendants: true }, { propertyName: "shapeTextRight", first: true, predicate: ["shapeTextRight"], descendants: true }, { propertyName: "shapeTextCenter", first: true, predicate: ["shapeTextCenter"], descendants: true }, { propertyName: "chipsGrid", first: true, predicate: ["chipsGrid"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "<div class=\"configure-backend-message\" *ngIf=\"!backendConfigured\">\r\n Configure backend\r\n</div>\r\n\r\n<div class=\"profile-screen\" *ngIf=\"backendConfigured\">\r\n <div #profileFlex class=\"profile-flex\" [class.fade-all]=\"fadeAllEdges\">\r\n <svg class=\"image-noise-overlay\" style=\"position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; overflow: hidden; z-index: 0;\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 320 320\" fill=\"none\" preserveAspectRatio=\"none\">\r\n <g filter=\"url(#filter0_n_1_1753)\">\r\n <rect width=\"100%\" height=\"100%\" fill=\"url(#paint0_radial_1_1753)\" fill-opacity=\"0.33\"/>\r\n </g>\r\n <defs>\r\n <filter id=\"filter0_n_1_1753\" x=\"0%\" y=\"0%\" width=\"100%\" height=\"100%\" filterUnits=\"objectBoundingBox\" color-interpolation-filters=\"sRGB\">\r\n <feFlood flood-opacity=\"0\" result=\"BackgroundImageFix\"/>\r\n <feBlend mode=\"normal\" in=\"SourceGraphic\" in2=\"BackgroundImageFix\" result=\"shape\"/>\r\n <feTurbulence type=\"fractalNoise\" baseFrequency=\"0.10869565606117249 0.10869565606117249\" stitchTiles=\"stitch\" numOctaves=\"3\" result=\"noise\" seed=\"7339\"/>\r\n <feColorMatrix in=\"noise\" type=\"luminanceToAlpha\" result=\"alphaNoise\"/>\r\n <feComponentTransfer in=\"alphaNoise\" result=\"coloredNoise1\">\r\n <feFuncA type=\"discrete\" tableValues=\"0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 \"/>\r\n </feComponentTransfer>\r\n <feComposite operator=\"in\" in2=\"shape\" in=\"coloredNoise1\" result=\"noise1Clipped\"/>\r\n <feFlood flood-color=\"rgba(0, 0, 0, 0.25)\" result=\"color1Flood\"/>\r\n <feComposite operator=\"in\" in2=\"noise1Clipped\" in=\"color1Flood\" result=\"color1\"/>\r\n <feMerge result=\"effect1_noise_1_1753\">\r\n <feMergeNode in=\"shape\"/>\r\n <feMergeNode in=\"color1\"/>\r\n </feMerge>\r\n </filter>\r\n <radialGradient id=\"paint0_radial_1_1753\" cx=\"50%\" cy=\"50%\" r=\"50%\" fx=\"50%\" fy=\"50%\">\r\n <stop offset=\"0%\" stop-opacity=\"0\"/>\r\n <stop offset=\"100%\" stop-opacity=\"0.25\"/>\r\n </radialGradient>\r\n </defs>\r\n </svg>\r\n <div #profileImgLeft class=\"profile-img left\" [class.fade-all]=\"fadeAllEdges\">\r\n <img\r\n [src]=\"user1Image\"\r\n alt=\"User 1\"\r\n [style.transform]=\"user1Transform\"\r\n [style.top]=\"user1Top\"\r\n [style.height]=\"user1Height\"\r\n [style.object-position]=\"user1ObjectPosition\"\r\n (load)=\"onUserImageLoad(1, $event)\"\r\n />\r\n </div>\r\n <div #profileImgRight class=\"profile-img right\" [class.fade-all]=\"fadeAllEdges\">\r\n <img\r\n [src]=\"user2Image\"\r\n alt=\"User 2\"\r\n [style.transform]=\"user2Transform\"\r\n [style.top]=\"user2Top\"\r\n [style.height]=\"user2Height\"\r\n [style.object-position]=\"user2ObjectPosition\"\r\n (load)=\"onUserImageLoad(2, $event)\"\r\n />\r\n </div>\r\n <div class=\"image-shade-overlay\"></div>\r\n\r\n <!-- Frosted Glass Overlay Layer -->\r\n <div class=\"overlay-container\">\r\n <!-- Top Headers \u2014 absolutely positioned overlays, exactly as original -->\r\n <div class=\"header-label left\">{{ person1Name }}</div>\r\n <div class=\"header-label center\">Shared</div>\r\n <div class=\"header-label right\">{{ person2Name }}</div>\r\n\r\n <!-- Force-directed chip canvas (scrollable) -->\r\n <div class=\"chips-grid\" #chipsGrid>\r\n <div class=\"force-canvas\" [style.height.px]=\"forceCanvasHeight\">\r\n <!-- SVG Connections Layer -->\r\n <svg class=\"connections-svg\" [style.height.px]=\"forceCanvasHeight\">\r\n <line\r\n *ngFor=\"let link of sortedForceLinks\"\r\n [attr.x1]=\"getLinkSourceX(link)\"\r\n [attr.y1]=\"getLinkSourceY(link)\"\r\n [attr.x2]=\"getLinkTargetX(link)\"\r\n [attr.y2]=\"getLinkTargetY(link)\"\r\n class=\"connection-line\"\r\n [class.active]=\"isLinkConnectedToActive(link)\"\r\n [class.cross-group]=\"isCrossGroupLink(link)\"\r\n ></line>\r\n </svg>\r\n\r\n <div\r\n *ngFor=\"let node of forceNodes\"\r\n class=\"frosted-chip\"\r\n [class.person-a]=\"node.group === 'p1'\"\r\n [class.shared]=\"node.group === 'shared'\"\r\n [class.person-b]=\"node.group === 'p2'\"\r\n [class.active]=\"activeNode === node\"\r\n [class.dimmed]=\"activeNode && !isNodeConnected(node)\"\r\n [style.left.px]=\"node.x\"\r\n [style.top.px]=\"node.y\"\r\n [style.padding]=\"chipPaddingYPx + 'px ' + chipPaddingXPx + 'px'\"\r\n (mouseenter)=\"onNodeMouseEnter(node)\"\r\n (mouseleave)=\"onNodeMouseLeave(node)\"\r\n (click)=\"onNodeClick(node, $event)\"\r\n (mousedown)=\"onNodeDragStart(node, $event)\"\r\n (touchstart)=\"onNodeDragStart(node, $event)\"\r\n >\r\n <span\r\n class=\"chip-label\"\r\n [style.font-size.px]=\"chipFontSizePx\"\r\n [style.line-height]=\"chipLineHeight\"\r\n >{{ node.label }}</span>\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <!-- Bottom Actions -->\r\n <div class=\"view-profile left\" (click)=\"onViewProfile('left')\">View Profile</div>\r\n <div class=\"view-profile right\" (click)=\"onViewProfile('right')\">View Profile</div>\r\n </div>\r\n\r\n <!-- Loading indicator for alignment process -->\r\n <div *ngIf=\"isAligning\" class=\"loading-indicator\">\r\n <div class=\"loading-spinner\"></div>\r\n </div>\r\n</div>\r\n\r\n\r\n", styles: ["@charset \"UTF-8\";:host{display:block;width:100%;max-width:100%;overflow-x:hidden!important;box-sizing:border-box!important}.profile-img.left{position:absolute;top:0;bottom:0;left:0;width:58%;height:100%;margin:0;z-index:1;-webkit-mask-image:linear-gradient(to right,black 0%,black 45%,transparent 92%);mask-image:linear-gradient(to right,black 0%,black 45%,transparent 92%)}.profile-img.right{position:absolute;inset:0 0 0 auto;width:58%;height:100%;margin:0;z-index:2;-webkit-mask-image:linear-gradient(to right,transparent 8%,black 55%,black 100%);mask-image:linear-gradient(to right,transparent 8%,black 55%,black 100%)}@media(min-width:768px){.profile-img.left{width:65%;-webkit-mask-image:linear-gradient(to right,black 0%,black 40%,transparent 90%);mask-image:linear-gradient(to right,black 0%,black 40%,transparent 90%)}.profile-img.right{width:65%;-webkit-mask-image:linear-gradient(to right,transparent 10%,black 60%,black 100%);mask-image:linear-gradient(to right,transparent 10%,black 60%,black 100%)}}.configure-backend-message{padding:1rem;text-align:center;color:#bdc3c7;background:#34495e;border-radius:8px}.backend-error-message{padding:.75rem 1rem;text-align:center;color:#fef3c7;background:#b4530940;border:1px solid rgba(245,158,11,.5);border-radius:8px;margin-bottom:.5rem}.profile-screen{width:100%;max-width:768px;margin:0 auto;overflow:hidden;position:relative;border-radius:12px}.profile-flex{display:flex;width:100%;height:440px;overflow:hidden;background:transparent;gap:0;margin:0;padding:0;position:relative;box-shadow:none;border:none;--overlap-size: 40px}.profile-flex.fade-all{-webkit-mask-image:linear-gradient(to right,transparent 0%,black 18%,black 82%,transparent 100%),linear-gradient(to bottom,transparent 0%,black 15%,black 85%,transparent 100%);-webkit-mask-composite:intersect;mask-image:linear-gradient(to right,transparent 0%,black 18%,black 82%,transparent 100%),linear-gradient(to bottom,transparent 0%,black 15%,black 85%,transparent 100%);mask-composite:intersect}.profile-flex .image-shade-overlay{position:absolute;inset:0;width:100%;height:100%;pointer-events:none;z-index:3;background:radial-gradient(ellipse at center,transparent 40%,rgba(41,37,45,.25) 100%)}.profile-flex .image-noise-overlay{position:absolute;inset:0;width:100%;height:100%;max-width:100%;max-height:100%;pointer-events:none;overflow:hidden;z-index:0}.shape-bg1,.shape-bg2{position:absolute;max-width:250px;opacity:1;transition:transform .2s ease-out}.shape-bg1{z-index:1;left:0}.shape-bg2{right:0}.shape-bg.fade-all .shape-bg1{-webkit-mask-image:linear-gradient(to right,transparent 0%,black 20%,black 100%);mask-image:linear-gradient(to right,transparent 0%,black 20%,black 100%)}.shape-bg.fade-all .shape-bg2{-webkit-mask-image:linear-gradient(to left,transparent 0%,black 20%,black 100%);mask-image:linear-gradient(to left,transparent 0%,black 20%,black 100%)}.shape-bg svg{cursor:grab}.shape-bg svg:active{cursor:grabbing}.profile-img{height:100%;position:relative;flex-shrink:0;background:transparent;margin:0;padding:0;left:0;top:0;border:none;box-sizing:border-box;overflow:hidden;box-shadow:none;outline:none}.profile-img img{transition:transform .3s ease;width:100%;height:100%;object-fit:cover;display:block;object-position:center center;opacity:1;transform-origin:center center;border:none;outline:none;box-shadow:none;position:relative;top:0;left:0;filter:contrast(1.2) brightness(1.1) saturate(1.1);image-rendering:auto}.overlay-container{position:absolute;inset:0;width:100%;height:100%;pointer-events:none;z-index:10;-webkit-user-select:none;user-select:none;overflow:hidden;background:#29252da6;backdrop-filter:blur(.5px);-webkit-backdrop-filter:blur(.5px);--name-label-font-size: 14px}.header-label{position:absolute;top:20px;font-family:Gilroy,sans-serif;text-shadow:0 1px 8px rgba(0,0,0,.8);pointer-events:none}.header-label.left{left:25%;transform:translate(-50%);font-size:var(--name-label-font-size);font-weight:400;color:#fffffff2;text-align:center}.header-label.center{left:50%;transform:translate(-50%);font-size:12px;font-weight:400;color:#fffc;letter-spacing:0}.header-label.right{left:75%;transform:translate(-50%);font-size:var(--name-label-font-size);font-weight:400;color:#fffffff2;text-align:center}.chips-grid{position:absolute;inset:45px 0;overflow-y:auto;overscroll-behavior:none;scrollbar-width:none;box-sizing:border-box}.chips-grid::-webkit-scrollbar{display:none}.force-canvas{position:relative;width:100%;box-sizing:border-box;min-height:120px}.connections-svg{position:absolute;top:0;left:0;width:100%;pointer-events:none;z-index:1}@keyframes lineGlow{0%{opacity:0;stroke-dashoffset:8}30%{opacity:.15}to{opacity:.6;stroke-dashoffset:0}}.connection-line{stroke:#fe3c72d1;stroke-width:1.5px;stroke-dasharray:4;opacity:0;pointer-events:none;transition:opacity .3s ease}.connection-line.active{animation:lineGlow .7s cubic-bezier(.4,0,.2,1) forwards}.frosted-chip{position:absolute;cursor:pointer;pointer-events:auto;transition:transform .2s ease,box-shadow .2s ease,background .2s ease,opacity .3s ease;flex-shrink:0;padding:6px 18px;border-radius:999px;display:inline-flex;align-items:center;justify-content:center;transform:translate(-50%,-50%);z-index:2;border:1px solid rgba(255,255,255,.24);background:#ffffff30;box-shadow:inset 0 1.5px 2px #ffffff59,0 0 0 1px #0f172a38;backdrop-filter:blur(.5px);-webkit-backdrop-filter:blur(.5px)}.frosted-chip:hover{transform:translate(-50%,-50%) scale(1.04);background:#ffffff29;cursor:grab}.frosted-chip.active{background:#ffffff4d;box-shadow:0 0 15px #fe3c7261,inset 0 1.5px 2px #ffffff80,0 0 0 1px #ffffff52;transform:translate(-50%,-50%) scale(1.06);z-index:10;cursor:grabbing}.frosted-chip.active .chip-label{color:#fff!important}.frosted-chip.dimmed{opacity:.35}.frosted-chip .chip-label{position:relative;color:#a8a8a8;text-align:center;font-family:Gilroy,sans-serif;font-size:14px;font-style:normal;font-weight:400;line-height:140%;letter-spacing:-.01em;text-shadow:none;white-space:nowrap;pointer-events:none;z-index:1}.ghost-chip{visibility:hidden!important;pointer-events:none!important}.view-profile{position:absolute;bottom:8px;color:#ffffff80;text-shadow:0 2px 4.2px #000;font-family:Calistoga,serif;font-size:var(--name-label-font-size);font-style:normal;font-weight:400;line-height:normal;cursor:pointer;pointer-events:auto;transition:opacity .2s ease,transform .2s ease}.view-profile:hover{opacity:.85}.view-profile.left{left:8%;width:34%;text-align:left;transform:none}.view-profile.left:hover{transform:translateY(-2px)}.view-profile.right{right:8%;width:34%;text-align:right;left:auto;transform:none}.view-profile.right:hover{transform:translateY(-2px)}#drag-preview{position:absolute;pointer-events:none;display:none;background:#000000b3;color:#fff;padding:5px 10px;border-radius:5px;z-index:9999;max-width:200px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.shape-text-container{width:70px;position:relative;overflow:hidden}.draggable{width:70px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;cursor:grab;-webkit-user-select:none;user-select:none;position:relative;display:inline-block}.dragging{width:auto;text-overflow:clip;cursor:grabbing;position:absolute;z-index:1000}.p-wrapper{width:70px;overflow-x:hidden}.shape-text-center{display:flex;justify-content:space-between;flex-direction:column;gap:10px;height:100%;margin-top:55px}.loading-indicator{position:absolute;inset:0;width:100%;height:100%;background:#00000040;backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px);color:#fff;z-index:100;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12px;pointer-events:auto}.loading-spinner{width:44px;height:44px;border:3.5px solid rgba(255,255,255,.25);border-top:3.5px solid #ffffff;border-radius:50%;animation:spin .8s linear infinite;box-shadow:0 0 15px #fff3}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.loading-indicator p{margin:0;font-size:14px;font-weight:500;color:#ffffffd9;letter-spacing:.02em}.api-key-modal-overlay{position:fixed;inset:0;background:#0f172a99;-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px);display:flex;align-items:center;justify-content:center;z-index:2000}.api-key-modal{width:min(640px,92vw);background:#0f172a;color:#e2e8f0;border:1px solid #334155;border-radius:12px;box-shadow:0 20px 50px #00000073;padding:16px 18px}.api-key-modal-header{display:flex;align-items:center;justify-content:space-between;gap:12px}.api-key-modal-header h2{margin:0;font-size:20px;font-weight:700;color:#e2e8f0}.warning-icon{color:#fbbf24}.api-key-modal .close-btn{background:#0b1220;border:1px solid #334155;color:#94a3b8;border-radius:8px;padding:6px;cursor:pointer;line-height:0}.api-key-modal-body{margin-top:12px;display:flex;flex-direction:column;gap:10px}.modal-message,.modal-instruction{margin:0;color:#cbd5e1}.modal-message strong{color:#e5e7eb}.api-key-input-group{display:flex;align-items:center;gap:10px}.api-key-input{flex:1 1 auto;background:#0a1020;color:#e2e8f0;border:1px solid #334155;border-radius:8px;padding:10px 12px}.api-key-input:focus{outline:none;border-color:#2563eb;box-shadow:0 0 0 3px #2563eb33}.api-key-load-btn{background:#2563eb;color:#fff;border:none;border-radius:8px;padding:10px 12px;cursor:pointer;font-weight:600}.api-key-load-btn:hover{background:#1d4ed8}.modal-hint{display:flex;align-items:center;gap:8px;color:#94a3b8}.modal-hint a{color:#93c5fd}@media(max-width:1023px){.profile-screen{max-width:640px}.profile-flex{height:400px}.profile-img{height:550px}}@media(max-width:767px){.profile-screen,.profile-flex,.overlay-container{width:100%;max-width:100%;overflow-x:hidden;box-sizing:border-box}.profile-screen{border-radius:0}.profile-flex{height:360px}.profile-img{height:510px}.chips-grid{width:100%;max-width:100%;padding:0;top:45px;bottom:50px;box-sizing:border-box}.frosted-chip{padding:4px 10px;max-width:100%;box-sizing:border-box}.frosted-chip .chip-label{font-size:11px;max-width:100%;overflow:hidden;text-overflow:ellipsis}.view-profile{bottom:10px}}@media(max-width:480px){.overlay-container{--name-label-font-size: 12px}.profile-flex{height:340px}.profile-img{height:480px}.frosted-chip{padding:3px 8px}.frosted-chip .chip-label,.header-label.center{font-size:10px}}\n"], dependencies: [{ kind: "directive", type: i2_1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i2_1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] });
|
|
3673
4191
|
}
|
|
3674
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.
|
|
4192
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.26", ngImport: i0, type: ProfileComparisonLibComponent, decorators: [{
|
|
3675
4193
|
type: Component,
|
|
3676
4194
|
args: [{ selector: 'lib-profile-comparison', standalone: false, template: "<div class=\"configure-backend-message\" *ngIf=\"!backendConfigured\">\r\n Configure backend\r\n</div>\r\n\r\n<div class=\"profile-screen\" *ngIf=\"backendConfigured\">\r\n <div #profileFlex class=\"profile-flex\" [class.fade-all]=\"fadeAllEdges\">\r\n <svg class=\"image-noise-overlay\" style=\"position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; overflow: hidden; z-index: 0;\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 320 320\" fill=\"none\" preserveAspectRatio=\"none\">\r\n <g filter=\"url(#filter0_n_1_1753)\">\r\n <rect width=\"100%\" height=\"100%\" fill=\"url(#paint0_radial_1_1753)\" fill-opacity=\"0.33\"/>\r\n </g>\r\n <defs>\r\n <filter id=\"filter0_n_1_1753\" x=\"0%\" y=\"0%\" width=\"100%\" height=\"100%\" filterUnits=\"objectBoundingBox\" color-interpolation-filters=\"sRGB\">\r\n <feFlood flood-opacity=\"0\" result=\"BackgroundImageFix\"/>\r\n <feBlend mode=\"normal\" in=\"SourceGraphic\" in2=\"BackgroundImageFix\" result=\"shape\"/>\r\n <feTurbulence type=\"fractalNoise\" baseFrequency=\"0.10869565606117249 0.10869565606117249\" stitchTiles=\"stitch\" numOctaves=\"3\" result=\"noise\" seed=\"7339\"/>\r\n <feColorMatrix in=\"noise\" type=\"luminanceToAlpha\" result=\"alphaNoise\"/>\r\n <feComponentTransfer in=\"alphaNoise\" result=\"coloredNoise1\">\r\n <feFuncA type=\"discrete\" tableValues=\"0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 \"/>\r\n </feComponentTransfer>\r\n <feComposite operator=\"in\" in2=\"shape\" in=\"coloredNoise1\" result=\"noise1Clipped\"/>\r\n <feFlood flood-color=\"rgba(0, 0, 0, 0.25)\" result=\"color1Flood\"/>\r\n <feComposite operator=\"in\" in2=\"noise1Clipped\" in=\"color1Flood\" result=\"color1\"/>\r\n <feMerge result=\"effect1_noise_1_1753\">\r\n <feMergeNode in=\"shape\"/>\r\n <feMergeNode in=\"color1\"/>\r\n </feMerge>\r\n </filter>\r\n <radialGradient id=\"paint0_radial_1_1753\" cx=\"50%\" cy=\"50%\" r=\"50%\" fx=\"50%\" fy=\"50%\">\r\n <stop offset=\"0%\" stop-opacity=\"0\"/>\r\n <stop offset=\"100%\" stop-opacity=\"0.25\"/>\r\n </radialGradient>\r\n </defs>\r\n </svg>\r\n <div #profileImgLeft class=\"profile-img left\" [class.fade-all]=\"fadeAllEdges\">\r\n <img\r\n [src]=\"user1Image\"\r\n alt=\"User 1\"\r\n [style.transform]=\"user1Transform\"\r\n [style.top]=\"user1Top\"\r\n [style.height]=\"user1Height\"\r\n [style.object-position]=\"user1ObjectPosition\"\r\n (load)=\"onUserImageLoad(1, $event)\"\r\n />\r\n </div>\r\n <div #profileImgRight class=\"profile-img right\" [class.fade-all]=\"fadeAllEdges\">\r\n <img\r\n [src]=\"user2Image\"\r\n alt=\"User 2\"\r\n [style.transform]=\"user2Transform\"\r\n [style.top]=\"user2Top\"\r\n [style.height]=\"user2Height\"\r\n [style.object-position]=\"user2ObjectPosition\"\r\n (load)=\"onUserImageLoad(2, $event)\"\r\n />\r\n </div>\r\n <div class=\"image-shade-overlay\"></div>\r\n\r\n <!-- Frosted Glass Overlay Layer -->\r\n <div class=\"overlay-container\">\r\n <!-- Top Headers \u2014 absolutely positioned overlays, exactly as original -->\r\n <div class=\"header-label left\">{{ person1Name }}</div>\r\n <div class=\"header-label center\">Shared</div>\r\n <div class=\"header-label right\">{{ person2Name }}</div>\r\n\r\n <!-- Force-directed chip canvas (scrollable) -->\r\n <div class=\"chips-grid\" #chipsGrid>\r\n <div class=\"force-canvas\" [style.height.px]=\"forceCanvasHeight\">\r\n <!-- SVG Connections Layer -->\r\n <svg class=\"connections-svg\" [style.height.px]=\"forceCanvasHeight\">\r\n <line\r\n *ngFor=\"let link of sortedForceLinks\"\r\n [attr.x1]=\"getLinkSourceX(link)\"\r\n [attr.y1]=\"getLinkSourceY(link)\"\r\n [attr.x2]=\"getLinkTargetX(link)\"\r\n [attr.y2]=\"getLinkTargetY(link)\"\r\n class=\"connection-line\"\r\n [class.active]=\"isLinkConnectedToActive(link)\"\r\n [class.cross-group]=\"isCrossGroupLink(link)\"\r\n ></line>\r\n </svg>\r\n\r\n <div\r\n *ngFor=\"let node of forceNodes\"\r\n class=\"frosted-chip\"\r\n [class.person-a]=\"node.group === 'p1'\"\r\n [class.shared]=\"node.group === 'shared'\"\r\n [class.person-b]=\"node.group === 'p2'\"\r\n [class.active]=\"activeNode === node\"\r\n [class.dimmed]=\"activeNode && !isNodeConnected(node)\"\r\n [style.left.px]=\"node.x\"\r\n [style.top.px]=\"node.y\"\r\n [style.padding]=\"chipPaddingYPx + 'px ' + chipPaddingXPx + 'px'\"\r\n (mouseenter)=\"onNodeMouseEnter(node)\"\r\n (mouseleave)=\"onNodeMouseLeave(node)\"\r\n (click)=\"onNodeClick(node, $event)\"\r\n (mousedown)=\"onNodeDragStart(node, $event)\"\r\n (touchstart)=\"onNodeDragStart(node, $event)\"\r\n >\r\n <span\r\n class=\"chip-label\"\r\n [style.font-size.px]=\"chipFontSizePx\"\r\n [style.line-height]=\"chipLineHeight\"\r\n >{{ node.label }}</span>\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <!-- Bottom Actions -->\r\n <div class=\"view-profile left\" (click)=\"onViewProfile('left')\">View Profile</div>\r\n <div class=\"view-profile right\" (click)=\"onViewProfile('right')\">View Profile</div>\r\n </div>\r\n\r\n <!-- Loading indicator for alignment process -->\r\n <div *ngIf=\"isAligning\" class=\"loading-indicator\">\r\n <div class=\"loading-spinner\"></div>\r\n </div>\r\n</div>\r\n\r\n\r\n", styles: ["@charset \"UTF-8\";:host{display:block;width:100%;max-width:100%;overflow-x:hidden!important;box-sizing:border-box!important}.profile-img.left{position:absolute;top:0;bottom:0;left:0;width:58%;height:100%;margin:0;z-index:1;-webkit-mask-image:linear-gradient(to right,black 0%,black 45%,transparent 92%);mask-image:linear-gradient(to right,black 0%,black 45%,transparent 92%)}.profile-img.right{position:absolute;inset:0 0 0 auto;width:58%;height:100%;margin:0;z-index:2;-webkit-mask-image:linear-gradient(to right,transparent 8%,black 55%,black 100%);mask-image:linear-gradient(to right,transparent 8%,black 55%,black 100%)}@media(min-width:768px){.profile-img.left{width:65%;-webkit-mask-image:linear-gradient(to right,black 0%,black 40%,transparent 90%);mask-image:linear-gradient(to right,black 0%,black 40%,transparent 90%)}.profile-img.right{width:65%;-webkit-mask-image:linear-gradient(to right,transparent 10%,black 60%,black 100%);mask-image:linear-gradient(to right,transparent 10%,black 60%,black 100%)}}.configure-backend-message{padding:1rem;text-align:center;color:#bdc3c7;background:#34495e;border-radius:8px}.backend-error-message{padding:.75rem 1rem;text-align:center;color:#fef3c7;background:#b4530940;border:1px solid rgba(245,158,11,.5);border-radius:8px;margin-bottom:.5rem}.profile-screen{width:100%;max-width:768px;margin:0 auto;overflow:hidden;position:relative;border-radius:12px}.profile-flex{display:flex;width:100%;height:440px;overflow:hidden;background:transparent;gap:0;margin:0;padding:0;position:relative;box-shadow:none;border:none;--overlap-size: 40px}.profile-flex.fade-all{-webkit-mask-image:linear-gradient(to right,transparent 0%,black 18%,black 82%,transparent 100%),linear-gradient(to bottom,transparent 0%,black 15%,black 85%,transparent 100%);-webkit-mask-composite:intersect;mask-image:linear-gradient(to right,transparent 0%,black 18%,black 82%,transparent 100%),linear-gradient(to bottom,transparent 0%,black 15%,black 85%,transparent 100%);mask-composite:intersect}.profile-flex .image-shade-overlay{position:absolute;inset:0;width:100%;height:100%;pointer-events:none;z-index:3;background:radial-gradient(ellipse at center,transparent 40%,rgba(41,37,45,.25) 100%)}.profile-flex .image-noise-overlay{position:absolute;inset:0;width:100%;height:100%;max-width:100%;max-height:100%;pointer-events:none;overflow:hidden;z-index:0}.shape-bg1,.shape-bg2{position:absolute;max-width:250px;opacity:1;transition:transform .2s ease-out}.shape-bg1{z-index:1;left:0}.shape-bg2{right:0}.shape-bg.fade-all .shape-bg1{-webkit-mask-image:linear-gradient(to right,transparent 0%,black 20%,black 100%);mask-image:linear-gradient(to right,transparent 0%,black 20%,black 100%)}.shape-bg.fade-all .shape-bg2{-webkit-mask-image:linear-gradient(to left,transparent 0%,black 20%,black 100%);mask-image:linear-gradient(to left,transparent 0%,black 20%,black 100%)}.shape-bg svg{cursor:grab}.shape-bg svg:active{cursor:grabbing}.profile-img{height:100%;position:relative;flex-shrink:0;background:transparent;margin:0;padding:0;left:0;top:0;border:none;box-sizing:border-box;overflow:hidden;box-shadow:none;outline:none}.profile-img img{transition:transform .3s ease;width:100%;height:100%;object-fit:cover;display:block;object-position:center center;opacity:1;transform-origin:center center;border:none;outline:none;box-shadow:none;position:relative;top:0;left:0;filter:contrast(1.2) brightness(1.1) saturate(1.1);image-rendering:auto}.overlay-container{position:absolute;inset:0;width:100%;height:100%;pointer-events:none;z-index:10;-webkit-user-select:none;user-select:none;overflow:hidden;background:#29252da6;backdrop-filter:blur(.5px);-webkit-backdrop-filter:blur(.5px);--name-label-font-size: 14px}.header-label{position:absolute;top:20px;font-family:Gilroy,sans-serif;text-shadow:0 1px 8px rgba(0,0,0,.8);pointer-events:none}.header-label.left{left:25%;transform:translate(-50%);font-size:var(--name-label-font-size);font-weight:400;color:#fffffff2;text-align:center}.header-label.center{left:50%;transform:translate(-50%);font-size:12px;font-weight:400;color:#fffc;letter-spacing:0}.header-label.right{left:75%;transform:translate(-50%);font-size:var(--name-label-font-size);font-weight:400;color:#fffffff2;text-align:center}.chips-grid{position:absolute;inset:45px 0;overflow-y:auto;overscroll-behavior:none;scrollbar-width:none;box-sizing:border-box}.chips-grid::-webkit-scrollbar{display:none}.force-canvas{position:relative;width:100%;box-sizing:border-box;min-height:120px}.connections-svg{position:absolute;top:0;left:0;width:100%;pointer-events:none;z-index:1}@keyframes lineGlow{0%{opacity:0;stroke-dashoffset:8}30%{opacity:.15}to{opacity:.6;stroke-dashoffset:0}}.connection-line{stroke:#fe3c72d1;stroke-width:1.5px;stroke-dasharray:4;opacity:0;pointer-events:none;transition:opacity .3s ease}.connection-line.active{animation:lineGlow .7s cubic-bezier(.4,0,.2,1) forwards}.frosted-chip{position:absolute;cursor:pointer;pointer-events:auto;transition:transform .2s ease,box-shadow .2s ease,background .2s ease,opacity .3s ease;flex-shrink:0;padding:6px 18px;border-radius:999px;display:inline-flex;align-items:center;justify-content:center;transform:translate(-50%,-50%);z-index:2;border:1px solid rgba(255,255,255,.24);background:#ffffff30;box-shadow:inset 0 1.5px 2px #ffffff59,0 0 0 1px #0f172a38;backdrop-filter:blur(.5px);-webkit-backdrop-filter:blur(.5px)}.frosted-chip:hover{transform:translate(-50%,-50%) scale(1.04);background:#ffffff29;cursor:grab}.frosted-chip.active{background:#ffffff4d;box-shadow:0 0 15px #fe3c7261,inset 0 1.5px 2px #ffffff80,0 0 0 1px #ffffff52;transform:translate(-50%,-50%) scale(1.06);z-index:10;cursor:grabbing}.frosted-chip.active .chip-label{color:#fff!important}.frosted-chip.dimmed{opacity:.35}.frosted-chip .chip-label{position:relative;color:#a8a8a8;text-align:center;font-family:Gilroy,sans-serif;font-size:14px;font-style:normal;font-weight:400;line-height:140%;letter-spacing:-.01em;text-shadow:none;white-space:nowrap;pointer-events:none;z-index:1}.ghost-chip{visibility:hidden!important;pointer-events:none!important}.view-profile{position:absolute;bottom:8px;color:#ffffff80;text-shadow:0 2px 4.2px #000;font-family:Calistoga,serif;font-size:var(--name-label-font-size);font-style:normal;font-weight:400;line-height:normal;cursor:pointer;pointer-events:auto;transition:opacity .2s ease,transform .2s ease}.view-profile:hover{opacity:.85}.view-profile.left{left:8%;width:34%;text-align:left;transform:none}.view-profile.left:hover{transform:translateY(-2px)}.view-profile.right{right:8%;width:34%;text-align:right;left:auto;transform:none}.view-profile.right:hover{transform:translateY(-2px)}#drag-preview{position:absolute;pointer-events:none;display:none;background:#000000b3;color:#fff;padding:5px 10px;border-radius:5px;z-index:9999;max-width:200px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.shape-text-container{width:70px;position:relative;overflow:hidden}.draggable{width:70px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;cursor:grab;-webkit-user-select:none;user-select:none;position:relative;display:inline-block}.dragging{width:auto;text-overflow:clip;cursor:grabbing;position:absolute;z-index:1000}.p-wrapper{width:70px;overflow-x:hidden}.shape-text-center{display:flex;justify-content:space-between;flex-direction:column;gap:10px;height:100%;margin-top:55px}.loading-indicator{position:absolute;inset:0;width:100%;height:100%;background:#00000040;backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px);color:#fff;z-index:100;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12px;pointer-events:auto}.loading-spinner{width:44px;height:44px;border:3.5px solid rgba(255,255,255,.25);border-top:3.5px solid #ffffff;border-radius:50%;animation:spin .8s linear infinite;box-shadow:0 0 15px #fff3}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.loading-indicator p{margin:0;font-size:14px;font-weight:500;color:#ffffffd9;letter-spacing:.02em}.api-key-modal-overlay{position:fixed;inset:0;background:#0f172a99;-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px);display:flex;align-items:center;justify-content:center;z-index:2000}.api-key-modal{width:min(640px,92vw);background:#0f172a;color:#e2e8f0;border:1px solid #334155;border-radius:12px;box-shadow:0 20px 50px #00000073;padding:16px 18px}.api-key-modal-header{display:flex;align-items:center;justify-content:space-between;gap:12px}.api-key-modal-header h2{margin:0;font-size:20px;font-weight:700;color:#e2e8f0}.warning-icon{color:#fbbf24}.api-key-modal .close-btn{background:#0b1220;border:1px solid #334155;color:#94a3b8;border-radius:8px;padding:6px;cursor:pointer;line-height:0}.api-key-modal-body{margin-top:12px;display:flex;flex-direction:column;gap:10px}.modal-message,.modal-instruction{margin:0;color:#cbd5e1}.modal-message strong{color:#e5e7eb}.api-key-input-group{display:flex;align-items:center;gap:10px}.api-key-input{flex:1 1 auto;background:#0a1020;color:#e2e8f0;border:1px solid #334155;border-radius:8px;padding:10px 12px}.api-key-input:focus{outline:none;border-color:#2563eb;box-shadow:0 0 0 3px #2563eb33}.api-key-load-btn{background:#2563eb;color:#fff;border:none;border-radius:8px;padding:10px 12px;cursor:pointer;font-weight:600}.api-key-load-btn:hover{background:#1d4ed8}.modal-hint{display:flex;align-items:center;gap:8px;color:#94a3b8}.modal-hint a{color:#93c5fd}@media(max-width:1023px){.profile-screen{max-width:640px}.profile-flex{height:400px}.profile-img{height:550px}}@media(max-width:767px){.profile-screen,.profile-flex,.overlay-container{width:100%;max-width:100%;overflow-x:hidden;box-sizing:border-box}.profile-screen{border-radius:0}.profile-flex{height:360px}.profile-img{height:510px}.chips-grid{width:100%;max-width:100%;padding:0;top:45px;bottom:50px;box-sizing:border-box}.frosted-chip{padding:4px 10px;max-width:100%;box-sizing:border-box}.frosted-chip .chip-label{font-size:11px;max-width:100%;overflow:hidden;text-overflow:ellipsis}.view-profile{bottom:10px}}@media(max-width:480px){.overlay-container{--name-label-font-size: 12px}.profile-flex{height:340px}.profile-img{height:480px}.frosted-chip{padding:3px 8px}.frosted-chip .chip-label,.header-label.center{font-size:10px}}\n"] }]
|
|
3677
4195
|
}], ctorParameters: () => [{ type: ProfileComparisonBackendService }, { type: i0.Renderer2 }, { type: FileConversionService }, { type: ImageCompressionService }, { type: i0.ChangeDetectorRef }, { type: i0.NgZone }, { type: i0.ElementRef }, { type: undefined, decorators: [{
|
|
@@ -3763,19 +4281,19 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.24", ngImpo
|
|
|
3763
4281
|
}] } });
|
|
3764
4282
|
|
|
3765
4283
|
class ProfileComparisonLibModule {
|
|
3766
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.
|
|
3767
|
-
static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "20.3.
|
|
4284
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.26", ngImport: i0, type: ProfileComparisonLibModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
|
|
4285
|
+
static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "20.3.26", ngImport: i0, type: ProfileComparisonLibModule, declarations: [ProfileComparisonLibComponent], imports: [CommonModule,
|
|
3768
4286
|
HttpClientModule,
|
|
3769
4287
|
FormsModule,
|
|
3770
4288
|
ReactiveFormsModule], exports: [ProfileComparisonLibComponent] });
|
|
3771
|
-
static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "20.3.
|
|
4289
|
+
static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "20.3.26", ngImport: i0, type: ProfileComparisonLibModule, providers: [
|
|
3772
4290
|
ProfileComparisonBackendService
|
|
3773
4291
|
], imports: [CommonModule,
|
|
3774
4292
|
HttpClientModule,
|
|
3775
4293
|
FormsModule,
|
|
3776
4294
|
ReactiveFormsModule] });
|
|
3777
4295
|
}
|
|
3778
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.
|
|
4296
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.26", ngImport: i0, type: ProfileComparisonLibModule, decorators: [{
|
|
3779
4297
|
type: NgModule,
|
|
3780
4298
|
args: [{
|
|
3781
4299
|
declarations: [
|