Py-detect / src /app /validationpage /validationpage.component.ts
pykara's picture
fix
73566f6
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { CaseStoreService, PoliceCase } from '../shared/case-store.service';
// For PDF generation
// import jsPDF from 'jspdf';
@Component({
selector: 'app-validationpage',
templateUrl: './validationpage.component.html',
styleUrls: ['./validationpage.component.css']
})
export class ValidationpageComponent implements OnInit {
truePercentage: number = 72;
falsePercentage: number = 28;
modalOpen: boolean = false;
reportDate: string = new Date().toLocaleString('default', { month: 'long', year: 'numeric' });
// Dashboard metrics (real-time from CaseStoreService)
sessionCount: number = 0;
officerCount: number = 0;
suspectCount: number = 0;
avgSessionDuration: string = '';
analysisCount: number = 0;
consistencyIndex: number = 0;
audioVideoCount: number = 0;
reportCount: number = 0;
// Donut chart (officer session distribution)
officer1Percent: number = 0;
officer2Percent: number = 0;
officer3Percent: number = 0;
// Audio/Video metrics (current values will be animated)
audioMetric1: number = 0; // Speech
audioMetric2: number = 0; // Eye Contact
audioMetric3: number = 0; // Emotion
audioMetric4: number = 0; // Clarity
audioMetric5: number = 0; // Confidence
// Targets for animations
private audioMetric1Target = 0;
private audioMetric2Target = 0;
private audioMetric3Target = 0;
private audioMetric4Target = 0;
private audioMetric5Target = 0;
// Outcome distribution
outcome1: number = 0; // Consistent
outcome2: number = 0; // Needs Review
outcome3: number = 0; // Inconsistent
// Track which section/tab is selected
selectedSection: string = 'Dashboard';
// Analysis overview scores (for radial diagram) - displayed values
audioAnalysisScore: number = 0;
videoAnalysisScore: number = 0;
verifiedScore: number = 0;
// Analysis targets (final percentages to animate to)
audioAnalysisTarget: number = 0;
videoAnalysisTarget: number = 0;
verifiedTarget: number = 0;
// New: truthness/confidence fields used by template
audioTruthness: number = 74;
videoTruthness: number = 72;
verifiedConfidence: number = 73;
// SVG circle radii and computed values
r1 = 80; // outer (blue)
r2 = 64; // middle (green)
r3 = 48; // inner (red)
circumference1 = 2 * Math.PI * this.r1;
circumference2 = 2 * Math.PI * this.r2;
circumference3 = 2 * Math.PI * this.r3;
// stroke-dashoffset values (start hidden = full circumference)
offset1 = this.circumference1;
offset2 = this.circumference2;
offset3 = this.circumference3;
constructor(private router: Router, private caseStore: CaseStoreService) {
// Get latest cases
const cases = this.caseStore.getPoliceCases();
this.sessionCount = cases.length;
this.officerCount = Array.from(new Set(cases.map(c => c.police?.name))).length;
this.suspectCount = Array.from(new Set(cases.map(c => c.accused?.name))).length;
this.analysisCount = cases.length;
this.reportCount = cases.length;
// Dummy: avgSessionDuration
this.avgSessionDuration = '00:42:18';
// Dummy: audio/video count
this.audioVideoCount = cases.length;
// Dummy: consistencyIndex (use a fallback value, or derive from available case data)
this.consistencyIndex = Math.round((cases.reduce((sum, c) => sum + 72, 0) / (cases.length || 1)));
// Donut chart: officer session distribution
const officerSessions = cases.reduce((acc, c) => {
const name = c.police?.name || 'Unknown';
acc[name] = (acc[name] || 0) + 1;
return acc;
}, {} as { [name: string]: number });
const officerNames = Object.keys(officerSessions);
const totalSessions = cases.length || 1;
this.officer1Percent = Math.round((officerSessions[officerNames[0]] || 0) * 100 / totalSessions);
this.officer2Percent = Math.round((officerSessions[officerNames[1]] || 0) * 100 / totalSessions);
this.officer3Percent = Math.round((officerSessions[officerNames[2]] || 0) * 100 / totalSessions);
// Dummy: audio/video metric targets
this.audioMetric1Target = 68;
this.audioMetric2Target = 75;
this.audioMetric3Target = 60;
this.audioMetric4Target = 85;
this.audioMetric5Target = 82;
// Dummy: outcome distribution
this.outcome1 = 60;
this.outcome2 = 25;
this.outcome3 = 15;
// true/false percentage from router state
const nav = this.router.getCurrentNavigation();
const state = nav?.extras?.state as { truePercentage?: number; falsePercentage?: number };
this.truePercentage = state?.truePercentage ?? 0;
this.falsePercentage = state?.falsePercentage ?? 0;
// compute overview targets (do NOT set displayed scores yet)
this.computeOverviewScores();
}
ngOnInit(): void {
// Start load animations
this.animateLoad();
}
private animateLoad() {
const duration = 3000; // ms — slow 3s animation per request
const start = performance.now();
const animate = (now: number) => {
const t = Math.min(1, (now - start) / duration);
// ease-out
const ease = 1 - Math.pow(1 - t, 3);
// animate metric bars from their targets
this.audioMetric1 = Math.round(this.audioMetric1Target * ease);
this.audioMetric2 = Math.round(this.audioMetric2Target * ease);
this.audioMetric3 = Math.round(this.audioMetric3Target * ease);
this.audioMetric4 = Math.round(this.audioMetric4Target * ease);
this.audioMetric5 = Math.round(this.audioMetric5Target * ease);
// interpolate displayed radial scores from0 to targets
this.audioAnalysisScore = Math.round(this.audioAnalysisTarget * ease);
this.videoAnalysisScore = Math.round(this.videoAnalysisTarget * ease);
this.verifiedScore = Math.round(this.verifiedTarget * ease);
// Also update the truthness/confidence displayed details
this.audioTruthness = this.audioAnalysisScore;
this.videoTruthness = this.videoAnalysisScore;
this.verifiedConfidence = this.verifiedScore;
// update SVG offsets so stroke animates from full circumference to target offset
this.offset1 = Math.round(this.circumference1 * (1 - this.audioAnalysisScore / 100));
this.offset2 = Math.round(this.circumference2 * (1 - this.videoAnalysisScore / 100));
this.offset3 = Math.round(this.circumference3 * (1 - this.verifiedScore / 100));
if (t < 1) {
requestAnimationFrame(animate);
}
};
// ensure offsets start hidden (full circumference)
this.offset1 = this.circumference1;
this.offset2 = this.circumference2;
this.offset3 = this.circumference3;
requestAnimationFrame(animate);
}
computeOverviewScores() {
// Audio analysis target: average of audio metric targets
const audioVals = [this.audioMetric1Target, this.audioMetric2Target, this.audioMetric3Target, this.audioMetric4Target, this.audioMetric5Target].filter(v => typeof v === 'number');
this.audioAnalysisTarget = audioVals.length ? Math.round(audioVals.reduce((a, b) => a + b, 0) / audioVals.length) : 0;
// Video analysis target: use consistencyIndex as proxy or derive from other available metrics
this.videoAnalysisTarget = this.consistencyIndex || Math.round((this.audioMetric2Target + this.audioMetric3Target + this.audioMetric4Target) / 3);
// Verified target: derive from truePercentage if available, else average of audio/video targets
const v = this.truePercentage || Math.round((this.audioAnalysisTarget + this.videoAnalysisTarget) / 2);
this.verifiedTarget = Math.round(v);
// Do not set displayed scores or offsets here; animateLoad will handle the animated transition
}
percentToOffset(percent: number, circumference: number) {
const pct = Math.max(0, Math.min(100, percent));
return Math.round(circumference * (1 - pct / 100));
}
selectSection(section: string) {
this.selectedSection = section;
}
navigateBackToCaseDetails() {
this.router.navigate(['/case-details']);
}
openModal() {
this.modalOpen = true;
}
closeModal() {
this.modalOpen = false;
}
downloadPDF() {
// Example: Use jsPDF to generate PDF
// const doc = new jsPDF();
// doc.text('Investigation Report', 10, 10);
// doc.text(`Pass Percentage: ${this.truePercentage}%`, 10, 30);
// doc.text(`Fail Percentage: ${this.falsePercentage}%`, 10, 40);
// doc.save('investigation-report.pdf');
alert('PDF download functionality to be implemented.');
}
emailReport() {
// Example: Use mailto or backend API
window.location.href = `mailto:?subject=Investigation Report&body=Pass Percentage: ${this.truePercentage}%\nFail Percentage: ${this.falsePercentage}%`;
}
navigateHome() {
this.router.navigate(['/']);
}
navigateBackToPyDetect() {
this.router.navigate(['/py-detect']);
}
reAnalyze() {
// TODO: Implement re-analysis logic
alert('Re-Analyze Audio/Video functionality to be implemented.');
}
// Navigate to question summary page
goToQuestionSummary() {
this.router.navigate(['/question-summary']);
}
getMetricGradient(metricValue: number): string {
const p = Math.max(0, Math.min(100, Math.round(metricValue)));
if (p >= 80) return 'linear-gradient(90deg, #10b981, #34d399)'; // green
if (p >= 60) return 'linear-gradient(90deg, #3b82f6, #60a5fa)'; // blue
if (p >= 40) return 'linear-gradient(90deg, #f59e0b, #f97316)'; // amber
return 'linear-gradient(90deg, #ef4444, #fb7185)'; // red
}
getMetricTextColor(metricValue: number): string {
const p = Math.max(0, Math.min(100, Math.round(metricValue)));
return p >= 50 ? '#ffffff' : '#0f172a';
}
}