File size: 9,741 Bytes
73566f6 463dbc3 73566f6 24822a7 463dbc3 73566f6 24822a7 463dbc3 73566f6 463dbc3 73566f6 463dbc3 73566f6 24822a7 463dbc3 24822a7 73566f6 463dbc3 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 |
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';
}
}
|