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';
  }
}