import { Component, OnInit } from '@angular/core'; import { CommonModule, NgSwitch, NgSwitchCase, NgSwitchDefault } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { MatchingService, ApiMatchResponse, ApiMatchItem } from '../matching.service'; import { ActivatedRoute, Router } from '@angular/router'; interface Profile { user_id: number; name: string; age?: number; city?: string; job?: string; photo_url?: string; isOnline?: boolean; match_score?: number; score?: number; gender?: string; blue?: number; green?: number; yellow?: number; red?: number; explanations?: string[]; explanation_source?: string; // Marriage profile fields current_city?: string; date_of_birth?: string; job_role?: string; employment_status?: string; marital_status?: string; education_level?: string; expectation_score?: number; character_score?: number; // Additional fields full_name?: string; height?: string; skin_tone?: string; languages_spoken?: string; country?: string; blood_group?: string; religion?: string; dual_citizenship?: string; number_of_siblings?: string; siblings_position?: string; parents_living_status?: string; live_with_parents?: string; support_parents_financially?: string; family_communication_frequency?: string; food_preference?: string; smoking_habit?: string; alcohol_habit?: string; daily_routine?: string; fitness_level?: string; own_pets?: string; travel_preference?: string; relaxation_mode?: string; work_experience_years?: string; career_aspirations?: string; field_of_study?: string; remark?: string; hobbies_interests?: string; conflict_approach?: string; financial_style?: string; income_range?: string; relocation_willingness?: string; family_type?: string; children_timeline?: string; open_to_adoption?: string; deal_breakers?: string; other_non_negotiables?: string; health_constraints?: string; live_with_inlaws?: string; } interface ChatMessage { sender: 'me' | 'them'; text: string; time: string; } interface CompatibilityAnalysis { exactMatches: string[]; differences: FieldComparison[]; missingData: string[]; compatibilityPercent: number; totalExactMatches: number; totalDifferences: number; totalMissingData: number; } interface FieldComparison { fieldName: string; yourExpectation: string; theirProfile: string; isCritical?: boolean; } @Component({ selector: 'app-matching-list', standalone: true, imports: [CommonModule, FormsModule, NgSwitch, NgSwitchCase, NgSwitchDefault], templateUrl: './matching-list.component.html', styleUrls: ['./matching-list.component.css'] }) export class MatchingListComponent implements OnInit { activeTab: 'details' | 'compatibility' = 'details'; subTab: 'expectation' | 'character' = 'expectation'; showUserProfileModal: boolean = false; private characterExplanationLoaded = false; // User data userId: number | null = null; userName: string = 'User Name'; userCity: string = 'City'; userPhoto: string = 'https://cdn-icons-png.flaticon.com/512/3135/3135715.png'; userProfileCompletion: number = 75; totalMatches: number = 0; strengths: string[] = []; risks: string[] = []; sacrifices: string[] = []; // User profile data userProfileData: any = null; userExpectationData: any = null; showUserInfo: boolean = false; // Loading state loading = false; error: string | null = null; // Matching data result: ApiMatchResponse | null = null; groupedMatches: { range: string; items: ApiMatchItem[] }[] = []; displayedMatches: Profile[] = []; // Filters and search searchTerm: string = ''; // Dynamic filter options availableLocations: string[] = []; availableAgeRanges: string[] = ['18-25', '26-35', '36-45', '46+']; availableProfessions: string[] = []; availableEducationLevels: string[] = []; availableReligions: string[] = []; // Selected filters selectedLocation: string = ''; selectedProfession: string = ''; selectedEducation: string = ''; selectedReligion: string = ''; selectedAgeRange: string = ''; selectedRange: string = 'all'; availableRanges: string[] = ['all', '90-100', '80-89', '70-79', '60-69', 'below_60']; // Default to 'character' so the "All Matches" button is active by default mode: 'expectation-only' | 'expectation' | 'character' = 'character'; // Modals and UI states showProfileModal: boolean = false; showEditModal: boolean = false; showChatWindow: boolean = false; showLikedDropdown: boolean = false; // Selected profile data selectedProfile: Profile | null = null; likedProfiles: Profile[] = []; // Chat functionality currentChatUser: Profile | null = null; chatMessages: ChatMessage[] = []; newMessage: string = ''; showTyping: boolean = false; // Edit profile editName: string = ''; editCity: string = ''; // Compatibility Analysis compatibilityAnalysis: CompatibilityAnalysis | null = null; showExactMatches: boolean = false; showDifferences: boolean = true; setActiveTab(tab: 'details' | 'compatibility'): void { this.activeTab = tab; // When switching to compatibility tab, ensure data is loaded if (tab === 'compatibility' && this.selectedProfile) { console.log('🔄 Switching to compatibility tab, checking data...'); // If in character mode and no character data loaded yet, fetch it if (this.mode === 'character' && this.strengths.length === 0 && this.risks.length === 0 && this.sacrifices.length === 0) { console.log('🎯 Loading character data on tab switch...'); /* this.fetchCompatibilityExplanation(this.selectedProfile);*/ } // Log current state for debugging console.log('📊 Current character data state:', { strengths: this.strengths, risks: this.risks, sacrifices: this.sacrifices, profileExplanations: this.selectedProfile.explanations }); } } // Store marriage profiles data private marriageProfiles: Map = new Map(); private userPhotoMap: Map = new Map(); private usedPhotos: Set = new Set(); // Photo distribution tracking private usedPhotoIndices: Set = new Set(); private lastUsedMaleIndex: number = -1; private lastUsedFemaleIndex: number = -1; private recentMaleIndices: number[] = []; private recentFemaleIndices: number[] = []; private readonly MAX_RECENT_INDICES = 8; private readonly indianMalePhotos: string[] = [ 'assets/men/1.png', 'assets/men/2.png', 'assets/men/3.png', 'assets/men/4.png', 'assets/men/5.png', 'assets/men/6.png', 'assets/men/7.png', 'assets/men/8.png', 'assets/men/9.png', 'assets/men/10.png', 'assets/men/11.png', 'assets/men/12.png', 'assets/men/13.png', 'assets/men/14.png', 'assets/men/15.png', 'assets/men/16.png', 'assets/men/17.png', 'assets/men/18.png', 'assets/men/19.png', 'assets/men/20.png', 'assets/men/21.png', 'assets/men/22.png', 'assets/men/23.png', 'assets/men/24.png', 'assets/men/25.png', 'assets/men/26.png', 'assets/men/27.png', 'assets/men/28.png', 'assets/men/29.png', 'assets/men/30.png', 'assets/men/31.png', 'assets/men/32.png', 'assets/men/33.png', 'assets/men/34.png', 'assets/men/35.png', 'assets/men/36.png', 'assets/men/37.png', 'assets/men/38.png', 'assets/men/39.png', 'assets/men/40.png' ] private readonly indianFemalePhotos: string[] = [ 'assets/women/1.png', 'assets/women/2.png', 'assets/women/3.png', 'assets/women/4.png', 'assets/women/5.png', 'assets/women/6.png', 'assets/women/7.png', 'assets/women/8.png', 'assets/women/9.png', 'assets/women/10.png', 'assets/women/11.png', 'assets/women/12.png', 'assets/women/13.png', 'assets/women/14.png', 'assets/women/15.png', 'assets/women/16.png', 'assets/women/17.png', 'assets/women/18.png', 'assets/women/19.png', 'assets/women/20.png', 'assets/women/21.png', 'assets/women/22.png', 'assets/women/23.png', 'assets/women/24.png', 'assets/women/25.png', 'assets/women/26.png', 'assets/women/27.png', 'assets/women/28.png', 'assets/women/29.png', 'assets/women/30.png', 'assets/women/31.png', 'assets/women/32.png', 'assets/women/33.png', 'assets/women/34.png', 'assets/women/35.png', 'assets/women/36.png', 'assets/women/37.png', 'assets/women/38.png', 'assets/women/39.png', 'assets/women/40.png' ]; constructor( private matchingService: MatchingService, private route: ActivatedRoute, private router: Router ) { } onDocumentClick(event: MouseEvent): void { const likeSection = document.querySelector('.like-section'); if (likeSection && !likeSection.contains(event.target as Node)) { this.showLikedDropdown = false; } } ngOnDestroy(): void { document.removeEventListener('click', this.onDocumentClick.bind(this)); } ngOnInit(): void { this.route.paramMap.subscribe((params) => { this.processUserId(params); }); this.route.queryParamMap.subscribe((queryParams) => { this.processUserId(queryParams); }); document.addEventListener('click', this.onDocumentClick.bind(this)); } private processUserId(paramMap: any): void { const idFromId = Number(paramMap.get('id')); const idFromUserId = Number(paramMap.get('user_id')); this.userId = !isNaN(idFromId) && idFromId > 0 ? idFromId : !isNaN(idFromUserId) && idFromUserId > 0 ? idFromUserId : null; if (this.userId && !this.loading) { this.findMatches(this.userId); this.loadUserProfile(this.userId); } else if (!this.userId && !this.loading) { this.error = 'No user ID available. Please provide a user ID.'; } } loadUserProfile(userId: number): void { this.loading = true; this.matchingService.getMarriageProfile(userId).subscribe({ next: (profile: any) => { if (profile) { this.userName = profile.full_name || 'User Name'; this.userCity = profile.current_city || 'City'; if (profile.gender) { this.userPhoto = this.getConsistentPhoto(userId, profile.gender); } } this.loading = false; }, error: (err: any) => { console.error('Failed to load user profile:', err); this.userName = 'User Name'; this.userCity = 'City'; this.loading = false; } }); } findMatches(userId: number): void { this.error = null; this.result = null; this.groupedMatches = []; this.displayedMatches = []; this.loading = true; this.matchingService.checkMandatoryFields(userId).subscribe({ next: (mandatoryResult) => { console.log('🎯 CURRENT MANDATORY FIELDS FOR MATCHING:', mandatoryResult); }, error: (err) => { console.error('❌ Failed to check mandatory fields:', err); } }); this.matchingService // Use the component's current mode (default set above) when fetching matches .getMatches(userId, { role: 'marriage', limit: 10, excludeSelf: true, mode: this.mode }) .subscribe({ next: (res: ApiMatchResponse) => { this.processMatches(res, this.mode); }, error: (err: any) => { console.error('❌ ERROR in match finding:', err); this.error = err?.error?.error || 'Failed to fetch matches.'; this.loading = false; }, complete: () => { this.loading = false; } }); } sortByExpectationOnly(): void { if (!this.userId) return; this.loading = true; this.error = null; this.mode = 'expectation-only'; this.matchingService .getMatches(this.userId, { role: 'marriage', limit: 10, excludeSelf: true, mode: 'expectation-only' }) .subscribe({ next: (res: ApiMatchResponse) => { this.processMatches(res, 'expectation-only'); // Reapply filters after mode change setTimeout(() => this.applyFilters(), 0); }, error: (err: any) => { this.error = err?.error?.error || 'Failed to fetch expectation matches.'; this.loading = false; }, complete: () => (this.loading = false), }); } sortByExpectation(): void { if (!this.userId) return; this.loading = true; this.error = null; this.mode = 'expectation'; this.matchingService .getMatches(this.userId, { role: 'marriage', limit: 10, excludeSelf: true, mode: 'expectation' }) .subscribe({ next: (res: ApiMatchResponse) => { this.processMatches(res, 'expectation'); // Reapply filters after mode change setTimeout(() => this.applyFilters(), 0); }, error: (err: any) => { this.error = err?.error?.error || 'Failed to fetch matches.'; this.loading = false; }, complete: () => (this.loading = false), }); } sortCharacterwise(): void { if (!this.userId) return; this.loading = true; this.error = null; this.mode = 'character'; this.matchingService .getMatches(this.userId, { role: 'marriage', limit: 10, excludeSelf: true, mode: 'character' }) .subscribe({ next: (res: ApiMatchResponse) => { this.processMatches(res, 'character'); // Reapply filters after mode change setTimeout(() => this.applyFilters(), 0); }, error: (err: any) => { this.error = err?.error?.error || 'Failed to fetch character matches.'; this.loading = false; }, complete: () => (this.loading = false), }); } private processMatches(res: ApiMatchResponse, mode: string): void { console.log(`🎯 Processing matches for mode: ${mode}`); this.result = res; let scoreKey: keyof Pick; if (mode === 'expectation-only') { scoreKey = 'score_expect'; } else if (mode === 'character') { scoreKey = 'score_color'; } else { scoreKey = 'final_score'; } this.groupedMatches = this.prepareGroups(res.matches, scoreKey); this.totalMatches = this.calculateTotalMatches(res.matches); this.loadMarriageProfiles().then(() => { this.processMatchesForDisplay(); console.log(`✅ ${mode} match processing completed`); }); this.mode = mode as 'expectation-only' | 'expectation' | 'character'; } private async loadMarriageProfiles(): Promise { if (!this.result) return; const userIds: number[] = []; this.groupedMatches.forEach(group => { group.items.forEach(item => { userIds.push(item.user_id); }); }); for (const userId of userIds) { if (!this.marriageProfiles.has(userId)) { try { const profile = await this.fetchMarriageProfile(userId); if (profile) { this.marriageProfiles.set(userId, profile); } } catch (error) { console.error(`Failed to fetch profile for user ${userId}:`, error); } } } } private fetchMarriageProfile(userId: number): Promise { return new Promise((resolve, reject) => { this.matchingService.getMarriageProfile(userId).subscribe({ next: (profile: any) => resolve(profile), error: (err: any) => reject(err) }); }); } public processMatchesForDisplay(): void { this.resetPhotoTracking(); const locations = new Set(); const professions = new Set(); const educationLevels = new Set(); const religions = new Set(); this.displayedMatches = []; const filteredGroups = this.filteredGroups; filteredGroups.forEach(group => { group.items.forEach(item => { const marriageProfile = this.marriageProfiles.get(item.user_id); let age: number | undefined; if (marriageProfile?.date_of_birth) { age = this.calculateAge(marriageProfile.date_of_birth); } const gender = this.getGender(item, marriageProfile); let matchScore: number; let expectationScore: number; let characterScore: number; if (this.mode === 'expectation-only') { matchScore = Math.round((item.score_expect || 0) * 100); expectationScore = matchScore; characterScore = Math.round((item.score_color || 0) * 100); } else if (this.mode === 'character') { matchScore = Math.round((item.score_color || 0) * 100); characterScore = matchScore; expectationScore = Math.round((item.score_expect || 0) * 100); } else { matchScore = Math.round(item.final_score || 0); expectationScore = Math.round((item.score_expect || 0) * 100); characterScore = Math.round((item.score_color || 0) * 100); } // Collect filter options if (marriageProfile?.current_city) { locations.add(marriageProfile.current_city); } if (marriageProfile?.job_role) { professions.add(marriageProfile.job_role); } if (marriageProfile?.education_level) { educationLevels.add(marriageProfile.education_level); } if (marriageProfile?.religion) { religions.add(marriageProfile.religion); } const profile: Profile = { user_id: item.user_id, name: item.name || 'Unknown', age: age, city: marriageProfile?.current_city || 'Unknown', job: marriageProfile?.job_role || marriageProfile?.employment_status || 'Not specified', photo_url: this.getConsistentPhoto(item.user_id, gender), isOnline: Math.random() > 0.5, match_score: matchScore, score: matchScore, gender: gender, blue: item.blue, green: item.green, yellow: item.yellow, red: item.red, explanations: item.explanations, explanation_source: item.explanation_source, // Map database fields current_city: marriageProfile?.current_city, date_of_birth: marriageProfile?.date_of_birth, job_role: marriageProfile?.job_role, employment_status: marriageProfile?.employment_status, marital_status: marriageProfile?.marital_status, education_level: marriageProfile?.education_level, full_name: marriageProfile?.full_name, height: marriageProfile?.height, skin_tone: marriageProfile?.skin_tone, languages_spoken: marriageProfile?.languages_spoken, country: marriageProfile?.country, blood_group: marriageProfile?.blood_group, religion: marriageProfile?.religion, dual_citizenship: marriageProfile?.dual_citizenship, number_of_siblings: marriageProfile?.number_of_siblings, siblings_position: marriageProfile?.siblings_position, parents_living_status: marriageProfile?.parents_living_status, live_with_parents: marriageProfile?.live_with_parents, support_parents_financially: marriageProfile?.support_parents_financially, family_communication_frequency: marriageProfile?.family_communication_frequency, food_preference: marriageProfile?.food_preference, smoking_habit: marriageProfile?.smoking_habit, alcohol_habit: marriageProfile?.alcohol_habit, daily_routine: marriageProfile?.daily_routine, fitness_level: marriageProfile?.fitness_level, own_pets: marriageProfile?.own_pets, travel_preference: marriageProfile?.travel_preference, relaxation_mode: marriageProfile?.relaxation_mode, work_experience_years: marriageProfile?.work_experience_years, career_aspirations: marriageProfile?.career_aspirations, field_of_study: marriageProfile?.field_of_study, remark: marriageProfile?.remark, hobbies_interests: marriageProfile?.hobbies_interests, conflict_approach: marriageProfile?.conflict_approach, financial_style: marriageProfile?.financial_style, income_range: marriageProfile?.income_range, relocation_willingness: marriageProfile?.relocation_willingness, family_type: marriageProfile?.family_type, children_timeline: marriageProfile?.children_timeline, open_to_adoption: marriageProfile?.open_to_adoption, deal_breakers: marriageProfile?.deal_breakers, other_non_negotiables: marriageProfile?.other_non_negotiables, health_constraints: marriageProfile?.health_constraints, live_with_inlaws: marriageProfile?.live_with_inlaws, expectation_score: expectationScore, character_score: characterScore }; this.displayedMatches.push(profile); }); }); // Update filter options with collected data this.availableLocations = Array.from(locations).sort(); this.availableProfessions = Array.from(professions).sort(); this.availableEducationLevels = Array.from(educationLevels).sort(); this.availableReligions = Array.from(religions).sort(); this.displayedMatches.sort((a, b) => (b.match_score || 0) - (a.match_score || 0)); } private getGender(item: ApiMatchItem, marriageProfile: any): string { if (item.gender) return item.gender; if (marriageProfile?.gender) return marriageProfile.gender; return Math.random() > 0.5 ? 'male' : 'female'; } private calculateAge(dateOfBirth: string): number { try { const birthDate = new Date(dateOfBirth); const today = new Date(); let age = today.getFullYear() - birthDate.getFullYear(); const monthDiff = today.getMonth() - birthDate.getMonth(); if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) { age--; } return age; } catch (error) { console.error('Error calculating age:', error); return Math.floor(Math.random() * 20) + 25; } } private calculateTotalMatches(matches: { [range: string]: ApiMatchItem[] }): number { return Object.values(matches).reduce((total, group) => total + group.length, 0); } private prepareGroups(matches: ApiMatchResponse['matches'], sortKey: keyof Pick) { const ordered: { range: string; items: ApiMatchItem[] }[] = []; const order = ['90-100', '80-89', '70-79', '60-69', 'below_60']; for (const range of Object.keys(matches)) { const items = matches[range] || []; ordered.push({ range, items: [...items].sort((a, b) => (Number(b[sortKey]) || 0) - (Number(a[sortKey]) || 0)), }); } ordered.sort((a, b) => order.indexOf(a.range) - order.indexOf(b.range)); return ordered; } // 🚨 UPDATED: Enhanced compatibility explanation methods /* fetchCompatibilityExplanation(profile: Profile): void { if (!this.userId) return; this.matchingService.getCompatibilityExplanation( this.userId, profile.user_id, this.mode ).subscribe({ next: (response) => { const updatedProfile = this.displayedMatches.find(p => p.user_id === profile.user_id); if (updatedProfile) { *//* updatedProfile.explanations = response.explanations; updatedProfile.explanation_source = this.mode === 'expectation-only' ? 'expectation' : this.mode === 'character' ? 'character' : 'combined';*//* updatedProfile.explanations = response.explanations; updatedProfile.explanation_source = response.source; } }, error: (err) => { console.error('Failed to fetch compatibility explanation:', err); } }); }*/ fetchCompatibilityExplanation(profile: Profile): void { if (!this.userId) return; // ✅ PREVENT DUPLICATE CALLS if (this.characterExplanationLoaded) { console.log(⏭️ Character explanation already loaded, skipping API call'); return; } this.characterExplanationLoaded = true; this.matchingService.getCompatibilityExplanation( this.userId, profile.user_id, this.mode ).subscribe({ next: (response) => { const updatedProfile = this.displayedMatches.find(p => p.user_id === profile.user_id); if (updatedProfile) { // ✅ Store LLM or Expectation explanation exactly as backend sends updatedProfile.explanations = response.explanations; // ✅ MOST IMPORTANT FIX updatedProfile.explanation_source = response.source; // ✅ Insert this line here this.processCharacterExplanation(response.explanations); } }, error: (err) => { console.error('Failed to fetch compatibility explanation:', err); } }); } processCharacterExplanation(explanations: string[]) { console.log('🔍 Frontend: RAW explanations from backend:', explanations); this.characterExplanationLoaded = false; // allow retry on failure this.strengths = []; this.risks = []; this.sacrifices = []; let currentSection = ''; explanations.forEach((line, index) => { const trimmedLine = line.trim(); if (!trimmedLine) return; console.log(`🔍 Frontend: Line ${index}: "${trimmedLine}"`); // Clean the line - remove markdown and other formatting const cleanLine = trimmedLine .replace(/^[-•*]\s*/, '') // Remove bullet points .replace(/^#+\s*/, '') // Remove markdown headers .replace(/\*\*(.*?)\*\*/g, '$1') // Remove bold formatting .trim(); // Skip if line is too short after cleaning if (cleanLine.length < 5) return; // Section detection - more flexible with duplicate prevention const lowerLine = cleanLine.toLowerCase(); const isSectionHeader = this.isSectionHeader(cleanLine); if (isSectionHeader) { // Only switch section if it's actually a different section if (lowerLine.includes('strength') && currentSection !== 'strengths') { currentSection = 'strengths'; console.log('🎯 Frontend: Switching to strengths section'); return; // Skip adding section header to content } else if (lowerLine.includes('risk') && currentSection !== 'risks') { currentSection = 'risks'; console.log('🎯 Frontend: Switching to risks section'); return; // Skip adding section header to content } else if (lowerLine.includes('sacrific') && currentSection !== 'sacrifices') { currentSection = 'sacrifices'; console.log('🎯 Frontend: Switching to sacrifices section'); return; // Skip adding section header to content } // Numbered section detection with duplicate prevention else if ((lowerLine.startsWith('1.') || lowerLine.includes('1)')) && currentSection !== 'strengths') { currentSection = 'strengths'; console.log('🎯 Frontend: Switching to strengths section (numbered)'); return; } else if ((lowerLine.startsWith('2.') || lowerLine.includes('2)')) && currentSection !== 'risks') { currentSection = 'risks'; console.log('🎯 Frontend: Switching to risks section (numbered)'); return; } else if ((lowerLine.startsWith('3.') || lowerLine.includes('3)')) && currentSection !== 'sacrifices') { currentSection = 'sacrifices'; console.log('🎯 Frontend: Switching to sacrifices section (numbered)'); return; } } // Content lines (not section headers) - only add if we have a current section if (currentSection && cleanLine.length > 10 && !isSectionHeader) { if (currentSection === 'strengths') { this.strengths.push(cleanLine); console.log('✅ Frontend: Added to strengths:', cleanLine); } else if (currentSection === 'risks') { this.risks.push(cleanLine); console.log('✅ Frontend: Added to risks:', cleanLine); } else if (currentSection === 'sacrifices') { this.sacrifices.push(cleanLine); console.log('✅ Frontend: Added to sacrifices:', cleanLine); } } }); // Fallback: if we didn't find sections but have content, distribute it if (this.strengths.length === 0 && this.risks.length === 0 && this.sacrifices.length === 0) { console.log('🔄 Frontend: No sections detected, using fallback distribution'); this.distributeContentToSections(explanations); } // Ensure each section has at least some content this.ensureMinimumContent(); // Split long content into multiple points if needed this.splitLongContent(); console.log('📊 Frontend: Final processed sections', { strengths: this.strengths, risks: this.risks, sacrifices: this.sacrifices }); } private isSectionHeader(line: string): boolean { const lowerLine = line.toLowerCase(); return lowerLine.includes('character strength') || lowerLine.includes('character risk') || lowerLine.includes('sacrifices needed') || lowerLine.includes('1. character') || lowerLine.includes('2. character') || lowerLine.includes('3. sacrifices') || lowerLine === 'character strengths' || lowerLine === 'character risks' || lowerLine === 'sacrifices needed' || lowerLine === '1. character strengths' || lowerLine === '2. character risks' || lowerLine === '3. sacrifices needed'; } private splitLongContent() { // Split very long content into multiple bullet points for better readability const splitLongString = (text: string, maxLength: number = 150): string[] => { if (text.length <= maxLength) return [text]; const sentences = text.split(/[.!?]+/).filter(s => s.trim().length > 0); const result: string[] = []; let currentChunk = ''; for (const sentence of sentences) { const trimmedSentence = sentence.trim(); if (!trimmedSentence) continue; if (currentChunk.length + trimmedSentence.length + 2 <= maxLength) { currentChunk += (currentChunk ? '. ' : '') + trimmedSentence; } else { if (currentChunk) { result.push(currentChunk + '.'); } currentChunk = trimmedSentence; } } if (currentChunk) { result.push(currentChunk + '.'); } return result.length > 0 ? result : [text]; }; this.strengths = this.strengths.flatMap(item => splitLongString(item)); this.risks = this.risks.flatMap(item => splitLongString(item)); this.sacrifices = this.sacrifices.flatMap(item => splitLongString(item)); } private distributeContentToSections(lines: string[]) { const substantiveLines = lines .map(line => line .replace(/^[-•*]\s*/, '') .replace(/^#+\s*/, '') .replace(/\*\*(.*?)\*\*/g, '$1') .trim() ) .filter(line => line.length > 10 && !this.isSectionHeader(line) && !line.toLowerCase().includes('compatibility') && !line.includes('---') ); if (substantiveLines.length === 0) return; // Simple distribution: first third to strengths, second to risks, last to sacrifices const sectionSize = Math.ceil(substantiveLines.length / 3); this.strengths = substantiveLines.slice(0, sectionSize); this.risks = substantiveLines.slice(sectionSize, sectionSize * 2); this.sacrifices = substantiveLines.slice(sectionSize * 2); } private ensureMinimumContent() { // If any section is empty but others have content, redistribute const totalItems = this.strengths.length + this.risks.length + this.sacrifices.length; if (totalItems > 0) { if (this.strengths.length === 0 && this.risks.length > 1) { this.strengths.push(this.risks.pop()!); } if (this.risks.length === 0 && this.sacrifices.length > 1) { this.risks.push(this.sacrifices.pop()!); } if (this.sacrifices.length === 0 && this.strengths.length > 1) { this.sacrifices.push(this.strengths.pop()!); } } // Ensure we have at least one item in each section if possible if (totalItems >= 3) { while (this.strengths.length === 0 && (this.risks.length > 1 || this.sacrifices.length > 1)) { if (this.risks.length > 1) { this.strengths.push(this.risks.pop()!); } else if (this.sacrifices.length > 1) { this.strengths.push(this.sacrifices.pop()!); } } while (this.risks.length === 0 && (this.strengths.length > 1 || this.sacrifices.length > 1)) { if (this.strengths.length > 1) { this.risks.push(this.strengths.pop()!); } else if (this.sacrifices.length > 1) { this.risks.push(this.sacrifices.pop()!); } } while (this.sacrifices.length === 0 && (this.strengths.length > 1 || this.risks.length > 1)) { if (this.strengths.length > 1) { this.sacrifices.push(this.strengths.pop()!); } else if (this.risks.length > 1) { this.sacrifices.push(this.risks.pop()!); } } } } // Call this when opening profile modal openProfileModal(profile: Profile): void { this.selectedProfile = profile; this.showProfileModal = true; this.showExactMatches = true; this.showDifferences = false; this.compatibilityAnalysis = null; this.activeTab = 'details'; this.characterExplanationLoaded = false; // Reset subTab based on mode if (this.mode === 'expectation') { this.subTab = 'expectation'; // Default to expectation tab for combined mode } else { this.subTab = 'expectation'; // Default for other modes } console.log('🟢 Opening profile modal for:', profile.name, 'Mode:', this.mode); // Reset character sections this.strengths = []; this.risks = []; this.sacrifices = []; // Load analysis based on mode if (this.mode === 'expectation-only') { console.log('🎯 Loading expectation-only analysis...'); this.loadCompatibilityAnalysis(profile); } else if (this.mode === 'character') { console.log('🎯 Loading character analysis...'); this.fetchCompatibilityExplanation(profile); } else if (this.mode === 'expectation') { console.log('🎯 Loading both expectation and character analysis...'); // Load both for combined mode this.loadCompatibilityAnalysis(profile); // This loads expectation analysis this.fetchCompatibilityExplanation(profile); // This loads character analysis } } private loadCompatibilityAnalysis(profile: Profile): void { if (!this.userId) return; console.log('🟢 loadCompatibilityAnalysis called for mode:', this.mode); // For Character mode, use the character explanation if (this.mode === 'character') { console.log('🎯 Character mode - fetching character explanation'); this.fetchCompatibilityExplanation(profile); } // For Expectation and Combined modes, fetch detailed analysis else if (this.mode === 'expectation-only' || this.mode === 'expectation') { this.matchingService.getCompatibilityExplanation( this.userId, profile.user_id, this.mode ).subscribe({ next: (response) => { console.log('✅ Compatibility analysis received:', response); this.processCompatibilityAnalysis(response.explanations); }, error: (err) => { console.error('❌ Failed to fetch compatibility analysis:', err); this.generateExpectationFallbackAnalysis(profile); } }); } } private generateCharacterStrengths(dominant: any, secondary: any): string[] { const combinations: { [key: string]: string[] } = { 'Blue-Red': [ 'Analytical thinking complements decisive action', 'Thorough planning balances quick decision-making', 'Data-driven approach supports confident leadership', 'Strategic thinking combined with strong execution' ], 'Green-Yellow': [ 'Structured organization grounds creative ideas', 'Process-oriented approach gives vision practical form', 'Reliability provides stability for innovation', 'Methodical planning supports imaginative solutions' ], 'Blue-Green': [ 'Detailed analysis combines with systematic execution', 'Methodical approach ensures thorough implementation', 'Precision and organization create reliable outcomes', 'Careful planning meets structured follow-through' ], 'Red-Yellow': [ 'Action-oriented drive brings creative ideas to life', 'Bold decisions support visionary thinking', 'Energy and enthusiasm fuel innovative projects', 'Confident leadership inspires creative collaboration' ], 'Blue-Yellow': [ 'Analytical depth enhances creative problem-solving', 'Thorough research supports innovative approaches', 'Logical thinking balances imaginative ideas', 'Data insights inspire creative solutions' ], 'Green-Red': [ 'Organized planning directs decisive action', 'Systematic approach channels energetic drive', 'Process efficiency supports quick implementation', 'Reliable structure guides confident decisions' ] }; const key = `${dominant.name}-${secondary.name}`; return combinations[key] || [ 'Complementary personality traits create balance', 'Different approaches bring diverse perspectives', 'Varied strengths cover multiple relationship aspects', 'Unique qualities enhance mutual understanding' ]; } private generateCharacterRisks(dominant: any, secondary: any): string[] { const combinations: { [key: string]: string[] } = { 'Blue-Red': [ 'Over-analysis may frustrate action-oriented partner', 'Quick decisions might overlook important details', 'Direct communication may clash with thoughtful processing', 'Impatience with detailed planning could cause tension' ], 'Green-Yellow': [ 'Rigid routines may limit spontaneous creativity', 'Unstructured ideas may disrupt organized systems', 'Process focus might slow down innovative thinking', 'Resistance to change could hinder creative expression' ], 'Blue-Green': [ 'Excessive planning may delay actual progress', 'Over-caution might prevent necessary risks', 'Analysis paralysis in decision-making situations', 'Too much structure could limit flexibility' ], 'Red-Yellow': [ 'Impulsive actions may lack long-term vision', 'Big ideas might overlook practical implementation', 'Enthusiasm may override careful consideration', 'Inconsistent follow-through on creative projects' ], 'Blue-Yellow': [ 'Over-thinking may dampen spontaneous creativity', 'Abstract ideas might lack practical grounding', 'Detail focus could miss the bigger picture', 'Analysis may slow down creative momentum' ], 'Green-Red': [ 'Bureaucratic processes may frustrate quick action', 'Impulsive decisions could disrupt careful planning', 'Directness may overwhelm methodical approach', 'Speed vs thoroughness conflict in projects' ] }; const key = `${dominant.name}-${secondary.name}`; return combinations[key] || [ 'Different communication styles may cause misunderstandings', 'Varying energy levels could lead to timing conflicts', 'Contrasting approaches to problems may create tension', 'Differing priorities might require careful negotiation' ]; } private generateCharacterSacrifices(dominant: any, secondary: any): string[] { const combinations: { [key: string]: string[] } = { 'Blue-Red': [ 'Analytical partner must accept quicker decisions sometimes', 'Action-oriented partner needs to allow time for reflection', 'Both must find middle ground between speed and thoroughness', 'Direct communicator should practice more patience in discussions' ], 'Green-Yellow': [ 'Organized partner should embrace some spontaneity', 'Creative partner needs to respect established routines', 'Both must balance structure with flexibility', 'Process-focused partner should allow creative experimentation' ], 'Blue-Green': [ 'Need to move from planning to action more quickly', 'Must embrace some uncertainty in decision-making', 'Both should practice more direct communication', 'Should balance perfectionism with practical progress' ], 'Red-Yellow': [ 'Need to ground big ideas with practical steps', 'Must balance enthusiasm with realistic planning', 'Both should develop more patience in execution', 'Should combine vision with actionable strategies' ], 'Blue-Yellow': [ 'Analytical thinker should embrace intuitive leaps', 'Creative partner needs to consider practical constraints', 'Both must balance imagination with reality checks', 'Should combine innovation with implementable plans' ], 'Green-Red': [ 'Structured partner should allow faster execution sometimes', 'Action-oriented partner needs to follow established processes', 'Both must compromise between speed and quality', 'Should balance efficiency with thoroughness' ] }; const key = `${dominant.name}-${secondary.name}`; return combinations[key] || [ 'Both partners need to understand different communication styles', 'Compromise between individual preferences and shared needs', 'Balance personal approaches with relationship harmony', 'Adapt to each other\'s natural rhythms and working styles' ]; } private analyzeCharacterForSections(profile: Profile): { strengths: string[], risks: string[], sacrifices: string[] } { const colors = [ { name: 'Blue', value: profile.blue || 0, trait: 'analytical and structured' }, { name: 'Green', value: profile.green || 0, trait: 'organized and process-oriented' }, { name: 'Yellow', value: profile.yellow || 0, trait: 'creative and visionary' }, { name: 'Red', value: profile.red || 0, trait: 'decisive and action-driven' } ]; // Find dominant and secondary colors const sortedColors = [...colors].sort((a, b) => b.value - a.value); const dominant = sortedColors[0]; const secondary = sortedColors[1]; // Generate dynamic content based on color combinations const strengths = this.generateCharacterStrengths(dominant, secondary); const risks = this.generateCharacterRisks(dominant, secondary); const sacrifices = this.generateCharacterSacrifices(dominant, secondary); return { strengths, risks, sacrifices }; } // 🚨 UPDATED: Character mode fallback analysis with LLM-style structure private generateCharacterFallbackAnalysis(profile: Profile): CompatibilityAnalysis { const exactMatches: string[] = []; const differences: FieldComparison[] = []; const missingData: string[] = []; // Get character score and color analysis using existing function const characterScore = profile.character_score || 0; const characterAnalysis = this.analyzeCharacterForSections(profile); // Character Strengths (Exact Matches) characterAnalysis.strengths.slice(0, 3).forEach(strength => { exactMatches.push(`✅ ${strength}`); }); // Character Risks (Differences) characterAnalysis.risks.slice(0, 2).forEach(risk => { differences.push({ fieldName: 'Personality Dynamics', yourExpectation: 'Harmonious interaction', theirProfile: risk, isCritical: false }); }); // Sacrifices Needed (Missing Data / Areas for Growth) characterAnalysis.sacrifices.slice(0, 2).forEach(sacrifice => { missingData.push(`⚠️ ${sacrifice}`); }); const totalComparisons = exactMatches.length + differences.length; const compatibilityPercent = characterScore; return { exactMatches, differences, missingData, compatibilityPercent, totalExactMatches: exactMatches.length, totalDifferences: differences.length, totalMissingData: missingData.length }; } // 🚨 NEW: Detailed color analysis generator matching LLM format private getDetailedColorAnalysis(profile: Profile): { strengths: { calmEnergy: string; decisionMaking: string; emotionalPatterns: string; }; risks: { communicationStyle: string; lifestylePace: string; }; sacrifices: { communication: string; emotionalNeeds: string; dailyRoutines: string; }; } { const colors = [ { name: 'Blue', value: profile.blue || 0, traits: ['analytical', 'calm', 'thoughtful'] }, { name: 'Green', value: profile.green || 0, traits: ['organized', 'structured', 'process-oriented'] }, { name: 'Yellow', value: profile.yellow || 0, traits: ['creative', 'visionary', 'spontaneous'] }, { name: 'Red', value: profile.red || 0, traits: ['decisive', 'action-driven', 'direct'] } ]; // Sort by dominance const dominantColors = [...colors].sort((a, b) => b.value - a.value); const primary = dominantColors[0]; const secondary = dominantColors[1]; // Generate strengths based on color combinations const strengths = this.generateStrengths(primary, secondary); const risks = this.generateRisks(primary, secondary); const sacrifices = this.generateSacrifices(primary, secondary); return { strengths, risks, sacrifices }; } // Add this method to handle range filter changes onRangeChange(): void { this.applyFilters(); } // 🚨 NEW: Generate character strengths in LLM format private generateStrengths(primary: any, secondary: any): any { const strengthTemplates = [ "One brings calm thinking while the other adds energy, creating balance.", "Their decisive nature pairs well with your thoughtful approach.", "Both show complementary emotional patterns.", "Practical planning meets creative problem-solving effectively.", "Structured approach balances spontaneous decision-making.", "Analytical thinking complements intuitive understanding." ]; // Select strengths based on color combinations const selectedStrengths = [ strengthTemplates[0], // Always include the first one this.getSpecificStrength(primary, secondary), strengthTemplates[5] // Always include emotional patterns ]; return { calmEnergy: selectedStrengths[0], decisionMaking: selectedStrengths[1], emotionalPatterns: selectedStrengths[2] }; } private getSpecificStrength(primary: any, secondary: any): string { const primaryName = primary.name.toLowerCase(); const secondaryName = secondary?.name.toLowerCase() || 'blue'; const strengthMap: { [key: string]: string } = { 'blue-red': 'Their decisive nature pairs well with your thoughtful approach.', 'blue-yellow': 'Creative vision enhances your analytical thinking.', 'blue-green': 'Structured planning complements your methodical nature.', 'red-blue': 'Your decisive action is balanced by their thoughtful analysis.', 'red-yellow': 'Direct approach combines well with creative solutions.', 'red-green': 'Action-oriented style benefits from organized planning.', 'yellow-blue': 'Spontaneous ideas are grounded by logical thinking.', 'yellow-red': 'Creative energy is channeled through decisive action.', 'yellow-green': 'Visionary thinking is supported by practical organization.', 'green-blue': 'Organized approach enhances analytical capabilities.', 'green-red': 'Structured planning supports decisive implementation.', 'green-yellow': 'Methodical nature balances creative spontaneity.' }; return strengthMap[`${primaryName}-${secondaryName}`] || 'Their personality traits create complementary dynamics.'; } // 🚨 NEW: Generate character risks in LLM format private generateRisks(primary: any, secondary: any): any { const primaryName = primary.name.toLowerCase(); const secondaryName = secondary?.name.toLowerCase() || 'blue'; const riskMap: { [key: string]: { communicationStyle: string; lifestylePace: string } } = { 'blue-red': { communicationStyle: 'One may speak directly while the other needs time to process feelings', lifestylePace: 'Methodical approach may conflict with fast-paced decision making' }, 'blue-yellow': { communicationStyle: 'Structured communication may clash with free-flowing expression', lifestylePace: 'Spontaneity vs routine may clash in day-to-day life' }, 'red-blue': { communicationStyle: 'Direct communication style may overwhelm reflective processing', lifestylePace: 'Fast-paced approach may conflict with deliberate decision making' }, 'red-yellow': { communicationStyle: 'Action-oriented talk may miss creative nuances', lifestylePace: 'Goal-driven pace may overlook imaginative possibilities' }, 'yellow-blue': { communicationStyle: 'Expressive communication may lack structured detail', lifestylePace: 'Flexible scheduling may frustrate planned routines' }, 'yellow-green': { communicationStyle: 'Big-picture thinking may miss practical details', lifestylePace: 'Spontaneous changes may disrupt organized systems' }, 'green-blue': { communicationStyle: 'Process-focused discussion may lack emotional depth', lifestylePace: 'Rigid routines may limit adaptive opportunities' }, 'green-red': { communicationStyle: 'Detailed explanations may frustrate action-oriented listening', lifestylePace: 'Structured planning may slow down decisive action' } }; return riskMap[`${primaryName}-${secondaryName}`] || { communicationStyle: 'Different communication paces may require adjustment', lifestylePace: 'Varying daily rhythms may need coordination' }; } // 🚨 NEW: Generate sacrifices needed in LLM format private generateSacrifices(primary: any, secondary: any): any { const primaryName = primary.name.toLowerCase(); const secondaryName = secondary?.name.toLowerCase() || 'blue'; const sacrificeMap: { [key: string]: { communication: string; emotionalNeeds: string; dailyRoutines: string } } = { 'blue-red': { communication: 'The fast-responding partner must slow down during emotional conversations', emotionalNeeds: 'The more reserved partner must communicate needs openly', dailyRoutines: 'Both must compromise between structure and flexibility in daily routines' }, 'blue-yellow': { communication: 'The structured thinker must allow space for creative expression', emotionalNeeds: 'The spontaneous partner must provide advance notice for plans', dailyRoutines: 'Both must balance routine with unexpected opportunities' }, 'red-blue': { communication: 'The direct communicator must practice patience in discussions', emotionalNeeds: 'The analytical partner must express feelings more immediately', dailyRoutines: 'Action-oriented approach must accommodate thoughtful planning' }, 'red-yellow': { communication: 'The goal-focused partner must listen to imaginative ideas', emotionalNeeds: 'The creative partner must respect decision deadlines', dailyRoutines: 'Both must blend practical action with innovative thinking' }, 'yellow-blue': { communication: 'The big-picture thinker must provide specific details when needed', emotionalNeeds: 'The methodical partner must embrace spontaneous emotional expression', dailyRoutines: 'Flexible approach must adapt to important routines' }, 'yellow-green': { communication: 'The visionary must ground ideas in practical reality', emotionalNeeds: 'The organized partner must allow for unexpected emotional expressions', dailyRoutines: 'Spontaneous nature must respect important schedules' }, 'green-blue': { communication: 'The process-oriented partner must prioritize emotional connection', emotionalNeeds: 'The analytical partner must express feelings beyond logical framework', dailyRoutines: 'Structured approach must allow for adaptive changes' }, 'green-red': { communication: 'The detailed planner must summarize key points for action-takers', emotionalNeeds: 'The decisive partner must consider emotional impacts of quick decisions', dailyRoutines: 'Organized systems must accommodate timely actions' } }; return sacrificeMap[`${primaryName}-${secondaryName}`] || { communication: 'Both partners must adjust communication style to meet in the middle', emotionalNeeds: 'Each must understand and respect different emotional expression styles', dailyRoutines: 'Compromise needed between different lifestyle preferences' }; } // 🚨 NEW: Generate fallback analysis for Character and Combined modes private generateModeSpecificFallbackAnalysis(profile: Profile): void { let analysis: CompatibilityAnalysis; if (this.mode === 'character') { analysis = this.generateCharacterFallbackAnalysis(profile); } else if (this.mode === 'expectation') { analysis = this.generateCombinedFallbackAnalysis(profile); } else { analysis = this.generateGenericFallbackAnalysis(profile); } this.compatibilityAnalysis = analysis; } // 🚨 NEW: Character mode fallback analysis private generateCharacterFallback(profile: Profile): string { const characterScore = profile.character_score || 0; // Get structured character analysis using existing function const characterAnalysis = this.analyzeCharacterForSections(profile); return `

Based on personality color analysis, you have ${characterScore}% character compatibility with ${profile.name}.

Character Strengths
    ${characterAnalysis.strengths.map(s => `
  • ${s}
  • `).join('')}
Character Risks
    ${characterAnalysis.risks.map(r => `
  • ${r}
  • `).join('')}
Sacrifices Needed
    ${characterAnalysis.sacrifices.map(s => `
  • ${s}
  • `).join('')}
`; } // 🚨 NEW: Combined mode fallback analysis private generateCombinedFallbackAnalysis(profile: Profile): CompatibilityAnalysis { const exactMatches: string[] = []; const differences: FieldComparison[] = []; const missingData: string[] = []; const expectationScore = profile.expectation_score || 0; const characterScore = profile.character_score || 0; const overallScore = profile.match_score || 0; exactMatches.push(`✅ Overall Compatibility: ${overallScore}% combined score`); exactMatches.push(`✅ Expectation Alignment: ${expectationScore}% match with your criteria`); exactMatches.push(`✅ Character Compatibility: ${characterScore}% personality match`); if (expectationScore < characterScore) { differences.push({ fieldName: 'Primary Strength', yourExpectation: 'Expectation-focused alignment', theirProfile: 'Character-focused compatibility', isCritical: false }); } else if (characterScore < expectationScore) { differences.push({ fieldName: 'Primary Strength', yourExpectation: 'Character-focused alignment', theirProfile: 'Expectation-focused compatibility', isCritical: false }); } missingData.push('⚠️ Detailed combined analysis breakdown'); missingData.push('⚠️ Weighted scoring details'); const totalComparisons = exactMatches.length + differences.length; const compatibilityPercent = overallScore; return { exactMatches, differences, missingData, compatibilityPercent, totalExactMatches: exactMatches.length, totalDifferences: differences.length, totalMissingData: missingData.length }; } // 🚨 UPDATED: Expectation mode fallback (when backend fails) private generateExpectationFallbackAnalysis(profile: Profile): void { const exactMatches: string[] = []; const differences: FieldComparison[] = []; const missingData: string[] = []; // Simulate expectation-based analysis const expectationScore = profile.expectation_score || 0; exactMatches.push(`✅ Overall Expectation Match: ${expectationScore}% alignment`); // Simulate some field comparisons based on available profile data if (profile.city && this.userCity && profile.city === this.userCity) { exactMatches.push(`✅ Location: Both in ${profile.city}`); } else if (profile.city) { differences.push({ fieldName: 'Location', yourExpectation: this.userCity, theirProfile: profile.city, isCritical: true }); } if (profile.religion) { exactMatches.push(`✅ Religion: ${profile.religion}`); } else { missingData.push(`⚠️ Religion: Not specified in profile`); } if (profile.education_level) { exactMatches.push(`✅ Education: ${profile.education_level}`); } // Add some simulated differences for demonstration if (expectationScore < 80) { differences.push({ fieldName: 'Income Expectations', yourExpectation: 'As per your criteria', theirProfile: 'Different range', isCritical: false }); } const totalComparisons = exactMatches.length + differences.length; const compatibilityPercent = expectationScore; this.compatibilityAnalysis = { exactMatches, differences, missingData, compatibilityPercent, totalExactMatches: exactMatches.length, totalDifferences: differences.length, totalMissingData: missingData.length }; } //// Add this public getter to your component class //get modeDisplayName(): string { // switch (this.mode) { // case 'expectation-only': return 'Pure Expectation'; // case 'character': return 'Pure Character'; // case 'expectation': return 'Expectation + Character'; // default: return 'Compatibility'; // } //} private isActualMissingData(explanation: string): boolean { // Country "missing" messages are often just informational, not actual missing data if (explanation.toLowerCase().includes('country')) { return false; } // Real missing data would be fields that are completely empty/null if (explanation.includes('field') || explanation.includes('data not available')) { return true; } return false; } // 🚨 NEW: Generic fallback for unexpected modes private generateGenericFallbackAnalysis(profile: Profile): CompatibilityAnalysis { return { exactMatches: [`✅ Basic compatibility: ${profile.match_score || 0}% match`], differences: [], missingData: ['⚠️ Detailed analysis not available for current mode'], compatibilityPercent: profile.match_score || 0, totalExactMatches: 1, totalDifferences: 0, totalMissingData: 0 }; } // 🚨 UPDATED: Process backend explanations with proper parsing for new format private processCompatibilityAnalysis(explanations: string[]): void { const exactMatches: string[] = []; const differences: FieldComparison[] = []; const missingData: string[] = []; console.log('🔍 Raw explanations from backend:', explanations); explanations.forEach(explanation => { console.log('🔍 Processing explanation:', explanation); // Skip summary and header lines if (explanation.includes('📊') || explanation.includes('Compatibility') || explanation.includes('matches out of') || explanation.includes('Missing Profile Data') || explanation.includes('Exact Matches') || explanation.includes('Differences')) { return; } // Parse missing data if (explanation.includes('Profile missing')) { missingData.push(explanation); return; } // Parse exact matches from new format: "• Location: New York (Both match)" if (explanation.includes('(Both match)')) { // Extract field name and value from "• Location: New York (Both match)" const matchResult = explanation.match(/•\s*([^:]+):\s*([^(]+)\s*\(Both match\)/); if (matchResult && matchResult.length >= 3) { const fieldName = matchResult[1].trim(); const value = matchResult[2].trim(); // Store in pipe-separated format for frontend parsing exactMatches.push(`${fieldName}|${value}|${value}`); console.log('✅ Found exact match:', { fieldName, value }); } return; } // Parse differences from new format: "• Height: You want '180-190 cm', they have '175 cm'" if (explanation.includes("You want '") && explanation.includes("', they have '")) { // Extract from "• Height: You want '180-190 cm', they have '175 cm'" const diffResult = explanation.match(/•\s*([^:]+):\s*You want\s*'([^']+)',\s*they have\s*'([^']+)'/); if (diffResult && diffResult.length >= 4) { const fieldName = diffResult[1].trim(); const yourExpectation = diffResult[2].trim(); const theirProfile = diffResult[3].trim(); differences.push({ fieldName: this.formatFieldName(fieldName), yourExpectation: yourExpectation, theirProfile: theirProfile, isCritical: this.isCriticalField(fieldName, yourExpectation, theirProfile) }); console.log('✅ Found difference:', { fieldName, yourExpectation, theirProfile }); } return; } // Try to parse raw pipe-separated format directly (if backend sends it raw) if (explanation.includes('|') && !explanation.startsWith('•')) { const parts = explanation.split('|'); if (parts.length === 3) { const [fieldName, expectValue, profileValue] = parts.map(p => p.trim()); if (expectValue === profileValue) { exactMatches.push(explanation); console.log('✅ Found exact match (raw format):', { fieldName, expectValue }); } else { differences.push({ fieldName: this.formatFieldName(fieldName), yourExpectation: expectValue, theirProfile: profileValue, isCritical: this.isCriticalField(fieldName, expectValue, profileValue) }); console.log('✅ Found difference (raw format):', { fieldName, expectValue, profileValue }); } } return; } // Handle old format for backward compatibility if (explanation.includes('Profile matches your preference for')) { // Old format exact match const fieldMatch = explanation.match(/for\s+([^(]+)\s+\(/); const valueMatch = explanation.match(/\(([^)]+)\)/); if (fieldMatch && valueMatch) { const fieldName = fieldMatch[1].trim(); const value = valueMatch[1].trim(); exactMatches.push(`${fieldName}|${value}|${value}`); } } else if (explanation.includes('Profile differs from your preference for')) { // Old format difference const fieldMatch = explanation.match(/for\s+([^(]+)\s+\(/); const expectMatch = explanation.match(/you want:\s*([^,)]+)/); const profileMatch = explanation.match(/they are:\s*([^)]+)/); if (fieldMatch && expectMatch && profileMatch) { const fieldName = fieldMatch[1].trim(); const yourExpectation = expectMatch[1].trim(); const theirProfile = profileMatch[1].trim(); differences.push({ fieldName: this.formatFieldName(fieldName), yourExpectation: yourExpectation, theirProfile: theirProfile, isCritical: this.isCriticalField(fieldName, yourExpectation, theirProfile) }); } } }); const totalComparisons = exactMatches.length + differences.length; const compatibilityPercent = totalComparisons > 0 ? Math.round((exactMatches.length / totalComparisons) * 100) : 0; this.compatibilityAnalysis = { exactMatches, differences, missingData, compatibilityPercent, totalExactMatches: exactMatches.length, totalDifferences: differences.length, totalMissingData: missingData.length }; console.log('✅ Processed compatibility analysis:', { exactMatches: this.compatibilityAnalysis.exactMatches, differences: this.compatibilityAnalysis.differences, totalExactMatches: this.compatibilityAnalysis.totalExactMatches, totalDifferences: this.compatibilityAnalysis.totalDifferences }); } // Add this helper method to determine critical fields private isCriticalField(fieldName: string, yourExpectation: string, theirProfile: string): boolean { const criticalFields = [ 'location', 'religion', 'marital status', 'children', 'diet', 'smoking', 'alcohol', 'deal breakers' ]; const lowerFieldName = fieldName.toLowerCase(); // These are always critical if they don't match if (criticalFields.some(critical => lowerFieldName.includes(critical))) { return true; } // For deal breakers specifically if (lowerFieldName.includes('deal breaker')) { return yourExpectation !== theirProfile; } return false; } // 🚨 NEW: Parse difference explanation from backend format private parseDifferenceExplanation(explanation: string): FieldComparison | null { // Example backend format: "Profile differs from your preference for {field} (you want: {expectation}, they are: {profile})" const fieldMatch = explanation.match(/for\s+([^()]+)\s+\(/); const expectationMatch = explanation.match(/you want:\s*([^,]+)/); const profileMatch = explanation.match(/they are:\s*([^)]+)/); if (fieldMatch && expectationMatch && profileMatch) { const fieldName = fieldMatch[1].trim(); const yourExpectation = expectationMatch[1].trim(); const theirProfile = profileMatch[1].trim(); return { fieldName: this.formatFieldName(fieldName), yourExpectation: yourExpectation, theirProfile: theirProfile, isCritical: explanation.includes('🚫') || fieldName.toLowerCase().includes('location') || fieldName.toLowerCase().includes('religion') }; } return null; } // 🚨 NEW: Format field names for better display private formatFieldName(fieldName: string): string { const fieldMap: { [key: string]: string } = { 'location': 'Location', 'religion': 'Religion', 'age': 'Age Range', 'marital status': 'Marital Status', 'education level': 'Education Level', 'employment status': 'Employment Status', 'income range': 'Income Range', 'family type': 'Family Type', 'diet': 'Dietary Preference', 'smoking': 'Smoking Habits', 'alcohol': 'Alcohol Consumption', 'fitness level': 'Fitness Level', 'conflict style': 'Conflict Resolution', 'financial style': 'Financial Management', 'hobbies': 'Hobbies & Interests', 'travel': 'Travel Preference', 'pets': 'Pet Ownership', 'children': 'Children Timeline', 'adoption': 'Open to Adoption', 'relocation': 'Relocation Willingness', 'health': 'Health Considerations', 'skin tone': 'Skin Tone', 'daily routine': 'Daily Routine', 'family communication': 'Family Communication', 'relaxation': 'Relaxation Preferences', 'career': 'Career Aspirations', 'in-laws': 'Living with In-Laws', 'parents': 'Living with Parents', 'financial support': 'Financial Support to Parents' }; return fieldMap[fieldName.toLowerCase()] || this.capitalizeFirstLetter(fieldName); } private capitalizeFirstLetter(text: string): string { return text.charAt(0).toUpperCase() + text.slice(1); } toggleExactMatches(): void { this.showExactMatches = true; this.showDifferences = false; } toggleDifferences(): void { this.showExactMatches = false; this.showDifferences = true; } // 🚨 UPDATED: Enhanced compatibility report generation getCompatibilityReport(profile: Profile): string { if (!profile.explanations || profile.explanations.length === 0) { return this.generateFallbackExplanation(profile); } return this.generateDetailedExplanation(profile); } private generateFallbackExplanation(profile: Profile): string { const score = profile.match_score || 0; let explanation = ''; switch (this.mode) { case 'expectation-only': explanation = this.generateExpectationFallback(profile); break; case 'character': explanation = this.generateCharacterFallback(profile); break; case 'expectation': explanation = this.generateCombinedFallback(profile); break; default: explanation = this.generateGenericFallback(profile); } return `

Compatibility Analysis (${this.getModeDisplayName()}):

${explanation}

Score: ${score}% compatibility

`; } private generateDetailedExplanation(profile: Profile): string { const score = profile.match_score || 0; const sourceType = profile.explanation_source || 'unknown'; const explanations = profile.explanations || []; return `

${this.getModeDisplayName()} Analysis (${this.getSourceDisplayName(sourceType)}):

    ${explanations.map(exp => `
  • ${exp}
  • `).join('')}

Overall Score: ${score}%

`; } // Mode-specific fallback explanations private generateExpectationFallback(profile: Profile): string { const expectationScore = profile.expectation_score || 0; return `

Based on your relationship expectations and preferences, you have ${expectationScore}% expectation alignment with ${profile.name}.

Detailed field-by-field analysis shows specific matches and differences.

`; } /*private generateCharacterFallback(profile: Profile): string { const characterScore = profile.character_score || 0; const colorAnalysis = this.analyzeColorTraits(profile); return `

Based on personality color analysis, you have ${characterScore}% character compatibility with ${profile.name}.

  • ${colorAnalysis.dominantMatch}
  • ${colorAnalysis.complementaryTraits}
  • ${colorAnalysis.potentialDynamics}
`; }*/ private generateCombinedFallback(profile: Profile): string { const expectationScore = profile.expectation_score || 0; const characterScore = profile.character_score || 0; return `

This match combines both expectation alignment and character compatibility:

  • Expectation Match: ${expectationScore}% - Based on your relationship criteria and preferences
  • Character Match: ${characterScore}% - Based on personality color compatibility
  • Final Score: ${profile.match_score}% - Weighted combination of both factors

The system prioritizes expectation alignment while considering character traits for refined matching.

`; } private generateGenericFallback(profile: Profile): string { return `

Based on your profile data, you have ${profile.match_score}% compatibility with ${profile.name}.

This comprehensive match considers multiple factors including personality traits, expectations, and lifestyle preferences.

`; } // Helper methods public getModeDisplayName(): string { switch (this.mode) { case 'expectation-only': return 'Pure Expectation'; case 'character': return 'Pure Character'; case 'expectation': return 'Expectation + Character'; default: return 'Compatibility'; } } private getSourceDisplayName(source: string): string { switch (source) { case 'expectation': return 'Expectation Analysis'; case 'character': return 'Character Analysis'; case 'llm': return 'AI Analysis'; default: return 'System Analysis'; } } private analyzeColorTraits(profile: Profile): { dominantMatch: string, complementaryTraits: string, potentialDynamics: string } { const colors = [ { name: 'Blue', value: profile.blue || 0, trait: 'analytical and structured' }, { name: 'Green', value: profile.green || 0, trait: 'organized and process-oriented' }, { name: 'Yellow', value: profile.yellow || 0, trait: 'creative and visionary' }, { name: 'Red', value: profile.red || 0, trait: 'decisive and action-driven' } ]; const dominantColor = colors.reduce((prev, current) => (prev.value > current.value) ? prev : current ); let dynamics = ''; if (dominantColor.value > 40) { dynamics = `Strong ${dominantColor.name} dominance suggests ${dominantColor.trait} approach to relationships`; } else { dynamics = 'Balanced color profile indicates adaptable and well-rounded personality'; } return { dominantMatch: `Dominant ${dominantColor.name} traits (${dominantColor.value}%)`, complementaryTraits: `Personality shows ${dominantColor.trait} characteristics`, potentialDynamics: dynamics }; } // MISSING METHODS - Add these to fix the template errors /** * Extract field name from match string */ /** * Extract field name from match string */ getFieldNameFromMatch(match: string): string { if (!match) return 'Unknown Field'; try { console.log('🔍 Parsing field name from:', match); // NEW BACKEND FORMAT: "field|expect_value|profile_value" // Example: "Fitness|Moderate|Moderate" → should return "Fitness" if (match.includes('|')) { const parts = match.split('|'); if (parts.length >= 1) { const fieldName = parts[0].trim(); console.log('✅ Extracted field name from pipe format:', fieldName); return this.formatFieldName(fieldName); } } // OLD BACKEND FORMAT: "• Profile matches your preference for location (New York)" if (match.includes('Profile matches your preference for')) { const fieldMatch = match.match(/for\s+([^(]+)\s+\(/); if (fieldMatch && fieldMatch[1]) { const fieldName = fieldMatch[1].trim(); console.log('✅ Extracted field name from "Profile matches" format:', fieldName); return this.formatFieldName(fieldName); } } // FORMAT: "• Location: New York (Both match)" if (match.includes(':') && match.includes('(Both match)')) { const colonIndex = match.indexOf(':'); if (colonIndex > -1) { // Remove the bullet point if present let fieldName = match.substring(0, colonIndex).trim(); fieldName = fieldName.replace('•', '').trim(); console.log('✅ Extracted field name from "Both match" format:', fieldName); return this.formatFieldName(fieldName); } } // Simple format: "✅ Location: Both in New York" const colonIndex = match.indexOf(':'); if (colonIndex > -1) { let fieldName = match.substring(0, colonIndex).trim(); // Remove emojis and symbols fieldName = fieldName.replace(/[✅❌🎯👤•]/g, '').trim(); console.log('✅ Extracted field name from colon format:', fieldName); return this.formatFieldName(fieldName); } // Fallback: return the match as-is (should be field name) console.log('⚠️ Could not parse field name, formatting raw:', match); return this.formatFieldName(match); } catch (error) { console.error('Error parsing field name from match:', error); return 'Field'; } } /** * Extract expectation value from match string */ getExpectationValueFromMatch(match: string): string { if (!match) return 'Not specified'; try { console.log('🔍 Parsing expectation value from:', match); // NEW BACKEND FORMAT: "field|expect_value|profile_value" // Example: "Diet|Vegetarian|Vegetarian" or "Location|New York|New York" if (match.includes('|')) { const parts = match.split('|'); if (parts.length >= 2) { const value = parts[1].trim(); console.log('✅ Extracted value from pipe format:', value); return value; } } // OLD BACKEND FORMAT: "• Profile matches your preference for location (New York)" const parenthesesMatch = match.match(/\(([^)]+)\)/); if (parenthesesMatch && parenthesesMatch[1]) { const value = parenthesesMatch[1].trim(); console.log('✅ Extracted value from parentheses:', value); return value; } // OLD BACKEND FORMAT: "Profile matches your preference for location (you want: New York, they are: New York)" if (match.includes('you want:')) { const expectationMatch = match.match(/you want:\s*([^,)]+)/); if (expectationMatch && expectationMatch[1]) { const value = expectationMatch[1].trim(); console.log('✅ Extracted value from "you want:" format:', value); return value; } } // SIMPLE FORMAT: "✅ Location: Both in New York" const colonIndex = match.indexOf(':'); if (colonIndex > -1) { const value = match.substring(colonIndex + 1).trim(); console.log('✅ Extracted value from colon format:', value); return value; } console.log('⚠️ Could not parse value, returning raw:', match); return match; } catch (error) { console.error('Error parsing expectation value from match:', error); return 'Match found'; } } getProfileValueFromMatch(match: string): string { if (!match) return 'Not specified'; try { console.log('🔍 Parsing profile value from:', match); // NEW BACKEND FORMAT: "field|expect_value|profile_value" if (match.includes('|')) { const parts = match.split('|'); if (parts.length >= 3) { const value = parts[2].trim(); console.log('✅ Extracted profile value from pipe format:', value); return value; } } // OLD BACKEND FORMAT: "Profile matches your preference for location (you want: New York, they are: New York)" if (match.includes('they are:')) { const profileMatch = match.match(/they are:\s*([^)]+)/); if (profileMatch && profileMatch[1]) { const value = profileMatch[1].trim(); console.log('✅ Extracted profile value from "they are:" format:', value); return value; } } // For exact matches, the value is the same as expectation const expectationValue = this.getExpectationValueFromMatch(match); console.log('✅ Using expectation value as profile value:', expectationValue); return expectationValue; } catch (error) { console.error('Error parsing profile value from match:', error); return 'Match found'; } } // Photo distribution methods private getConsistentPhoto(userId: number, gender?: string): string { if (this.userPhotoMap.has(userId)) { return this.userPhotoMap.get(userId)!; } const profileGender = (gender || 'male').toLowerCase().trim(); const isFemale = profileGender === 'female' || profileGender === 'f' || profileGender.includes('female') || profileGender.includes('woman') || profileGender.includes('girl'); const photoPool = isFemale ? this.indianFemalePhotos : this.indianMalePhotos; const poolSize = photoPool.length; let selectedIndex = -1; let attempts = 0; const maxAttempts = poolSize * 2; while (attempts < maxAttempts) { let candidateIndex = Math.floor(Math.random() * poolSize); if (this.usedPhotoIndices.size === 0) { selectedIndex = candidateIndex; break; } const isSuitable = this.isPhotoSuitable(candidateIndex, isFemale, poolSize); if (isSuitable) { selectedIndex = candidateIndex; break; } attempts++; if (attempts >= maxAttempts / 2) { for (let i = 0; i < poolSize; i++) { if (this.isPhotoSuitable(i, isFemale, poolSize)) { selectedIndex = i; break; } } if (selectedIndex !== -1) break; } } if (selectedIndex === -1) { selectedIndex = this.getLeastRecentlyUsedIndex(isFemale, poolSize); } this.usedPhotoIndices.add(selectedIndex); this.updateRecentIndices(selectedIndex, isFemale); if (isFemale) { this.lastUsedFemaleIndex = selectedIndex; } else { this.lastUsedMaleIndex = selectedIndex; } const selectedPhoto = photoPool[selectedIndex]; this.userPhotoMap.set(userId, selectedPhoto); return selectedPhoto; } private isPhotoSuitable(candidateIndex: number, isFemale: boolean, poolSize: number): boolean { const recentIndices = isFemale ? this.recentFemaleIndices : this.recentMaleIndices; const lastUsedIndex = isFemale ? this.lastUsedFemaleIndex : this.lastUsedMaleIndex; if (recentIndices.includes(candidateIndex)) { return false; } if (candidateIndex === lastUsedIndex) { return false; } if (this.usedPhotoIndices.size >= poolSize * 0.7) { return !recentIndices.slice(-3).includes(candidateIndex); } return true; } private updateRecentIndices(index: number, isFemale: boolean): void { const recentIndices = isFemale ? this.recentFemaleIndices : this.recentMaleIndices; recentIndices.push(index); if (recentIndices.length > this.MAX_RECENT_INDICES) { recentIndices.shift(); } if (isFemale) { this.recentFemaleIndices = recentIndices; } else { this.recentMaleIndices = recentIndices; } } private getLeastRecentlyUsedIndex(isFemale: boolean, poolSize: number): number { const recentIndices = isFemale ? this.recentFemaleIndices : this.recentMaleIndices; for (let i = 0; i < poolSize; i++) { if (!recentIndices.includes(i)) { return i; } } return recentIndices[0] || 0; } public resetPhotoTracking(): void { this.usedPhotoIndices.clear(); this.recentMaleIndices = []; this.recentFemaleIndices = []; this.lastUsedMaleIndex = -1; this.lastUsedFemaleIndex = -1; } // Filter methods onSearchChange(): void { console.log('Search term:', this.searchTerm); } // Update the applyFilters method to handle all modes properly applyFilters(): void { // First, reprocess matches for the current mode this.processMatchesForDisplay(); // Then apply the range filter if (this.selectedRange && this.selectedRange !== 'all') { this.displayedMatches = this.displayedMatches.filter(profile => { const score = profile.match_score || 0; switch (this.selectedRange) { case '90-100': return score >= 90 && score <= 100; case '80-89': return score >= 80 && score < 90; case '70-79': return score >= 70 && score < 80; case '60-69': return score >= 60 && score < 70; case 'below_60': return score < 60; default: return true; } }); } // Apply other filters if (this.selectedLocation) { this.displayedMatches = this.displayedMatches.filter(profile => profile.city === this.selectedLocation ); } if (this.selectedProfession) { this.displayedMatches = this.displayedMatches.filter(profile => profile.job_role === this.selectedProfession ); } if (this.selectedEducation) { this.displayedMatches = this.displayedMatches.filter(profile => profile.education_level === this.selectedEducation ); } if (this.selectedReligion) { this.displayedMatches = this.displayedMatches.filter(profile => profile.religion === this.selectedReligion ); } if (this.selectedAgeRange) { this.displayedMatches = this.displayedMatches.filter(profile => { if (!profile.age) return false; const [minAge, maxAge] = this.selectedAgeRange.split('-').map(Number); return profile.age >= minAge && profile.age <= maxAge; }); } } // Update the mode switching methods to preserve and reapply filters /*sortByExpectationOnly(): void { if (!this.userId) return; this.loading = true; this.error = null; this.mode = 'expectation-only'; this.matchingService .getMatches(this.userId, { role: 'marriage', limit: 10, excludeSelf: true, mode: 'expectation-only' }) .subscribe({ next: (res: ApiMatchResponse) => { this.processMatches(res, 'expectation-only'); // Reapply filters after mode change setTimeout(() => this.applyFilters(), 0); }, error: (err: any) => { this.error = err?.error?.error || 'Failed to fetch expectation matches.'; this.loading = false; }, complete: () => (this.loading = false), }); } */ clearFilters(): void { this.selectedLocation = ''; this.selectedProfession = ''; this.selectedEducation = ''; this.selectedReligion = ''; this.selectedAgeRange = ''; this.selectedRange = 'all'; // Reprocess matches and reset to show all this.processMatchesForDisplay(); } // Profile methods closeModal(): void { this.showProfileModal = false; this.selectedProfile = null; this.showUserInfo = false; this.compatibilityAnalysis = null; this.showExactMatches = false; this.showDifferences = true; } openEditModal(): void { this.loadUserProfileData(this.userId!); this.showUserProfileModal = true; } editProfile(): void { if (this.userId) { this.router.navigate(['/question-answer'], { queryParams: { role: 'marriage', user_id: this.userId } }); } } closeUserProfileModal(): void { this.showUserProfileModal = false; } closeEditModal(): void { this.showEditModal = false; } saveProfile(): void { this.userName = this.editName; this.userCity = this.editCity; this.closeEditModal(); } // Like functionality toggleLike(profile: Profile): void { const index = this.likedProfiles.findIndex(p => p.user_id === profile.user_id); if (index > -1) { this.likedProfiles.splice(index, 1); } else { this.likedProfiles.push(profile); } } isLiked(profile: Profile): boolean { return this.likedProfiles.some(p => p.user_id === profile.user_id); } toggleLikedDropdown(event: Event): void { event.stopPropagation(); this.showLikedDropdown = !this.showLikedDropdown; } // User info methods toggleUserInfo(): void { this.showUserInfo = !this.showUserInfo; if (this.showUserInfo && this.userId && !this.userProfileData) { this.loadUserProfileData(this.userId); } } loadUserProfileData(userId: number): void { this.loading = true; this.matchingService.getMarriageProfile(userId).subscribe({ next: (profile: any) => { this.userProfileData = profile; // Update user details if available if (profile) { this.userName = profile.full_name || this.userName; this.userCity = profile.current_city || this.userCity; } this.loading = false; }, error: (err: any) => { console.error('Failed to load user profile data:', err); this.loading = false; // Show fallback data this.userProfileData = { full_name: this.userName, current_city: this.userCity }; } }); } // Chat functionality openChat(profile: Profile): void { this.currentChatUser = profile; this.chatMessages = [ { sender: 'them', text: 'Hello! How are you?', time: '10:00 AM' }, { sender: 'me', text: 'I\'m good, thanks! How about you?', time: '10:01 AM' } ]; this.showChatWindow = true; } closeChat(): void { this.showChatWindow = false; this.currentChatUser = null; this.chatMessages = []; } sendMessage(): void { if (this.newMessage.trim()) { const now = new Date(); const timeString = now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); this.chatMessages.push({ sender: 'me', text: this.newMessage, time: timeString }); this.newMessage = ''; setTimeout(() => { this.showTyping = true; setTimeout(() => { this.showTyping = false; this.chatMessages.push({ sender: 'them', text: 'That\'s interesting! Tell me more.', time: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) }); }, 2000); }, 1000); } } typingIndicator(): void { // Typing indicator logic } // Utility methods handleImageError(event: any, profile: Profile): void { event.target.src = 'https://cdn-icons-png.flaticon.com/512/3135/3135715.png'; } get filteredGroups() { if (this.selectedRange === 'all') { const order = ['90-100', '80-89', '70-79', '60-69', 'below_60']; return this.groupedMatches .filter(g => g.items.length > 0) .sort((a, b) => order.indexOf(a.range) - order.indexOf(b.range)); } return this.groupedMatches.filter((g) => g.range === this.selectedRange && g.items.length > 0); } clearPhotoCache(): void { this.userPhotoMap.clear(); this.usedPhotos.clear(); this.resetPhotoTracking(); } }