-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathindex.tsx
More file actions
807 lines (689 loc) · 32.2 KB
/
Copy pathindex.tsx
File metadata and controls
807 lines (689 loc) · 32.2 KB
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
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { GoogleGenAI } from '@google/genai';
import { marked } from 'marked';
// Tell TypeScript that the mermaid global exists.
declare const mermaid: any;
const API_KEY = process.env.API_KEY;
// File Upload Limits
const MAX_FILES = 10;
const MAX_TOTAL_SIZE = 20 * 1024 * 1024; // 20 MB
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const analyzeButton = document.getElementById(
'analyze-button'
) as HTMLButtonElement;
const fileList = document.getElementById('file-list') as HTMLUListElement;
const fileSummary = document.getElementById('file-summary') as HTMLDivElement;
const fileErrorMessage = document.getElementById(
'file-error-message'
) as HTMLDivElement;
// Main containers
const thinkingContainer = document.getElementById(
'thinking-container'
) as HTMLElement;
const analysisResultsContainer = document.getElementById(
'analysis-results'
) as HTMLElement;
const searchAnalysisContainer = document.getElementById(
'search-analysis-container'
) as HTMLElement;
// Tab elements
const tabButtons = document.querySelectorAll('.tab-button');
const tabPanels = document.querySelectorAll('.tab-panel');
// Summary Tab elements
const summaryVerdict = document.getElementById('summary-verdict') as HTMLElement;
const summaryRedFlags = document.getElementById(
'summary-red-flags'
) as HTMLElement;
const downloadRedFlagsButton = document.getElementById(
'download-red-flags-button'
) as HTMLButtonElement;
const riskGaugeFill = document.getElementById('risk-gauge-fill') as HTMLElement;
const riskLabel = document.getElementById('risk-label') as HTMLElement;
// Threat Vector Gauges
const threatGauges = {
'Psychological Tactics': {
fill: document.getElementById('psych-gauge') as HTMLElement,
score: document.getElementById('psych-score') as HTMLElement,
},
'Technical Red Flags': {
fill: document.getElementById('tech-gauge') as HTMLElement,
score: document.getElementById('tech-score') as HTMLElement,
},
'Financial Scrutiny': {
fill: document.getElementById('finance-gauge') as HTMLElement,
score: document.getElementById('finance-score') as HTMLElement,
},
'Communication Analysis': {
fill: document.getElementById('comm-gauge') as HTMLElement,
score: document.getElementById('comm-score') as HTMLElement,
},
};
// Details Tab elements
const detailsTabPanel = document.getElementById('details-tab') as HTMLElement;
// Workflow Diagram Tab elements
const workflowDiagramContainer = document.getElementById(
'workflow-diagram-container'
) as HTMLElement;
// Search elements (now global)
const searchInput = document.getElementById('search-input') as HTMLInputElement;
const searchButton = document.getElementById(
'search-button'
) as HTMLButtonElement;
// Final analysis elements
const finalAnalysisSection = document.getElementById(
'final-analysis-section'
) as HTMLElement;
const analyzeCombinedButton = document.getElementById(
'analyze-combined-button'
) as HTMLButtonElement;
const whistleblowerContainer = document.getElementById(
'whistleblower-container'
) as HTMLElement;
const downloadReportButton = document.getElementById(
'download-report-button'
) as HTMLButtonElement;
let selectedFiles: File[] = [];
let fullFileAnalysisResponse = '';
let lastSearchResult = '';
let whistleblowerReportContent = '';
let redFlagsContent = '';
// Initialize Mermaid.js for manual rendering.
mermaid.initialize({ startOnLoad: false, theme: 'default' });
/**
* A centralized function to handle and display errors in a user-friendly way.
* @param error The error object.
* @param container The HTML element to display the error message in.
* @param title A title for the error message container.
*/
function handleApiError(
error: unknown,
container: HTMLElement,
title = 'Operation Failed'
) {
console.error(error); // Keep logging for developers
const e = error as Error;
let friendlyMessage = `<p><strong>An unexpected error occurred.</strong> Please try again later.</p>`;
if (e && e.message) {
const message = e.message.toLowerCase();
if (message.includes('api key')) {
// User-facing message for what is likely a server-side config issue.
friendlyMessage = `<p><strong>Connection Error:</strong> Could not connect to the analysis service. This is not an issue on your end. Please try again in a few moments.</p>`;
} else if (
message.includes('failed to fetch') ||
message.includes('network request failed')
) {
friendlyMessage = `<p><strong>Network Error:</strong> Could not reach the analysis service. Please check your internet connection and try again.</p>`;
} else if (
message.includes('deadline exceeded') ||
message.includes('timeout')
) {
friendlyMessage = `<p><strong>Request Timeout:</strong> The analysis took too long to complete. This can happen with very large files or a slow network. Please try again.</p>`;
} else if (
message.includes('read file') ||
message.includes('process file')
) {
// Catches errors from our file reader logic.
friendlyMessage = `<p><strong>File Processing Error:</strong> There was a problem reading one of your files. Please ensure it is not corrupted and is a supported file type. <br><small>Details: ${e.message}</small></p>`;
} else if (message.includes('400') || message.includes('invalid argument')) {
// This indicates a bad request, possibly due to unsupported content.
friendlyMessage = `<p><strong>Invalid Request:</strong> The server could not process the request. This may be due to an unsupported file format or content within the files. Please check your files and try again.</p>`;
} else if (message.includes('500') || message.includes('internal error')) {
friendlyMessage = `<p><strong>Server Error:</strong> The analysis service encountered a temporary problem. We've been notified and are working on it. Please try again in a few minutes.</p>`;
} else {
// A fallback for other specific Gemini errors that are not network/server related.
friendlyMessage = `<p><strong>Analysis Failed:</strong> An error occurred while processing your request. <br><small>Details: ${e.message}</small></p>`;
}
}
container.innerHTML = `<div class="error-container"><h2>${title}</h2>${friendlyMessage}</div>`;
}
fileInput.addEventListener('change', () => {
const files = Array.from(fileInput.files ?? []);
// Reset UI
fileList.innerHTML = '';
fileSummary.textContent = '';
fileErrorMessage.style.display = 'none';
selectedFiles = [];
analyzeButton.disabled = true;
if (files.length > MAX_FILES) {
fileErrorMessage.textContent = `You can select a maximum of ${MAX_FILES} files.`;
fileErrorMessage.style.display = 'block';
fileInput.value = '';
return;
}
const totalSize = files.reduce((acc, file) => acc + file.size, 0);
if (totalSize > MAX_TOTAL_SIZE) {
const totalSizeMB = (totalSize / 1024 / 1024).toFixed(2);
const maxSizeMB = MAX_TOTAL_SIZE / 1024 / 1024;
fileErrorMessage.textContent = `Total file size cannot exceed ${maxSizeMB} MB. Your selection is ${totalSizeMB} MB.`;
fileErrorMessage.style.display = 'block';
fileInput.value = '';
return;
}
selectedFiles = files;
if (selectedFiles.length > 0) {
selectedFiles.forEach((file) => {
const listItem = document.createElement('li');
listItem.textContent = file.name;
fileList.appendChild(listItem);
});
const totalSizeFormatted =
totalSize / 1024 / 1024 < 0.1
? `${(totalSize / 1024).toFixed(2)} KB`
: `${(totalSize / 1024 / 1024).toFixed(2)} MB`;
fileSummary.textContent = `${selectedFiles.length} file(s) selected (${totalSizeFormatted})`;
analyzeButton.disabled = false;
}
});
analyzeButton.addEventListener('click', async () => {
if (selectedFiles.length === 0) return;
// UI setup
analyzeButton.disabled = true;
searchButton.disabled = true;
thinkingContainer.innerHTML =
'<h2>Analyzing Files...</h2><div class="loader"></div>';
thinkingContainer.classList.remove('hidden');
analysisResultsContainer.classList.add('hidden');
searchAnalysisContainer.classList.add('hidden'); // Also hide this
finalAnalysisSection.classList.add('hidden');
downloadRedFlagsButton.classList.add('hidden'); // Reset download button
fullFileAnalysisResponse = ''; // Reset previous analysis
redFlagsContent = ''; // Reset red flags content
try {
const ai = new GoogleGenAI({ apiKey: API_KEY });
const fileToGenerativePart = (file: File) => {
return new Promise<{ inlineData: { data: string; mimeType: string } }>(
(resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => {
if (reader.error) {
return reject(new Error(`Error reading file "${file.name}"`));
}
const result = reader.result as string;
if (!result) {
return reject(
new Error(
`Could not process file: ${file.name}. It may be empty.`
)
);
}
resolve({
inlineData: { data: result.split(',')[1], mimeType: file.type },
});
};
reader.onerror = () => {
reject(new Error(`Failed to process file: ${file.name}`));
};
reader.readAsDataURL(file);
}
);
};
const prompt = `
As an expert in cybersecurity and multi-vector scam detection, meticulously analyze the provided file(s) to determine if the application, website, or service described is a potential scam.
Structure your response in Markdown format with the following non-negotiable headings:
## Verdict
(A one-sentence conclusion: "High Risk of Scam," "Potential Red Flags," "Likely Legitimate," etc.)
## Confidence Score
(A percentage from 0% to 100% indicating your confidence in the verdict. e.g., "95%")
## Threat Vector Scores
(Provide a score from 0 to 100 for each category below.)
- Psychological Tactics: [score]
- Technical Red Flags: [score]
- Financial Scrutiny: [score]
- Communication Analysis: [score]
## Key Red Flags
(A bulleted list of the most critical warning signs discovered.)
## Detailed Breakdown
(Provide an **extremely detailed and comprehensive analysis** here, using the subheadings below. For each point, you MUST cite specific evidence or quotes from the provided files and explain *why* it is a red flag.)
### Psychological Tactics
(Analyze for high-pressure tactics, FOMO, fake social proof, emotional manipulation, urgency, and authority claims. Provide specific examples and quotes from the files.)
### Technical Red Flags
(Analyze for domain spoofing, typosquatting, lack of HTTPS, suspicious file metadata, unusual file types, and code obfuscation. Go into detail and explain the technical significance of each finding.)
### Financial Scrutiny
(Analyze for unrealistic profit promises, requests for unusual payment methods like gift cards or crypto, lack of clear refund policy, hidden fees, and pressure to invest more. Detail any suspicious transaction patterns.)
### Communication Analysis
(Analyze for poor grammar, spelling errors, overly generic language, unprofessional tone, mismatched sender addresses, and urgent calls to action. Quote specific phrases that are problematic.)
## Workflow Diagram
(Based on your analysis, **you must create a flowchart** of the process described in the files using Mermaid.js 'graph TD' syntax. This diagram should be enclosed in a \`\`\`mermaid code block. Visually highlight the steps that are part of the suspected scam. To do this, define a style for scam nodes at the end of the diagram, for example: 'style scamNodeId fill:#f8d7da,stroke:#dc3545,stroke-width:2px'. Apply this style to all relevant nodes.)
## Recommendations
(Provide a clear, actionable list of recommendations for the user.)
Base your entire analysis STRICTLY on the evidence within the provided files.
`;
const fileParts = await Promise.all(
selectedFiles.map(fileToGenerativePart)
);
const allParts = [{ text: prompt }, ...fileParts];
const responseStream = await ai.models.generateContentStream({
model: 'gemini-2.5-flash',
contents: { parts: allParts },
});
const thinkingContent = document.createElement('div');
thinkingContainer.innerHTML = '<h2>Thinking Process</h2>';
thinkingContainer.appendChild(thinkingContent);
for await (const chunk of responseStream) {
const chunkText = chunk.text;
fullFileAnalysisResponse += chunkText;
thinkingContent.innerHTML = marked.parse(
fullFileAnalysisResponse
) as string;
}
// Processing finished, now parse and display in tabs
thinkingContainer.classList.add('hidden');
populateResults(fullFileAnalysisResponse);
analysisResultsContainer.classList.remove('hidden');
if (lastSearchResult) {
finalAnalysisSection.classList.remove('hidden');
}
} catch (e) {
handleApiError(e, thinkingContainer, 'Analysis Failed');
} finally {
analyzeButton.disabled = false;
searchButton.disabled = false;
}
});
function parseSection(markdown: string, sectionTitle: string): string {
const regex = new RegExp(`## ${sectionTitle}\\n([\\s\\S]*?)(?=\\n##|$)`, 'i');
const match = markdown.match(regex);
return match ? match[1].trim() : 'Not found in analysis.';
}
/**
* A more robust function to get the detailed breakdown, as it can contain
* sub-headings that might confuse a simpler regex.
*/
function getDetailedBreakdown(markdown: string): string {
const startMarker = '## Detailed Breakdown';
const startIndex = markdown.indexOf(startMarker);
if (startIndex === -1) {
return 'Detailed analysis not found in the response.';
}
const contentStartIndex = startIndex + startMarker.length;
// Find the start of the next H2 section
const nextSectionIndex = markdown.indexOf('\n## ', contentStartIndex);
if (nextSectionIndex !== -1) {
return markdown.substring(contentStartIndex, nextSectionIndex).trim();
} else {
// If no next section, take the rest of the string
return markdown.substring(contentStartIndex).trim();
}
}
function parseThreatScores(markdown: string): Record<string, number> {
const scores: Record<string, number> = {
'Psychological Tactics': 0,
'Technical Red Flags': 0,
'Financial Scrutiny': 0,
'Communication Analysis': 0,
};
const threatScoresText = parseSection(markdown, 'Threat Vector Scores');
if (threatScoresText === 'Not found in analysis.') {
return scores;
}
const lines = threatScoresText.split('\n');
lines.forEach((line) => {
// Regex to capture the category name and the score number
const match = line.match(/-\s*(.*?):\s*.*?(\d+)/);
if (match) {
const key = match[1].trim() as keyof typeof scores;
const value = parseInt(match[2], 10);
if (key in scores) {
scores[key] = value;
}
}
});
return scores;
}
function parseWorkflowDiagram(markdown: string): string {
const diagramSection = parseSection(markdown, 'Workflow Diagram');
if (diagramSection === 'Not found in analysis.') {
return '';
}
const match = diagramSection.match(/```mermaid\n([\s\\S]*?)```/);
return match ? match[1].trim() : '';
}
async function populateResults(response: string) {
// Parse sections
const verdict = parseSection(response, 'Verdict');
const confidenceScoreText = parseSection(response, 'Confidence Score');
const redFlags = parseSection(response, 'Key Red Flags');
const detailedBreakdown = getDetailedBreakdown(response);
const threatScores = parseThreatScores(response);
const workflowDiagram = parseWorkflowDiagram(response);
// Populate Summary Tab
summaryVerdict.innerHTML = marked.parse(verdict) as string;
// Store raw red flags content and render markdown
redFlagsContent = redFlags;
summaryRedFlags.innerHTML = marked.parse(redFlags) as string;
if (redFlagsContent && redFlagsContent !== 'Not found in analysis.') {
downloadRedFlagsButton.classList.remove('hidden');
}
// Populate Details Tab
detailsTabPanel.innerHTML = marked.parse(detailedBreakdown) as string;
// Populate Workflow Diagram Tab
if (workflowDiagram) {
try {
// Use mermaid.render for more robust, programmatic rendering
const { svg } = await mermaid.render('mermaid-svg', workflowDiagram);
workflowDiagramContainer.innerHTML = svg;
} catch (e) {
console.error('Mermaid rendering failed:', e);
workflowDiagramContainer.innerHTML = `<p>Error rendering workflow diagram. The diagram code may be invalid.</p><pre>${workflowDiagram}</pre>`;
}
} else {
workflowDiagramContainer.innerHTML =
'<p>No workflow diagram could be generated from the provided files.</p>';
}
// Update Risk-O-Meter
const scoreMatch = confidenceScoreText.match(/(\d+)/);
const score = scoreMatch ? parseInt(scoreMatch[1], 10) : 0;
// Map 0-100 score to -90 to +90 degrees rotation for the needle
const rotation = (score / 100) * 180 - 90;
riskGaugeFill.style.transform = `rotate(${rotation}deg)`;
let riskText = 'Low';
if (score >= 75) riskText = 'Critical';
else if (score >= 50) riskText = 'High';
else if (score >= 25) riskText = 'Medium';
riskLabel.textContent = `${riskText} Risk (${score}%)`;
// Update Threat Vector Gauges
for (const key in threatGauges) {
const gauge = threatGauges[key as keyof typeof threatGauges];
const score = threatScores[key];
gauge.fill.style.width = `${score}%`;
gauge.score.textContent = `${score}%`;
// Set data-level for color coding in CSS
let level = 'low';
if (score >= 75) level = 'critical';
else if (score >= 50) level = 'high';
else if (score >= 25) level = 'medium';
gauge.fill.dataset.level = level;
}
}
tabButtons.forEach((button) => {
button.addEventListener('click', () => {
tabButtons.forEach((btn) => btn.classList.remove('active'));
button.classList.add('active');
const tabId = (button as HTMLElement).dataset.tab;
tabPanels.forEach((panel) => {
panel.id === tabId
? panel.classList.add('active')
: panel.classList.remove('active');
});
});
});
searchButton.addEventListener('click', async () => {
const query = searchInput.value.trim();
if (!query) {
searchAnalysisContainer.innerHTML = '<p>Please enter a search term.</p>';
searchAnalysisContainer.classList.remove('hidden');
return;
}
// UI setup
searchButton.disabled = true;
analyzeButton.disabled = true;
thinkingContainer.innerHTML =
'<h2>Investigating Online...</h2><div class="loader"></div>';
thinkingContainer.classList.remove('hidden');
searchAnalysisContainer.classList.add('hidden');
analysisResultsContainer.classList.add('hidden'); // Hide file results
finalAnalysisSection.classList.add('hidden');
lastSearchResult = '';
try {
const ai = new GoogleGenAI({ apiKey: API_KEY });
// Step 1: Perform the Google Search
const searchResponse = await ai.models.generateContent({
model: 'gemini-2.5-flash',
contents: `Perform a thorough web search about "${query}" and provide a summary of the findings.`,
config: { tools: [{ googleSearch: {} }] },
});
const searchResultText = searchResponse.text;
const groundingChunks =
searchResponse.candidates?.[0]?.groundingMetadata?.groundingChunks;
// Step 2: Analyze the search results for scam indicators
thinkingContainer.innerHTML =
'<h2>Analyzing Search Results...</h2><div class="loader"></div>';
const analysisPrompt = `
You are a world-class scam detection expert and digital forensics analyst.
Based *only* on the provided web search summary about "${query}", create a detailed risk assessment.
**Web Search Summary:**
---
${searchResultText}
---
**Your Task:**
Generate a concise, user-friendly risk assessment in Markdown format with these exact headings:
### Overall Assessment
(A one-sentence conclusion. Be direct: "Appears Legitimate," "Shows Potential Red Flags," "High Risk of Scam," or "Inconclusive Evidence.")
### Key Findings
(A bulleted list of the most critical positive or negative findings from the web search. Each point should be a single, impactful sentence.)
### Detailed Analysis
(A paragraph explaining the reasoning behind your assessment. Reference specific information from the search summary.)
`;
const analysisResponse = await ai.models.generateContent({
model: 'gemini-2.5-flash',
contents: analysisPrompt,
});
const analysisText = analysisResponse.text;
// Store the combined result for the whistleblower report
lastSearchResult = `**Web Investigation for "${query}"**\n\n${analysisText}`;
let finalHtml = marked.parse(analysisText) as string;
// Append sources from the initial search
if (groundingChunks && groundingChunks.length > 0) {
const sources = groundingChunks
.map((chunk) => chunk.web)
.filter((web) => web?.uri);
if (sources.length > 0) {
finalHtml += '<h3>Sources</h3><ul>';
for (const source of sources) {
finalHtml += `<li><a href="${source.uri}" target="_blank" rel="noopener noreferrer">${
source.title || source.uri
}</a></li>`;
}
finalHtml += '</ul>';
}
}
searchAnalysisContainer.innerHTML = finalHtml;
searchAnalysisContainer.classList.remove('hidden');
thinkingContainer.classList.add('hidden');
// Check if we can enable the final report
if (fullFileAnalysisResponse) {
finalAnalysisSection.classList.remove('hidden');
}
} catch (e) {
handleApiError(e, thinkingContainer, 'Investigation Failed');
} finally {
searchButton.disabled = false;
analyzeButton.disabled = selectedFiles.length === 0;
}
});
analyzeCombinedButton.addEventListener('click', async () => {
if (!fullFileAnalysisResponse || !lastSearchResult) {
whistleblowerContainer.innerHTML =
'<p>Please perform both a file analysis and a web search first.</p>';
return;
}
analyzeCombinedButton.disabled = true;
downloadReportButton.classList.add('hidden');
whistleblowerContainer.innerHTML = '<div class="loader"></div>';
whistleblowerReportContent = '';
try {
const ai = new GoogleGenAI({ apiKey: API_KEY });
const prompt = `
You are a consumer protection expert and digital forensics analyst. You have been provided with two pieces of evidence regarding a potential scam:
1. An in-depth analysis of user-provided files.
2. A summary of web search results about the entity.
Your task is to synthesize all of this information into a comprehensive and highly detailed whistleblower report. This report should be actionable and easy for the user to understand and use when contacting authorities.
**IMPORTANT:** If the web search findings contradict the user-provided files, you MUST prioritize the analysis from the user's files as the primary source of evidence. The web search is supplementary. Your final report should align with the user's provided materials and use the web search to add context, not to invalidate the user's evidence.
**Evidence 1: Initial File Analysis**
---
${fullFileAnalysisResponse}
---
**Evidence 2: Web Search Summary**
---
${lastSearchResult}
---
Based on the combined evidence, generate a report in Markdown format with the following precise structure. Be as detailed as possible in each section, referencing specific points from the evidence provided.
#### Suspected Scam Category
(Identify the most likely type of scam. Be specific, e.g., "Advanced Fee Fraud via Investment Platform," "Phishing attempt impersonating a financial institution," "Tech Support Scam with remote access software," etc.)
#### Executive Summary
(Provide a brief, one-paragraph summary of the situation, the key risks, and the overall recommendation.)
#### Recommended Reporting Agencies
(Provide a list of the most relevant government or official agencies. For EACH agency, you MUST first write its name as a level 4 Markdown heading (e.g., \`#### Federal Trade Commission (FTC)\`), and then on the following lines, provide the details in a bulleted list:)
- **Why it's relevant:** A clear explanation connecting the suspected scam to the agency's jurisdiction.
- **Direct Link:** A direct URL to their online complaint form.
- **Information to provide:** A short list of key details the user should have ready (e.g., company name, dates, amount of money lost).
#### Platform-Specific Reporting
(If applicable, do the same for platforms. For EACH platform, first write its name as a level 4 Markdown heading (e.g., \`#### Meta (Facebook)\`), and then on the following lines, provide the details:)
- **What to report:** e.g., "Report the user profile for impersonation."
- **Direct Link:** A link to the platform's reporting page if possible.
#### Financial Steps & Recovery
(If financial information was compromised or sent, provide a detailed, bulleted list of crucial next steps. Be specific. For example, instead of just "Contact your bank," say "Contact your bank's fraud department immediately at the number on the back of your card. Use the phrase 'unauthorized transaction'...")
#### Evidence Summary for Reporting
(Create a concise, bullet-pointed summary of the key evidence from both the file analysis and web search that the user can copy and paste into their reports to authorities.)
`;
const response = await ai.models.generateContent({
model: 'gemini-2.5-flash',
contents: prompt,
});
whistleblowerReportContent = response.text;
whistleblowerContainer.innerHTML = marked.parse(
whistleblowerReportContent
) as string;
addInteractiveReportButtons(whistleblowerContainer);
downloadReportButton.classList.remove('hidden');
} catch (e) {
handleApiError(e, whistleblowerContainer, 'Report Generation Failed');
} finally {
analyzeCombinedButton.disabled = false;
}
});
/**
* Scans the whistleblower report for H4 headings (agencies/platforms)
* and injects interactive buttons to generate targeted reports.
* @param container The container element of the whistleblower report.
*/
function addInteractiveReportButtons(container: HTMLElement) {
const reportHeadings = container.querySelectorAll('h4');
reportHeadings.forEach((heading) => {
const headingText = heading.textContent?.trim() || '';
// These are the main structural headings, not entities to report to.
const structuralHeadings = [
'Suspected Scam Category',
'Executive Summary',
'Recommended Reporting Agencies',
'Platform-Specific Reporting',
'Financial Steps & Recovery',
'Evidence Summary for Reporting',
];
if (headingText && !structuralHeadings.includes(headingText)) {
const button = document.createElement('button');
button.className = 'generate-specific-report-button';
button.textContent = 'Generate Targeted Report';
button.dataset.entityName = headingText;
const reportContainer = document.createElement('div');
reportContainer.className = 'specific-report-container';
reportContainer.setAttribute('aria-live', 'polite');
// Insert button and container right after the heading
heading.insertAdjacentElement('afterend', reportContainer);
heading.insertAdjacentElement('afterend', button);
}
});
}
/**
* Handles clicks within the whistleblower container to generate targeted reports.
*/
whistleblowerContainer.addEventListener('click', async (event) => {
const target = event.target as HTMLElement;
if (target.classList.contains('generate-specific-report-button')) {
const button = target as HTMLButtonElement;
const entityName = button.dataset.entityName;
// The report container is always the next sibling of the button.
const reportContainer = button.nextElementSibling as HTMLElement;
if (entityName && reportContainer) {
await generateTargetedReport(entityName, button, reportContainer);
}
}
});
/**
* Generates a specific, focused report for a single entity (agency or platform).
* @param entityName The name of the target entity.
* @param button The button that was clicked, to update its state.
* @param container The container element to render the report into.
*/
async function generateTargetedReport(
entityName: string,
button: HTMLButtonElement,
container: HTMLElement
) {
button.disabled = true;
button.textContent = 'Generating...';
container.innerHTML = '<div class="loader-small"></div>';
try {
const ai = new GoogleGenAI({ apiKey: API_KEY });
const prompt = `
As a paralegal and consumer protection specialist, you are tasked with drafting a formal complaint for a specific regulatory body based on a pre-existing analysis.
**Target Audience:** ${entityName}
**Contextual Evidence:**
1. **Initial File Analysis:**
---
${fullFileAnalysisResponse}
---
2. **Web Search Summary:**
---
${lastSearchResult}
---
**Your Task:**
1. **Draft a Targeted Complaint:** Write a formal, concise, and highly focused complaint addressed to the "${entityName}". The tone should be professional and factual.
- **Focus:** Extract and summarize ONLY the evidence directly relevant to the likely jurisdiction of "${entityName}". For example:
- If reporting to the **FTC**, focus on deceptive advertising, unfair business practices, and fraud.
- If reporting to a **bank's fraud department**, focus exclusively on unauthorized transactions, account details, dates, and amounts.
- If reporting to a **social media platform**, focus on the violation of their specific terms of service (e.g., impersonation, scams, spam).
- **Structure:** The complaint should be well-structured, clear, and ready to be copy-pasted into a reporting form. Start with a clear subject line (e.g., "Subject: Formal Complaint Regarding Deceptive Practices by [Company Name]").
2. **Provide Submission Information:** After the complaint text, add a new section under the markdown heading "### How to Submit". In this section:
- Identify the most effective method for submitting this complaint to "${entityName}".
- Provide a direct URL to their official online complaint form if available.
- If a form isn't the primary method, provide the official contact email address for their fraud or complaints department.
- Briefly state what information the user should have ready (e.g., "Be prepared to provide your personal contact information and any transaction IDs.").
Generate the response in Markdown format.
`;
const response = await ai.models.generateContent({
model: 'gemini-2.5-flash',
contents: prompt,
});
const reportContent = response.text;
container.innerHTML = marked.parse(reportContent) as string;
button.textContent = 'Report Generated';
// Button remains disabled to prevent re-generation.
} catch (e) {
handleApiError(e, container, `Failed to generate report for ${entityName}`);
button.textContent = 'Generation Failed - Retry';
button.disabled = false;
}
}
downloadReportButton.addEventListener('click', () => {
if (!whistleblowerReportContent) return;
const blob = new Blob([whistleblowerReportContent], {
type: 'text/markdown;charset=utf-8',
});
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = 'whistleblower-report.md';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
});
downloadRedFlagsButton.addEventListener('click', () => {
if (!redFlagsContent) return;
const blob = new Blob([redFlagsContent], {
type: 'text/plain;charset=utf-8',
});
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = 'key-red-flags.txt';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
});