yasserrmd commited on
Commit
e6ecc60
·
verified ·
1 Parent(s): 8d04ddf

Upload 21 files

Browse files
app/__init__.py ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask
2
+ from config import Config
3
+ from .models import db
4
+
5
+ def create_app(config_class=Config):
6
+ app = Flask(__name__)
7
+ app.config.from_object(config_class)
8
+
9
+ db.init_app(app)
10
+
11
+ with app.app_context():
12
+ from . import routes # Import routes
13
+ # db.drop_all() # Uncomment if you need to reset the DB
14
+ db.create_all() # Create sql tables for our data models
15
+ routes.register_routes(app) # Register the routes
16
+
17
+ # Register blueprints here if you plan to use them
18
+ # from .main import bp as main_bp
19
+ # app.register_blueprint(main_bp)
20
+
21
+ return app
22
+
app/__pycache__/__init__.cpython-311.pyc ADDED
Binary file (1.1 kB). View file
 
app/__pycache__/models.cpython-311.pyc ADDED
Binary file (8.42 kB). View file
 
app/__pycache__/routes.cpython-311.pyc ADDED
Binary file (16.5 kB). View file
 
app/models.py ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask_sqlalchemy import SQLAlchemy
2
+ from datetime import datetime
3
+ import enum
4
+
5
+ db = SQLAlchemy()
6
+
7
+ class Country(db.Model):
8
+ id = db.Column(db.Integer, primary_key=True)
9
+ name = db.Column(db.String(100), unique=True, nullable=False)
10
+ region = db.Column(db.String(100), nullable=False)
11
+ cases = db.relationship("Case", backref="country_context", lazy=True)
12
+ reports = db.relationship("Report", backref="report_country_context", lazy=True)
13
+
14
+ def __repr__(self):
15
+ return f"<Country {self.name}>"
16
+
17
+ class CaseStatus(enum.Enum):
18
+ ACTIVE = "Active"
19
+ PENDING = "Pending"
20
+ COMPLETED = "Completed"
21
+ CLOSED = "Closed"
22
+
23
+ class Case(db.Model):
24
+ id = db.Column(db.Integer, primary_key=True)
25
+ case_id_display = db.Column(db.String(50), unique=True, nullable=False)
26
+ case_type = db.Column(db.String(100), nullable=False)
27
+ suspect_name = db.Column(db.String(150), nullable=False)
28
+ profile_details = db.Column(db.Text, nullable=True)
29
+ evidence_summary = db.Column(db.Text, nullable=True)
30
+ status = db.Column(db.Enum(CaseStatus), default=CaseStatus.PENDING, nullable=False)
31
+ created_at = db.Column(db.DateTime, default=datetime.utcnow)
32
+ updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
33
+ country_id = db.Column(db.Integer, db.ForeignKey("country.id"), nullable=True) # For recommendation context
34
+
35
+ interrogation_sessions = db.relationship("InterrogationSession", backref="case", lazy=True, cascade="all, delete-orphan")
36
+ reports = db.relationship("Report", backref="case", lazy=True, cascade="all, delete-orphan")
37
+
38
+ def __repr__(self):
39
+ return f"<Case {self.case_id_display}>"
40
+
41
+ class InterrogationSession(db.Model):
42
+ id = db.Column(db.Integer, primary_key=True)
43
+ case_id = db.Column(db.Integer, db.ForeignKey("case.id"), nullable=False)
44
+ session_date = db.Column(db.DateTime, default=datetime.utcnow)
45
+ summary_notes = db.Column(db.Text, nullable=True)
46
+ generated_questions = db.relationship("GeneratedQuestion", backref="session", lazy=True, cascade="all, delete-orphan")
47
+
48
+ def __repr__(self):
49
+ return f"<InterrogationSession {self.id} for Case {self.case_id}>"
50
+
51
+ class GeneratedQuestion(db.Model):
52
+ id = db.Column(db.Integer, primary_key=True)
53
+ interrogation_session_id = db.Column(db.Integer, db.ForeignKey("interrogation_session.id"), nullable=False)
54
+ question_text = db.Column(db.Text, nullable=False)
55
+ category = db.Column(db.String(100), nullable=True)
56
+ generated_at = db.Column(db.DateTime, default=datetime.utcnow)
57
+ responses = db.relationship("InterrogationResponse", backref="question", lazy=True, cascade="all, delete-orphan")
58
+
59
+ def __repr__(self):
60
+ return f"<GeneratedQuestion {self.id}>"
61
+
62
+ class InterrogationResponse(db.Model):
63
+ id = db.Column(db.Integer, primary_key=True)
64
+ generated_question_id = db.Column(db.Integer, db.ForeignKey("generated_question.id"), nullable=False)
65
+ response_text = db.Column(db.Text, nullable=True)
66
+ tags = db.Column(db.String(200), nullable=True) # e.g., "evasiveness,contradiction"
67
+ responded_at = db.Column(db.DateTime, default=datetime.utcnow)
68
+
69
+ def __repr__(self):
70
+ return f"<InterrogationResponse {self.id} for Question {self.generated_question_id}>"
71
+
72
+ class Report(db.Model):
73
+ id = db.Column(db.Integer, primary_key=True)
74
+ case_id = db.Column(db.Integer, db.ForeignKey("case.id"), nullable=False)
75
+ generated_at = db.Column(db.DateTime, default=datetime.utcnow)
76
+ llm_json_output = db.Column(db.Text, nullable=True) # Raw JSON from LLM
77
+ report_content_summary = db.Column(db.Text, nullable=True) # Processed summary for display
78
+ recommendations = db.Column(db.Text, nullable=True) # Country-specific recommendations
79
+ country_id = db.Column(db.Integer, db.ForeignKey("country.id"), nullable=True) # Context for recommendations
80
+
81
+ def __repr__(self):
82
+ return f"<Report {self.id} for Case {self.case_id}>"
83
+
app/routes.py ADDED
@@ -0,0 +1,226 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import render_template, request, jsonify, current_app
2
+ from app import db # Assuming db is initialized in app's __init__.py
3
+ from app.models import Case, Country, InterrogationSession, GeneratedQuestion, InterrogationResponse, Report, CaseStatus
4
+ from app.utils.groq_client import GroqClient
5
+ import json
6
+
7
+ # This will be the main blueprint or directly in app.routes if not using blueprints
8
+ # For simplicity, adding routes directly here. In a larger app, use Blueprints.
9
+
10
+ def register_routes(app):
11
+
12
+ @app.route('/')
13
+ @app.route('/dashboard')
14
+ def dashboard():
15
+ # Fetch some data for the dashboard
16
+ active_cases_count = Case.query.filter_by(status=CaseStatus.ACTIVE).count()
17
+ total_interrogations = InterrogationSession.query.count()
18
+ completed_reports_count = Report.query.join(Case).filter(Case.status == CaseStatus.COMPLETED).count()
19
+
20
+ recent_cases = Case.query.order_by(Case.created_at.desc()).limit(5).all()
21
+ countries = Country.query.order_by(Country.name).all()
22
+
23
+ # Placeholder for resolution rate
24
+ resolution_rate = "85%" # This would be calculated based on closed/completed cases
25
+
26
+ return render_template(
27
+ 'dashboard.html',
28
+ active_cases_count=active_cases_count,
29
+ total_interrogations=total_interrogations,
30
+ completed_reports_count=completed_reports_count,
31
+ resolution_rate=resolution_rate,
32
+ recent_cases=recent_cases,
33
+ countries=countries # Pass countries to the template for selection
34
+ )
35
+
36
+ @app.route('/cases', methods=['GET', 'POST'])
37
+ def manage_cases():
38
+ if request.method == 'POST':
39
+ data = request.form
40
+ new_case = Case(
41
+ case_id_display=data.get('case_id_display', f"C-NEW-{db.session.query(Case).count() + 1}"), # Generate a simple new ID
42
+ case_type=data.get('case_type'),
43
+ suspect_name=data.get('suspect_name'),
44
+ profile_details=data.get('profile_details'),
45
+ evidence_summary=data.get('evidence_summary'),
46
+ status=CaseStatus.PENDING, # Default status
47
+ country_id=data.get('country_id') # Assuming country_id is passed from form
48
+ )
49
+ db.session.add(new_case)
50
+ db.session.commit()
51
+ return jsonify({'message': 'Case created successfully', 'case_id': new_case.id}), 201
52
+
53
+ cases = Case.query.all()
54
+ return render_template('cases.html', cases=cases) # Placeholder, need a cases.html
55
+
56
+ @app.route('/case/<int:case_id>')
57
+ def view_case(case_id):
58
+ case = Case.query.get_or_404(case_id)
59
+ countries = Country.query.all()
60
+ return render_template('case_detail.html', case=case, countries=countries) # Placeholder, need case_detail.html
61
+
62
+ @app.route('/generate_questions/<int:case_id>', methods=['POST'])
63
+ def generate_questions_route(case_id):
64
+ case = Case.query.get_or_404(case_id)
65
+ if not case.profile_details or not case.evidence_summary:
66
+ return jsonify({'error': 'Case profile and evidence summary are required to generate questions.'}), 400
67
+
68
+ groq_cli = GroqClient()
69
+ try:
70
+ questions_list = groq_cli.generate_interrogation_questions(
71
+ case_details=f"Case Type: {case.case_type}, ID: {case.case_id_display}",
72
+ suspect_profile=case.profile_details,
73
+ evidence_summary=case.evidence_summary
74
+ )
75
+
76
+ # Store questions in the database
77
+ # First, ensure an interrogation session exists or create one
78
+ session = InterrogationSession.query.filter_by(case_id=case.id).first()
79
+ if not session:
80
+ session = InterrogationSession(case_id=case.id, summary_notes="Initial question generation session.")
81
+ db.session.add(session)
82
+ db.session.commit()
83
+
84
+ generated_q_objects = []
85
+ for q_text in questions_list:
86
+ gq = GeneratedQuestion(interrogation_session_id=session.id, question_text=q_text, category="AI Generated")
87
+ db.session.add(gq)
88
+ generated_q_objects.append({'id': None, 'text': q_text}) # ID will be set after commit
89
+ db.session.commit()
90
+
91
+ # Update IDs after commit
92
+ for i, gq_obj in enumerate(GeneratedQuestion.query.filter_by(interrogation_session_id=session.id).order_by(GeneratedQuestion.id.desc()).limit(len(questions_list)).all()):
93
+ # This is a bit of a hack to get IDs back, ideally return them from DB operation
94
+ if i < len(generated_q_objects):
95
+ generated_q_objects[-(i+1)]['id'] = gq_obj.id
96
+
97
+ return jsonify({'case_id': case.id, 'questions': generated_q_objects}), 200
98
+ except Exception as e:
99
+ current_app.logger.error(f"Error in /generate_questions/{case_id}: {e}")
100
+ return jsonify({'error': str(e)}), 500
101
+
102
+ @app.route('/generate_report/<int:case_id>', methods=['POST'])
103
+ def generate_report_route(case_id):
104
+ case = Case.query.get_or_404(case_id)
105
+ data = request.json
106
+ selected_country_id = data.get('country_id')
107
+ interrogation_summary_text = data.get('interrogation_summary', 'No detailed interrogation summary provided by user yet.')
108
+
109
+ if not selected_country_id:
110
+ return jsonify({'error': 'Country ID is required for report generation.'}), 400
111
+
112
+ selected_country = Country.query.get(selected_country_id)
113
+ if not selected_country:
114
+ return jsonify({'error': 'Invalid Country ID.'}), 400
115
+
116
+ # Consolidate interrogation data for the LLM
117
+ full_interrogation_summary = interrogation_summary_text # Start with user provided summary
118
+ sessions = InterrogationSession.query.filter_by(case_id=case.id).all()
119
+ if sessions:
120
+ full_interrogation_summary += "\n\n--- Recorded Interrogation Details ---"
121
+ for sess_idx, session in enumerate(sessions):
122
+ full_interrogation_summary += f"""\nSession {sess_idx+1} (Date: {session.session_date.strftime("%Y-%m-%d")}):\n"""
123
+ if session.summary_notes:
124
+ full_interrogation_summary += f"Session Notes: {session.summary_notes}\n"
125
+ questions = GeneratedQuestion.query.filter_by(interrogation_session_id=session.id).all()
126
+ if questions:
127
+ full_interrogation_summary += "Questions and Responses:\n"
128
+ for q_idx, q in enumerate(questions):
129
+ full_interrogation_summary += f" Q{q_idx+1}: {q.question_text}\n"
130
+ responses = InterrogationResponse.query.filter_by(generated_question_id=q.id).all()
131
+ if responses:
132
+ for r_idx, r in enumerate(responses):
133
+ full_interrogation_summary += f" A{r_idx+1}: {r.response_text} (Tags: {r.tags or 'N/A'})\n"
134
+ else:
135
+ full_interrogation_summary += " A: No response recorded.\n"
136
+ else:
137
+ full_interrogation_summary += "No specific questions recorded for this session.\n"
138
+ else:
139
+ full_interrogation_summary += "\nNo formal interrogation sessions found in the database for this case."
140
+
141
+ groq_cli = GroqClient()
142
+ try:
143
+ report_json_str = groq_cli.generate_report_and_recommendations(
144
+ interrogation_summary=full_interrogation_summary,
145
+ profile_details=case.profile_details or "Not provided",
146
+ evidence_summary=case.evidence_summary or "Not provided",
147
+ selected_country_name=selected_country.name
148
+ )
149
+
150
+ # Validate JSON and extract summary for DB
151
+ report_content_summary_for_db = "Error parsing LLM JSON output."
152
+ recommendations_for_db = "Error parsing LLM JSON output."
153
+ try:
154
+ report_data = json.loads(report_json_str)
155
+ # Extract a brief summary and recommendations for storing in main text fields
156
+ # This is a simplified extraction. A more robust parsing would be needed for complex JSON.
157
+ cs = report_data.get("caseSummary", {})
158
+ if isinstance(cs, dict):
159
+ report_content_summary_for_db = cs.get("briefOverview", "Summary not found in JSON.")
160
+ else: # if caseSummary is a string directly
161
+ report_content_summary_for_db = str(cs)
162
+
163
+ recom = report_data.get("recommendations", {})
164
+ if isinstance(recom, dict):
165
+ recommendations_for_db = json.dumps(recom) # Store the whole recommendations object as JSON string
166
+ else:
167
+ recommendations_for_db = str(recom)
168
+
169
+ except json.JSONDecodeError as e:
170
+ current_app.logger.error(f"Failed to parse report JSON from LLM: {e}. Raw: {report_json_str}")
171
+ # report_json_str itself will be stored, and summary/recom will have error messages
172
+
173
+ # Store the report
174
+ new_report = Report(
175
+ case_id=case.id,
176
+ llm_json_output=report_json_str,
177
+ report_content_summary=report_content_summary_for_db,
178
+ recommendations=recommendations_for_db,
179
+ country_id=selected_country.id
180
+ )
181
+ db.session.add(new_report)
182
+ case.status = CaseStatus.COMPLETED # Optionally update case status
183
+ db.session.commit()
184
+
185
+ return jsonify({'message': 'Report generated successfully', 'report_id': new_report.id, 'raw_json_report': report_json_str}), 200
186
+ except Exception as e:
187
+ current_app.logger.error(f"Error in /generate_report/{case_id}: {e}")
188
+ return jsonify({'error': str(e)}), 500
189
+
190
+ @app.route('/get_countries', methods=['GET'])
191
+ def get_countries():
192
+ countries = Country.query.order_by(Country.name).all()
193
+ return jsonify([{'id': c.id, 'name': c.name, 'region': c.region} for c in countries])
194
+
195
+ # Add a route to view a specific report
196
+ @app.route('/report/<int:report_id>')
197
+ def view_report(report_id):
198
+ report = Report.query.get_or_404(report_id)
199
+ # The LLM output is stored as a JSON string, parse it for rendering
200
+ try:
201
+ report_data = json.loads(report.llm_json_output)
202
+ except (json.JSONDecodeError, TypeError):
203
+ report_data = {"error": "Could not parse report data", "raw": report.llm_json_output}
204
+ return render_template('report_detail.html', report=report, report_data=report_data) # Placeholder, need report_detail.html
205
+
206
+ # Placeholder for other routes from sidebar
207
+ @app.route('/interrogations')
208
+ def interrogations_page():
209
+ sessions = InterrogationSession.query.all()
210
+ return render_template('interrogations.html', sessions=sessions) # Placeholder
211
+
212
+ @app.route('/reports_list') # Renamed to avoid conflict with /report/<id>
213
+ def reports_list_page():
214
+ reports = Report.query.all()
215
+ return render_template('reports_list.html', reports=reports) # Placeholder
216
+
217
+ @app.route('/regional_guidelines')
218
+ def regional_guidelines_page():
219
+ countries = Country.query.all()
220
+ return render_template('regional_guidelines.html', countries=countries) # Placeholder
221
+
222
+ # Simple placeholder for other menu items
223
+ for item_route in ['team', 'analytics', 'evidence', 'settings', 'help']:
224
+ app.add_url_rule(f'/{item_route}', item_route, lambda item_route=item_route: f"{item_route.capitalize()} page coming soon! This is a placeholder.")
225
+
226
+
app/templates/analytics.html ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base_layout.html" %}
2
+
3
+ {% block title %}Analytics - InterroGen{% endblock %}
4
+
5
+ {% block page_content %}
6
+ <div class="heading-container">
7
+ <h1 class="h3">Application Analytics</h1>
8
+ </div>
9
+
10
+ <div class="card">
11
+ <div class="card-header">
12
+ <i class="bi bi-graph-up me-2"></i>Data Insights
13
+ </div>
14
+ <div class="card-body">
15
+ <p class="text-muted">This section will provide analytics and visualizations regarding case processing, interrogation effectiveness, report generation, and overall application usage.</p>
16
+
17
+ <h5 class="mt-4">Potential Analytics Dashboards:</h5>
18
+ <ul>
19
+ <li>Case resolution times and success rates.</li>
20
+ <li>Effectiveness of generated interrogation questions.</li>
21
+ <li>Report generation statistics.</li>
22
+ <li>User activity and engagement.</li>
23
+ <li>Trends in case types and outcomes.</li>
24
+ </ul>
25
+ <p><em>This page is currently under development. Full analytics features will be available soon.</em></p>
26
+ </div>
27
+ </div>
28
+
29
+ {% endblock %}
30
+
31
+ {% block scripts %}
32
+ <script>
33
+ // Add any analytics page specific JS here if needed
34
+ console.log("Analytics page loaded");
35
+ </script>
36
+ {% endblock %}
37
+
app/templates/base_layout.html ADDED
@@ -0,0 +1,228 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>InterroGen - {% block title %}Intelligent Interrogation Assistant{% endblock %}</title>
7
+ <link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/css/bootstrap.min.css" rel="stylesheet">
8
+ <link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-icons/1.10.5/font/bootstrap-icons.min.css" rel="stylesheet">
9
+ <style>
10
+ :root {
11
+ --primary: #3f6ad8; --secondary: #6c757d; --success: #3ac47d; --info: #16aaff;
12
+ --warning: #f7b924; --danger: #d92550; --light: #eee; --dark: #343a40;
13
+ --bg-light: #f8f9fa; --text-muted: #6c757d;
14
+ }
15
+ body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background-color: #f5f7fa; color: #495057; }
16
+ .sidebar {
17
+ background-color: white;
18
+ box-shadow: 0 0.125rem 0.25rem rgba(0,0,0,0.075);
19
+ height: 100vh;
20
+ position: fixed;
21
+ z-index: 100;
22
+ width: 280px; /* Fixed width for sidebar */
23
+ overflow-y: auto; /* Scroll for sidebar if content overflows */
24
+ }
25
+ .sidebar .nav-link {
26
+ color: #495057;
27
+ border-radius: 0.25rem;
28
+ margin: 0.25rem 0.5rem;
29
+ padding: 0.75rem 1rem;
30
+ transition: all 0.2s;
31
+ }
32
+ .sidebar .nav-link:hover, .sidebar .nav-link.active {
33
+ background-color: var(--primary);
34
+ color: white;
35
+ }
36
+ .sidebar .nav-link i {
37
+ margin-right: 0.75rem;
38
+ width: 1.25rem;
39
+ text-align: center;
40
+ }
41
+ .logo-wrapper {
42
+ padding: 1.5rem 1rem;
43
+ border-bottom: 1px solid rgba(0,0,0,0.05);
44
+ }
45
+ .logo {
46
+ font-size: 1.5rem;
47
+ font-weight: 700;
48
+ color: var(--primary);
49
+ display: flex;
50
+ align-items: center;
51
+ }
52
+ .main-content {
53
+ margin-left: 280px; /* Same as sidebar width */
54
+ padding: 0; /* Remove padding, will be handled by wrappers */
55
+ }
56
+ .top-bar-wrapper {
57
+ padding: 2rem 2rem 0 2rem; /* Padding for the container of top-bar */
58
+ }
59
+ .page-content-wrapper {
60
+ padding: 2rem; /* Padding for the actual page content */
61
+ }
62
+ .card {
63
+ border: none;
64
+ box-shadow: 0 0.125rem 0.25rem rgba(0,0,0,0.075);
65
+ margin-bottom: 1.5rem;
66
+ border-radius: 0.5rem;
67
+ }
68
+ .card-header {
69
+ background-color: white;
70
+ border-bottom: 1px solid rgba(0,0,0,0.05);
71
+ font-weight: 600;
72
+ padding: 1.25rem 1.5rem;
73
+ }
74
+ .top-bar {
75
+ background-color: white;
76
+ box-shadow: 0 0.125rem 0.25rem rgba(0,0,0,0.075);
77
+ padding: 1rem 2rem;
78
+ border-radius: 0.5rem;
79
+ display: flex;
80
+ justify-content: space-between;
81
+ align-items: center;
82
+ }
83
+ .search-wrapper {
84
+ position: relative;
85
+ width: 300px;
86
+ }
87
+ .search-wrapper i {
88
+ position: absolute;
89
+ left: 1rem;
90
+ top: 50%;
91
+ transform: translateY(-50%);
92
+ color: var(--text-muted);
93
+ }
94
+ .search-input {
95
+ padding-left: 2.5rem;
96
+ border-radius: 2rem;
97
+ background-color: #f5f7fa;
98
+ border: none;
99
+ }
100
+ .user-menu {
101
+ display: flex;
102
+ align-items: center;
103
+ }
104
+ .user-avatar {
105
+ width: 40px;
106
+ height: 40px;
107
+ border-radius: 50%;
108
+ background-color: var(--primary);
109
+ color: white;
110
+ display: flex;
111
+ align-items: center;
112
+ justify-content: center;
113
+ font-weight: 600;
114
+ margin-right: 0.75rem;
115
+ }
116
+ .btn-primary {
117
+ background-color: var(--primary);
118
+ border-color: var(--primary);
119
+ }
120
+ .btn-primary:hover {
121
+ background-color: #3257b3;
122
+ border-color: #3257b3;
123
+ }
124
+ .btn-outline-primary {
125
+ color: var(--primary);
126
+ border-color: var(--primary);
127
+ }
128
+ .btn-outline-primary:hover {
129
+ background-color: var(--primary);
130
+ color: white;
131
+ }
132
+ .dropdown-item i {
133
+ margin-right: 0.5rem;
134
+ width: 1rem;
135
+ text-align: center;
136
+ }
137
+ .heading-container {
138
+ display: flex;
139
+ justify-content: space-between;
140
+ align-items: center;
141
+ margin-bottom: 1.5rem;
142
+ }
143
+ </style>
144
+ {% block head_extra %}{% endblock %}
145
+ </head>
146
+ <body>
147
+ <!-- Sidebar -->
148
+ <div class="sidebar">
149
+ <div class="logo-wrapper">
150
+ <div class="logo">
151
+ <i class="bi bi-shield-lock-fill me-2"></i> InterroGen
152
+ </div>
153
+ </div>
154
+ <div class="position-sticky pt-3">
155
+ <ul class="nav flex-column">
156
+ <li class="nav-item"><a class="nav-link {% if request.endpoint == 'dashboard' %}active{% endif %}" href="{{ url_for('dashboard') }}"><i class="bi bi-grid-1x2-fill"></i> Dashboard</a></li>
157
+ <li class="nav-item"><a class="nav-link {% if request.endpoint in ['manage_cases', 'view_case'] %}active{% endif %}" href="{{ url_for('manage_cases') }}"><i class="bi bi-folder2-open"></i> Cases</a></li>
158
+ <li class="nav-item"><a class="nav-link {% if request.endpoint == 'interrogations_page' %}active{% endif %}" href="{{ url_for('interrogations_page') }}"><i class="bi bi-chat-square-text-fill"></i> Interrogations</a></li>
159
+ <li class="nav-item"><a class="nav-link {% if request.endpoint in ['reports_list_page', 'view_report'] %}active{% endif %}" href="{{ url_for('reports_list_page') }}"><i class="bi bi-file-earmark-text-fill"></i> Reports</a></li>
160
+ <li class="nav-item"><a class="nav-link {% if request.endpoint == 'regional_guidelines_page' %}active{% endif %}" href="{{ url_for('regional_guidelines_page') }}"><i class="bi bi-globe"></i> Regional Guidelines</a></li>
161
+ <li class="nav-item"><a class="nav-link {% if request.endpoint == 'team' %}active{% endif %}" href="{{ url_for('team') }}"><i class="bi bi-people-fill"></i> Team</a></li>
162
+ <li class="nav-item"><a class="nav-link {% if request.endpoint == 'analytics' %}active{% endif %}" href="{{ url_for('analytics') }}"><i class="bi bi-graph-up"></i> Analytics</a></li>
163
+ <li class="nav-item"><a class="nav-link {% if request.endpoint == 'evidence' %}active{% endif %}" href="{{ url_for('evidence') }}"><i class="bi bi-archive-fill"></i> Evidence</a></li>
164
+ <li class="nav-item"><a class="nav-link {% if request.endpoint == 'settings' %}active{% endif %}" href="{{ url_for('settings') }}"><i class="bi bi-gear-fill"></i> Settings</a></li>
165
+ <li class="nav-item"><a class="nav-link {% if request.endpoint == 'help' %}active{% endif %}" href="{{ url_for('help') }}"><i class="bi bi-question-circle-fill"></i> Help</a></li>
166
+ </ul>
167
+ </div>
168
+ </div>
169
+
170
+ <!-- Main Content -->
171
+ <div class="main-content">
172
+ <!-- Top bar -->
173
+ <div class="top-bar-wrapper">
174
+ <div class="top-bar">
175
+ <div class="search-wrapper">
176
+ <i class="bi bi-search"></i>
177
+ <input type="text" class="form-control search-input" placeholder="Search cases, reports...">
178
+ </div>
179
+ <div class="user-menu">
180
+ <div class="dropdown">
181
+ <a class="btn btn-sm btn-primary me-3" href="{{ url_for('manage_cases', action='new') }}" role="button">
182
+ <i class="bi bi-plus-lg"></i> New Case
183
+ </a>
184
+ <button class="btn" type="button" id="notificationDropdown" data-bs-toggle="dropdown" aria-expanded="false">
185
+ <i class="bi bi-bell position-relative">
186
+ <span class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger">3<span class="visually-hidden">unread messages</span></span>
187
+ </i>
188
+ </button>
189
+ <ul class="dropdown-menu dropdown-menu-end" aria-labelledby="notificationDropdown">
190
+ <li><h6 class="dropdown-header">Notifications</h6></li>
191
+ <li><a class="dropdown-item" href="#"><i class="bi bi-file-earmark-text"></i> New report available</a></li>
192
+ <li><a class="dropdown-item" href="#"><i class="bi bi-person-plus"></i> New team member added</a></li>
193
+ <li><a class="dropdown-item" href="#"><i class="bi bi-exclamation-triangle"></i> Case status updated</a></li>
194
+ <li><hr class="dropdown-divider"></li>
195
+ <li><a class="dropdown-item text-center" href="#">View all</a></li>
196
+ </ul>
197
+ </div>
198
+ <div class="dropdown">
199
+ <a href="#" class="d-flex align-items-center text-decoration-none dropdown-toggle" id="userDropdown" data-bs-toggle="dropdown" aria-expanded="false">
200
+ <div class="user-avatar">JD</div>
201
+ <div>
202
+ <div class="fw-bold">John Doe</div>
203
+ <div class="small text-muted">Senior Investigator</div>
204
+ </div>
205
+ </a>
206
+ <ul class="dropdown-menu dropdown-menu-end" aria-labelledby="userDropdown">
207
+ <li><a class="dropdown-item" href="#"><i class="bi bi-person"></i> Profile</a></li>
208
+ <li><a class="dropdown-item" href="#"><i class="bi bi-gear"></i> Settings</a></li>
209
+ <li><a class="dropdown-item" href="#"><i class="bi bi-activity"></i> Activity Log</a></li>
210
+ <li><hr class="dropdown-divider"></li>
211
+ <li><a class="dropdown-item" href="#"><i class="bi bi-box-arrow-right"></i> Sign out</a></li>
212
+ </ul>
213
+ </div>
214
+ </div>
215
+ </div>
216
+ </div>
217
+
218
+ <!-- Page specific content -->
219
+ <div class="page-content-wrapper">
220
+ {% block page_content %}{% endblock %}
221
+ </div>
222
+ </div>
223
+
224
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/js/bootstrap.bundle.min.js"></script>
225
+ {% block scripts %}{% endblock %}
226
+ </body>
227
+ </html>
228
+
app/templates/case_detail.html ADDED
@@ -0,0 +1,275 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base_layout.html" %}
2
+
3
+ {% block title %}Case Details: {{ case.case_id_display }} - InterroGen{% endblock %}
4
+
5
+ {% block head_extra %}
6
+ <style>
7
+ .json-display {
8
+ background-color: #f8f9fa;
9
+ border: 1px solid #dee2e6;
10
+ padding: 15px;
11
+ border-radius: 5px;
12
+ white-space: pre-wrap;
13
+ word-wrap: break-word;
14
+ max-height: 400px; /* Added for scrollability */
15
+ overflow-y: auto; /* Added for scrollability */
16
+ }
17
+ .case-info-pre {
18
+ background-color: #f8f9fa;
19
+ border: 1px solid #eee;
20
+ padding: 10px;
21
+ border-radius: 4px;
22
+ white-space: pre-wrap;
23
+ word-wrap: break-word;
24
+ font-family: monospace;
25
+ }
26
+ </style>
27
+ {% endblock %}
28
+
29
+ {% block page_content %}
30
+ <div class="heading-container">
31
+ <h1 class="h3">Case Details: {{ case.case_id_display }}</h1>
32
+ <div>
33
+ <a href="{{ url_for('manage_cases') }}" class="btn btn-outline-secondary"><i class="bi bi-arrow-left-circle me-1"></i>Back to Cases List</a>
34
+ </div>
35
+ </div>
36
+
37
+ <div class="row">
38
+ <div class="col-lg-7">
39
+ <div class="card mb-3">
40
+ <div class="card-header">
41
+ <i class="bi bi-file-text me-2"></i>Case Information
42
+ </div>
43
+ <div class="card-body">
44
+ <dl class="row">
45
+ <dt class="col-sm-4">Case ID:</dt>
46
+ <dd class="col-sm-8">{{ case.case_id_display }}</dd>
47
+
48
+ <dt class="col-sm-4">Case Type:</dt>
49
+ <dd class="col-sm-8">{{ case.case_type }}</dd>
50
+
51
+ <dt class="col-sm-4">Suspect Name:</dt>
52
+ <dd class="col-sm-8">{{ case.suspect_name }}</dd>
53
+
54
+ <dt class="col-sm-4">Status:</dt>
55
+ <dd class="col-sm-8"><span class="case-status status-{{ case.status.value.lower() }}">{{ case.status.value }}</span></dd>
56
+
57
+ <dt class="col-sm-4">Created At:</dt>
58
+ <dd class="col-sm-8">{{ case.created_at.strftime('%Y-%m-%d %H:%M:%S') }}</dd>
59
+
60
+ <dt class="col-sm-4">Last Updated:</dt>
61
+ <dd class="col-sm-8">{{ case.updated_at.strftime('%Y-%m-%d %H:%M:%S') }}</dd>
62
+
63
+ <dt class="col-sm-4">Country Context:</dt>
64
+ <dd class="col-sm-8">{{ case.country_context.name if case.country_context else 'N/A' }}</dd>
65
+ </dl>
66
+ <hr>
67
+ <h6>Profile Details:</h6>
68
+ <div class="case-info-pre">{{ case.profile_details if case.profile_details else 'Not provided' }}</div>
69
+ <h6 class="mt-3">Evidence Summary:</h6>
70
+ <div class="case-info-pre">{{ case.evidence_summary if case.evidence_summary else 'Not provided' }}</div>
71
+ </div>
72
+ </div>
73
+
74
+ <div class="card mb-3">
75
+ <div class="card-header">
76
+ <i class="bi bi-journal-richtext me-2"></i>Generated Reports for this Case
77
+ </div>
78
+ <div class="card-body">
79
+ {% if case.reports %}
80
+ <ul class="list-group list-group-flush">
81
+ {% for report in case.reports %}
82
+ <li class="list-group-item d-flex justify-content-between align-items-center">
83
+ Report generated on {{ report.generated_at.strftime('%Y-%m-%d %H:%M') }} (Country: {{ report.report_country_context.name if report.report_country_context else 'N/A' }})
84
+ <a href="{{ url_for('view_report', report_id=report.id) }}" class="btn btn-sm btn-outline-info">View Report</a>
85
+ </li>
86
+ {% endfor %}
87
+ </ul>
88
+ {% else %}
89
+ <p class="text-muted">No reports generated for this case yet.</p>
90
+ {% endif %}
91
+ </div>
92
+ </div>
93
+ </div>
94
+
95
+ <div class="col-lg-5">
96
+ <div class="card mb-3">
97
+ <div class="card-header">
98
+ <i class="bi bi-patch-question-fill me-2"></i>Interrogation Questions
99
+ </div>
100
+ <div class="card-body">
101
+ <button id="generateQuestionsBtn" class="btn btn-primary w-100 mb-3" data-case-id="{{ case.id }}">
102
+ <i class="bi bi-magic me-1"></i> Generate Interrogation Questions (LLM)
103
+ </button>
104
+ <div id="questionsArea">
105
+ {% if case.interrogation_sessions %}
106
+ {% for session in case.interrogation_sessions | sort(attribute='session_date', reverse=True) %}
107
+ <div class="accordion mb-2" id="accordionSession{{ session.id }}">
108
+ <div class="accordion-item">
109
+ <h2 class="accordion-header" id="headingSession{{ session.id }}">
110
+ <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseSession{{ session.id }}" aria-expanded="false" aria-controls="collapseSession{{ session.id }}">
111
+ Session on {{ session.session_date.strftime('%Y-%m-%d %H:%M') }} ({{ session.generated_questions | length }} Qs)
112
+ </button>
113
+ </h2>
114
+ <div id="collapseSession{{ session.id }}" class="accordion-collapse collapse" aria-labelledby="headingSession{{ session.id }}" data-bs-parent="#accordionSession{{ session.id }}">
115
+ <div class="accordion-body">
116
+ {% if session.summary_notes %}
117
+ <p><strong>Notes:</strong> {{ session.summary_notes }}</p>
118
+ {% endif %}
119
+ {% if session.generated_questions %}
120
+ <ul class="list-group list-group-flush">
121
+ {% for q in session.generated_questions %}
122
+ <li class="list-group-item">{{ q.question_text }} <small class="text-muted">({{ q.category }})</small></li>
123
+ {% endfor %}
124
+ </ul>
125
+ {% else %}
126
+ <p class="text-muted">No questions recorded for this session.</p>
127
+ {% endif %}
128
+ </div>
129
+ </div>
130
+ </div>
131
+ </div>
132
+ {% endfor %}
133
+ {% else %}
134
+ <p class="text-muted text-center mt-3">No interrogation sessions found. Click button to generate questions for a new session.</p>
135
+ {% endif %}
136
+ </div>
137
+ </div>
138
+ </div>
139
+
140
+ <div class="card mb-3">
141
+ <div class="card-header">
142
+ <i class="bi bi-file-earmark-medical-fill me-2"></i>Generate New Report (LLM)
143
+ </div>
144
+ <div class="card-body">
145
+ <form id="generateReportForm">
146
+ <div class="mb-3">
147
+ <label for="country_id_report" class="form-label">Select Country for Report Recommendations: <span class="text-danger">*</span></label>
148
+ <select class="form-select" id="country_id_report" name="country_id" required>
149
+ <option value="">Select Country</option>
150
+ {% for country in countries %}
151
+ <option value="{{ country.id }}" {% if case.country_context and case.country_context.id == country.id %}selected{% endif %}>{{ country.name }}</option>
152
+ {% endfor %}
153
+ </select>
154
+ </div>
155
+ <div class="mb-3">
156
+ <label for="interrogation_summary_manual" class="form-label">Additional Interrogation Summary (Optional):</label>
157
+ <textarea class="form-control" id="interrogation_summary_manual" name="interrogation_summary" rows="4" placeholder="Add any manual notes or summary of the interrogation to be included in the report generation prompt. Data from saved questions/answers will be automatically included."></textarea>
158
+ </div>
159
+ <button type="submit" class="btn btn-success w-100" data-case-id="{{ case.id }}">
160
+ <i class="bi bi-lightning-charge-fill me-1"></i> Generate Report
161
+ </button>
162
+ </form>
163
+ <div id="reportResultArea" class="mt-3"></div>
164
+ </div>
165
+ </div>
166
+ </div>
167
+ </div>
168
+
169
+ {% endblock %}
170
+
171
+ {% block scripts %}
172
+ <script>
173
+ document.addEventListener('DOMContentLoaded', function() {
174
+ const generateQuestionsBtn = document.getElementById('generateQuestionsBtn');
175
+ const questionsArea = document.getElementById('questionsArea');
176
+
177
+ if(generateQuestionsBtn) {
178
+ generateQuestionsBtn.addEventListener('click', function() {
179
+ const caseId = this.dataset.caseId;
180
+ this.disabled = true;
181
+ this.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Generating...';
182
+
183
+ fetch(`/generate_questions/${caseId}`, { method: 'POST' })
184
+ .then(response => {
185
+ if (!response.ok) {
186
+ return response.json().then(err => { throw err; });
187
+ }
188
+ return response.json();
189
+ })
190
+ .then(data => {
191
+ if(data.error) {
192
+ alert('Error: ' + data.error);
193
+ return;
194
+ }
195
+ // Instead of replacing, we just inform and ask to refresh for simplicity now
196
+ // Or, ideally, dynamically add the new session and questions to the accordion
197
+ alert('Questions generated successfully! They have been saved to a new interrogation session. Please refresh the page to see them.');
198
+ window.location.reload(); // Simple refresh to show new data
199
+ })
200
+ .catch(error => {
201
+ console.error('Error generating questions:', error);
202
+ alert('Failed to generate questions: ' + (error.error || 'Unknown error'));
203
+ })
204
+ .finally(() => {
205
+ this.disabled = false;
206
+ this.innerHTML = '<i class="bi bi-magic me-1"></i> Generate Interrogation Questions (LLM)';
207
+ });
208
+ });
209
+ }
210
+
211
+ const generateReportForm = document.getElementById('generateReportForm');
212
+ const reportResultArea = document.getElementById('reportResultArea');
213
+
214
+ if(generateReportForm) {
215
+ generateReportForm.addEventListener('submit', function(event) {
216
+ event.preventDefault();
217
+ const submitButton = this.querySelector('button[type="submit"]');
218
+ const caseId = submitButton.dataset.caseId;
219
+ const countryId = document.getElementById('country_id_report').value;
220
+ const interrogationSummary = document.getElementById('interrogation_summary_manual').value;
221
+
222
+ if (!countryId) {
223
+ alert('Please select a country for the report recommendations.');
224
+ return;
225
+ }
226
+
227
+ submitButton.disabled = true;
228
+ submitButton.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Generating Report...';
229
+ reportResultArea.innerHTML = '<div class="alert alert-info">Generating report, please wait...</div>';
230
+
231
+ fetch(`/generate_report/${caseId}`, {
232
+ method: 'POST',
233
+ headers: {
234
+ 'Content-Type': 'application/json',
235
+ },
236
+ body: JSON.stringify({ country_id: countryId, interrogation_summary: interrogationSummary })
237
+ })
238
+ .then(response => {
239
+ if (!response.ok) {
240
+ return response.json().then(err => { throw err; });
241
+ }
242
+ return response.json();
243
+ })
244
+ .then(data => {
245
+ if(data.error) {
246
+ reportResultArea.innerHTML = `<div class="alert alert-danger">Error: ${data.error}</div>`;
247
+ return;
248
+ }
249
+ let reportJsonParsed = {};
250
+ try {
251
+ reportJsonParsed = JSON.parse(data.raw_json_report);
252
+ } catch (e) {
253
+ console.warn("Could not parse raw_json_report for pretty print", e);
254
+ reportJsonParsed = data.raw_json_report; // show as string if not parsable
255
+ }
256
+ reportResultArea.innerHTML = `<div class="alert alert-success">Report generated successfully! Report ID: ${data.report_id}</div> \
257
+ <h6>Raw LLM Output (JSON):</h6> \
258
+ <div class="json-display">${JSON.stringify(reportJsonParsed, null, 2)}</div> \
259
+ <p class="mt-2"><a href="/report/${data.report_id}" class="btn btn-info mt-2"><i class="bi bi-eye-fill me-1"></i>View Formatted Report</a></p><p class="text-muted"><small>Page will refresh to show the new report in the list below.</small></p>`;
260
+ setTimeout(() => { window.location.reload(); }, 3000); // Refresh to update list
261
+ })
262
+ .catch(error => {
263
+ console.error('Error generating report:', error);
264
+ reportResultArea.innerHTML = `<div class="alert alert-danger">Failed to generate report: ${(error.error || 'Unknown error')}. Check console.</div>`;
265
+ })
266
+ .finally(() => {
267
+ submitButton.disabled = false;
268
+ submitButton.innerHTML = '<i class="bi bi-lightning-charge-fill me-1"></i> Generate Report';
269
+ });
270
+ });
271
+ }
272
+ });
273
+ </script>
274
+ {% endblock %}
275
+
app/templates/cases.html ADDED
@@ -0,0 +1,199 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base_layout.html" %}
2
+
3
+ {% block title %}Manage Cases - InterroGen{% endblock %}
4
+
5
+ {% block head_extra %}
6
+ <style>
7
+ .case-status {
8
+ display: inline-block;
9
+ padding: 0.25rem 0.5rem;
10
+ border-radius: 0.25rem;
11
+ font-size: 0.75rem;
12
+ font-weight: 600;
13
+ }
14
+ .status-active {
15
+ background-color: rgba(22, 170, 255, 0.1);
16
+ color: var(--info);
17
+ }
18
+ .status-completed {
19
+ background-color: rgba(58, 196, 125, 0.1);
20
+ color: var(--success);
21
+ }
22
+ .status-pending {
23
+ background-color: rgba(247, 185, 36, 0.1);
24
+ color: var(--warning);
25
+ }
26
+ .status-closed {
27
+ background-color: rgba(108, 117, 125, 0.1); /* Using secondary color for closed */
28
+ color: var(--secondary);
29
+ }
30
+ </style>
31
+ {% endblock %}
32
+
33
+ {% block page_content %}
34
+ <div class="heading-container">
35
+ <h1 class="h3">Manage Cases</h1>
36
+ <button class="btn btn-primary" type="button" data-bs-toggle="modal" data-bs-target="#newCaseModal">
37
+ <i class="bi bi-plus-lg"></i> Create New Case
38
+ </button>
39
+ </div>
40
+
41
+ <!-- New Case Modal -->
42
+ <div class="modal fade" id="newCaseModal" tabindex="-1" aria-labelledby="newCaseModalLabel" aria-hidden="true">
43
+ <div class="modal-dialog modal-lg">
44
+ <div class="modal-content">
45
+ <form id="newCaseFormModal">
46
+ <div class="modal-header">
47
+ <h5 class="modal-title" id="newCaseModalLabel">Create New Case</h5>
48
+ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
49
+ </div>
50
+ <div class="modal-body">
51
+ <div class="mb-3">
52
+ <label for="modal_case_id_display" class="form-label">Case ID (e.g., C-2025XXXX - will be auto-generated if left blank)</label>
53
+ <input type="text" class="form-control" id="modal_case_id_display" name="case_id_display">
54
+ </div>
55
+ <div class="row">
56
+ <div class="col-md-6 mb-3">
57
+ <label for="modal_case_type" class="form-label">Case Type <span class="text-danger">*</span></label>
58
+ <input type="text" class="form-control" id="modal_case_type" name="case_type" required>
59
+ </div>
60
+ <div class="col-md-6 mb-3">
61
+ <label for="modal_suspect_name" class="form-label">Suspect Name <span class="text-danger">*</span></label>
62
+ <input type="text" class="form-control" id="modal_suspect_name" name="suspect_name" required>
63
+ </div>
64
+ </div>
65
+ <div class="mb-3">
66
+ <label for="modal_profile_details" class="form-label">Profile Details</label>
67
+ <textarea class="form-control" id="modal_profile_details" name="profile_details" rows="4"></textarea>
68
+ </div>
69
+ <div class="mb-3">
70
+ <label for="modal_evidence_summary" class="form-label">Evidence Summary</label>
71
+ <textarea class="form-control" id="modal_evidence_summary" name="evidence_summary" rows="4"></textarea>
72
+ </div>
73
+ <div class="mb-3">
74
+ <label for="modal_country_id_case" class="form-label">Country Context (for recommendations)</label>
75
+ <select class="form-select" id="modal_country_id_case" name="country_id">
76
+ <option value="">Select Country (Optional)</option>
77
+ {# Countries will be loaded by JavaScript #}
78
+ </select>
79
+ </div>
80
+ </div>
81
+ <div class="modal-footer">
82
+ <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
83
+ <button type="submit" class="btn btn-primary">Create Case</button>
84
+ </div>
85
+ </form>
86
+ </div>
87
+ </div>
88
+ </div>
89
+
90
+ <div class="card">
91
+ <div class="card-header">
92
+ Existing Cases
93
+ </div>
94
+ <div class="card-body p-0">
95
+ <div class="table-responsive">
96
+ <table class="table table-hover mb-0">
97
+ <thead>
98
+ <tr>
99
+ <th>Case ID</th>
100
+ <th>Suspect</th>
101
+ <th>Type</th>
102
+ <th>Status</th>
103
+ <th>Country</th>
104
+ <th>Created</th>
105
+ <th>Updated</th>
106
+ <th>Actions</th>
107
+ </tr>
108
+ </thead>
109
+ <tbody>
110
+ {% if cases %}
111
+ {% for case_item in cases %}
112
+ <tr>
113
+ <td><a href="{{ url_for(\'view_case\', case_id=case_item.id) }}">{{ case_item.case_id_display }}</a></td>
114
+ <td>{{ case_item.suspect_name }}</td>
115
+ <td>{{ case_item.case_type }}</td>
116
+ <td><span class="case-status status-{{ case_item.status.value.lower() }}">{{ case_item.status.value }}</span></td>
117
+ <td>{{ case_item.country_context.name if case_item.country_context else \'N/A\'}}</td>
118
+ <td>{{ case_item.created_at.strftime(\"%Y-%m-%d\") }}</td>
119
+ <td>{{ case_item.updated_at.strftime(\"%Y-%m-%d\") }}</td>
120
+ <td>
121
+ <a href="{{ url_for(\'view_case\', case_id=case_item.id) }}" class="btn btn-sm btn-outline-primary">View</a>
122
+ <!-- Add edit/delete buttons if needed -->
123
+ </td>
124
+ </tr>
125
+ {% endfor %}
126
+ {% else %}
127
+ <tr>
128
+ <td colspan="8" class="text-center text-muted py-4">No cases found. Create one to get started!</td>
129
+ </tr>
130
+ {% endif %}
131
+ </tbody>
132
+ </table>
133
+ </div>
134
+ </div>
135
+ {% if cases and cases.has_prev %}
136
+ <a href="{{ url_for(\'manage_cases\', page=cases.prev_num) }}">Previous</a>
137
+ {% endif %}
138
+ {% if cases and cases.has_next %}
139
+ <a href="{{ url_for(\'manage_cases\', page=cases.next_num) }}">Next</a>
140
+ {% endif %}
141
+ </div>
142
+
143
+ {% endblock %}
144
+
145
+ {% block scripts %}
146
+ <script>
147
+ document.addEventListener(\'DOMContentLoaded\', function() {
148
+ // Fetch countries for the modal dropdown
149
+ const countrySelectModal = document.getElementById(\'modal_country_id_case\');
150
+ if (countrySelectModal) {
151
+ fetch("{{ url_for(\'get_countries\') }}")
152
+ .then(response => response.json())
153
+ .then(data => {
154
+ data.forEach(country => {
155
+ const option = document.createElement(\'option\');
156
+ option.value = country.id;
157
+ option.textContent = country.name;
158
+ countrySelectModal.appendChild(option);
159
+ });
160
+ }).catch(error => console.error(\'Error fetching countries for modal:\', error));
161
+ }
162
+
163
+ const newCaseFormModal = document.getElementById(\'newCaseFormModal\');
164
+ if (newCaseFormModal) {
165
+ newCaseFormModal.addEventListener(\'submit\', function(event) {
166
+ event.preventDefault();
167
+ const formData = new FormData(newCaseFormModal);
168
+ fetch("{{ url_for(\'manage_cases\') }}", {
169
+ method: \'POST\',
170
+ body: formData
171
+ })
172
+ .then(response => {
173
+ if (!response.ok) {
174
+ return response.json().then(err => { throw err; });
175
+ }
176
+ return response.json();
177
+ })
178
+ .then(data => {
179
+ alert(data.message || \'Case created successfully!\');
180
+ var modal = bootstrap.Modal.getInstance(document.getElementById(\'newCaseModal\'));
181
+ modal.hide();
182
+ window.location.reload(); // Reload to see the new case
183
+ })
184
+ .catch(error => {
185
+ console.error(\'Error creating case:\', error);
186
+ let errorMsg = \'Error creating case.\';
187
+ if (error && error.error) { // Assuming error object has an error field
188
+ errorMsg = error.error;
189
+ } else if (error && error.message) {
190
+ errorMsg = error.message;
191
+ }
192
+ alert(errorMsg);
193
+ });
194
+ });
195
+ }
196
+ });
197
+ </script>
198
+ {% endblock %}
199
+
app/templates/dashboard.html ADDED
@@ -0,0 +1,250 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base_layout.html" %}
2
+
3
+ {% block title %}Dashboard - InterroGen{% endblock %}
4
+
5
+ {% block head_extra %}
6
+ <style>
7
+ .stats-card {
8
+ text-align: center;
9
+ padding: 1.5rem;
10
+ }
11
+ .stats-card i {
12
+ font-size: 2rem;
13
+ margin-bottom: 1rem;
14
+ }
15
+ .stats-number {
16
+ font-size: 2rem;
17
+ font-weight: 700;
18
+ margin-bottom: 0.5rem;
19
+ }
20
+ .stats-label {
21
+ color: var(--text-muted);
22
+ font-size: 0.875rem;
23
+ }
24
+ .region-badge {
25
+ display: inline-block;
26
+ font-size: 0.75rem;
27
+ padding: 0.25rem 0.5rem;
28
+ border-radius: 0.25rem;
29
+ margin-right: 0.5rem;
30
+ margin-bottom: 0.5rem;
31
+ background-color: rgba(63, 106, 216, 0.1);
32
+ color: var(--primary);
33
+ }
34
+ .region-card {
35
+ cursor: pointer;
36
+ transition: all 0.2s;
37
+ }
38
+ .region-card:hover {
39
+ transform: translateY(-3px);
40
+ box-shadow: 0 0.5rem 1rem rgba(0,0,0,0.08);
41
+ }
42
+ .table-card {
43
+ overflow: hidden;
44
+ }
45
+ .case-status {
46
+ display: inline-block;
47
+ padding: 0.25rem 0.5rem;
48
+ border-radius: 0.25rem;
49
+ font-size: 0.75rem;
50
+ font-weight: 600;
51
+ }
52
+ .status-active {
53
+ background-color: rgba(22, 170, 255, 0.1);
54
+ color: var(--info);
55
+ }
56
+ .status-completed {
57
+ background-color: rgba(58, 196, 125, 0.1);
58
+ color: var(--success);
59
+ }
60
+ .status-pending {
61
+ background-color: rgba(247, 185, 36, 0.1);
62
+ color: var(--warning);
63
+ }
64
+ .case-row {
65
+ cursor: pointer;
66
+ transition: background-color 0.15s;
67
+ }
68
+ .case-row:hover {
69
+ background-color: rgba(63, 106, 216, 0.05);
70
+ }
71
+ .progress {
72
+ height: 0.5rem;
73
+ border-radius: 0.25rem;
74
+ }
75
+ .chart-bar {
76
+ height: 0.5rem;
77
+ background-color: var(--primary);
78
+ border-radius: 0.25rem;
79
+ /* animation: fillUp 1s ease-out forwards; */ /* Animation can be added if desired */
80
+ }
81
+ .chart-container {
82
+ margin-top: 1rem;
83
+ width: 100%;
84
+ }
85
+ .chart-label {
86
+ display: flex;
87
+ justify-content: space-between;
88
+ margin-bottom: 0.25rem;
89
+ font-size: 0.75rem;
90
+ }
91
+ .region-icon {
92
+ width: 2.5rem;
93
+ height: 2.5rem;
94
+ border-radius: 50%;
95
+ background-color: rgba(63, 106, 216, 0.1);
96
+ color: var(--primary);
97
+ display: flex;
98
+ align-items: center;
99
+ justify-content: center;
100
+ font-size: 1.25rem;
101
+ margin-bottom: 0.75rem;
102
+ }
103
+ </style>
104
+ {% endblock %}
105
+
106
+ {% block page_content %}
107
+ <div class="heading-container">
108
+ <h1 class="h3">Dashboard</h1>
109
+ <div class="btn-group" role="group" aria-label="Time period">
110
+ <button type="button" class="btn btn-outline-secondary">Day</button>
111
+ <button type="button" class="btn btn-outline-secondary active">Week</button>
112
+ <button type="button" class="btn btn-outline-secondary">Month</button>
113
+ <button type="button" class="btn btn-outline-secondary">Year</button>
114
+ </div>
115
+ </div>
116
+
117
+ <!-- Stats row -->
118
+ <div class="row">
119
+ <div class="col-md-3">
120
+ <div class="card stats-card">
121
+ <div class="text-primary">
122
+ <i class="bi bi-folder2-open"></i>
123
+ </div>
124
+ <div class="stats-number">{{ active_cases_count | default("0") }}</div>
125
+ <div class="stats-label">Active Cases</div>
126
+ </div>
127
+ </div>
128
+ <div class="col-md-3">
129
+ <div class="card stats-card">
130
+ <div class="text-info">
131
+ <i class="bi bi-chat-square-text"></i>
132
+ </div>
133
+ <div class="stats-number">{{ total_interrogations | default("0") }}</div>
134
+ <div class="stats-label">Interrogations</div>
135
+ </div>
136
+ </div>
137
+ <div class="col-md-3">
138
+ <div class="card stats-card">
139
+ <div class="text-success">
140
+ <i class="bi bi-file-earmark-check"></i>
141
+ </div>
142
+ <div class="stats-number">{{ completed_reports_count | default("0") }}</div>
143
+ <div class="stats-label">Completed Reports</div>
144
+ </div>
145
+ </div>
146
+ <div class="col-md-3">
147
+ <div class="card stats-card">
148
+ <div class="text-warning">
149
+ <i class="bi bi-clock-history"></i>
150
+ </div>
151
+ <div class="stats-number">{{ resolution_rate | default("N/A") }}</div>
152
+ <div class="stats-label">Case Resolution Rate</div>
153
+ </div>
154
+ </div>
155
+ </div>
156
+
157
+ <!-- Recent cases and regional guidelines -->
158
+ <div class="row">
159
+ <div class="col-lg-8">
160
+ <div class="card table-card">
161
+ <div class="card-header">
162
+ Recent Cases
163
+ </div>
164
+ <div class="card-body p-0">
165
+ <div class="table-responsive">
166
+ <table class="table table-hover mb-0">
167
+ <thead>
168
+ <tr>
169
+ <th>Case ID</th>
170
+ <th>Suspect</th>
171
+ <th>Type</th>
172
+ <th>Status</th>
173
+ <th>Last Update</th>
174
+ <th>Action</th>
175
+ </tr>
176
+ </thead>
177
+ <tbody>
178
+ {% if recent_cases %}
179
+ {% for case in recent_cases %}
180
+ <tr class="case-row" onclick="window.location=\'{{ url_for('view_case', case_id=case.id) }}\';">
181
+ <td>{{ case.case_id_display }}</td>
182
+ <td>{{ case.suspect_name }}</td>
183
+ <td>{{ case.case_type }}</td>
184
+ <td><span class="case-status status-{{ case.status.value.lower() }}">{{ case.status.value }}</span></td>
185
+ <td>{{ case.updated_at.strftime("%Y-%m-%d %H:%M") }}</td>
186
+ <td><a href="{{ url_for("view_case", case_id=case.id) }}" class="btn btn-sm btn-outline-primary">View</a></td>
187
+ </tr>
188
+ {% endfor %}
189
+ {% else %}
190
+ <tr>
191
+ <td colspan="6" class="text-center text-muted py-4">No recent cases found.</td>
192
+ </tr>
193
+ {% endif %}
194
+ </tbody>
195
+ </table>
196
+ </div>
197
+ </div>
198
+ </div>
199
+ </div>
200
+ <div class="col-lg-4">
201
+ <div class="card">
202
+ <div class="card-header">
203
+ Regional Guidelines Overview
204
+ </div>
205
+ <div class="card-body">
206
+ {% if countries %}
207
+ {% for country in countries | sort(attribute=\'name
208
+
209
+ ') | list | batch(3) %}
210
+ <div class="row mb-2">
211
+ {% for c_item in country %}
212
+ <div class="col-4">
213
+ <a href="{{ url_for(\'regional_guidelines_page\', country_id=c_item.id) }}" class="text-decoration-none">
214
+ <div class="region-card p-2 text-center border rounded">
215
+ <div class="region-icon"><i class="bi bi-geo-alt-fill"></i></div>
216
+ <small>{{ c_item.name }}</small>
217
+ </div>
218
+ </a>
219
+ </div>
220
+ {% endfor %}
221
+ </div>
222
+ {% endfor %}
223
+ <a href="{{ url_for(\'regional_guidelines_page\') }}" class="btn btn-sm btn-outline-primary mt-2">View All Guidelines</a>
224
+ {% else %}
225
+ <p class="text-muted">No countries configured for regional guidelines.</p>
226
+ {% endif %}
227
+ </div>
228
+ </div>
229
+ <div class="card">
230
+ <div class="card-header">
231
+ Quick Actions
232
+ </div>
233
+ <div class="card-body">
234
+ <a href="{{ url_for(\'manage_cases\', action=\'new\') }}" class="btn btn-primary w-100 mb-2"> <i class="bi bi-plus-circle-fill me-2"></i>New Case</a>
235
+ <a href="{{ url_for(\'reports_list_page\') }}" class="btn btn-outline-secondary w-100 mb-2"><i class="bi bi-search me-2"></i>Find Report</a>
236
+ <a href="{{ url_for(\'interrogations_page\') }}" class="btn btn-outline-secondary w-100"><i class="bi bi-mic-fill me-2"></i>Start Interrogation</a>
237
+ </div>
238
+ </div>
239
+ </div>
240
+ </div>
241
+
242
+ {% endblock %}
243
+
244
+ {% block scripts %}
245
+ <script>
246
+ // Add any dashboard specific JS here if needed
247
+ console.log("Dashboard loaded");
248
+ </script>
249
+ {% endblock %}
250
+
app/templates/evidence.html ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base_layout.html" %}
2
+
3
+ {% block title %}Evidence Locker - InterroGen{% endblock %}
4
+
5
+ {% block page_content %}
6
+ <div class="heading-container">
7
+ <h1 class="h3">Evidence Locker</h1>
8
+ </div>
9
+
10
+ <div class="card">
11
+ <div class="card-header">
12
+ <i class="bi bi-archive-fill me-2"></i>Case Evidence Management
13
+ </div>
14
+ <div class="card-body">
15
+ <p class="text-muted">This section will provide a centralized location to manage and link evidence related to cases.</p>
16
+
17
+ <h5 class="mt-4">Features to be Implemented:</h5>
18
+ <ul>
19
+ <li>Upload and categorize evidence files (documents, images, videos, audio).</li>
20
+ <li>Link evidence to specific cases and interrogation sessions.</li>
21
+ <li>Maintain chain of custody information.</li>
22
+ <li>Search and filter evidence.</li>
23
+ </ul>
24
+ <p><em>This page is currently under development. Full functionality will be available soon.</em></p>
25
+ </div>
26
+ </div>
27
+
28
+ {% endblock %}
29
+
30
+ {% block scripts %}
31
+ <script>
32
+ // Add any evidence page specific JS here if needed
33
+ console.log("Evidence page loaded");
34
+ </script>
35
+ {% endblock %}
36
+
app/templates/help.html ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base_layout.html" %}
2
+
3
+ {% block title %}Help & Support - InterroGen{% endblock %}
4
+
5
+ {% block page_content %}
6
+ <div class="heading-container">
7
+ <h1 class="h3">Help & Support</h1>
8
+ </div>
9
+
10
+ <div class="card">
11
+ <div class="card-header">
12
+ <i class="bi bi-question-circle-fill me-2"></i>Frequently Asked Questions & Documentation
13
+ </div>
14
+ <div class="card-body">
15
+ <p class="text-muted">This section will provide help documentation, FAQs, and support resources for using the InterroGen application.</p>
16
+
17
+ <h5 class="mt-4">Topics to be Covered:</h5>
18
+ <ul>
19
+ <li>Getting Started Guide.</li>
20
+ <li>How to create and manage cases.</li>
21
+ <li>Generating and interpreting interrogation questions.</li>
22
+ <li>Generating and understanding investigation reports.</li>
23
+ <li>Using regional guidelines.</li>
24
+ <li>Troubleshooting common issues.</li>
25
+ <li>Contacting support.</li>
26
+ </ul>
27
+ <p><em>This page is currently under development. Comprehensive help resources will be available soon.</em></p>
28
+ </div>
29
+ </div>
30
+
31
+ {% endblock %}
32
+
33
+ {% block scripts %}
34
+ <script>
35
+ // Add any help page specific JS here if needed
36
+ console.log("Help page loaded");
37
+ </script>
38
+ {% endblock %}
39
+
app/templates/interrogations.html ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base_layout.html" %}
2
+
3
+ {% block title %}Interrogation Sessions - InterroGen{% endblock %}
4
+
5
+ {% block page_content %}
6
+ <div class="heading-container">
7
+ <h1 class="h3">Interrogation Sessions</h1>
8
+ {# Add a button or link to initiate a new interrogation if applicable, or link to cases #}
9
+ <a href="{{ url_for(\'manage_cases\') }}" class="btn btn-outline-primary"><i class="bi bi-folder2-open me-1"></i>View Cases</a>
10
+ </div>
11
+
12
+ <div class="card">
13
+ <div class="card-header">
14
+ <i class="bi bi-list-ul me-2"></i>All Recorded Interrogation Sessions
15
+ </div>
16
+ <div class="card-body p-0">
17
+ {% if sessions %}
18
+ <div class="table-responsive">
19
+ <table class="table table-hover mb-0">
20
+ <thead>
21
+ <tr>
22
+ <th>Session ID</th>
23
+ <th>Case ID</th>
24
+ <th>Suspect</th>
25
+ <th>Session Date</th>
26
+ <th>Questions Generated</th>
27
+ <th>Summary Notes</th>
28
+ <th>Actions</th>
29
+ </tr>
30
+ </thead>
31
+ <tbody>
32
+ {% for session in sessions | sort(attribute=\'session_date\
33
+ ', reverse=True) %}
34
+ <tr>
35
+ <td>{{ session.id }}</td>
36
+ <td><a href="{{ url_for(\'view_case\', case_id=session.case.id) }}">{{ session.case.case_id_display }}</a></td>
37
+ <td>{{ session.case.suspect_name }}</td>
38
+ <td>{{ session.session_date.strftime(\'%Y-%m-%d %H:%M\') }}</td>
39
+ <td>{{ session.generated_questions | length }}</td>
40
+ <td>{{ session.summary_notes | truncate(100, True) if session.summary_notes else \'N/A\' }}</td>
41
+ <td>
42
+ <a href="{{ url_for(\'view_case\', case_id=session.case.id) }}#accordionSession{{session.id}}" class="btn btn-sm btn-outline-primary">View Details in Case</a>
43
+ </td>
44
+ </tr>
45
+ {% endfor %}
46
+ </tbody>
47
+ </table>
48
+ </div>
49
+ {% else %}
50
+ <div class="text-center p-5">
51
+ <i class="bi bi-mic-mute-fill fs-1 text-muted"></i>
52
+ <p class="mt-3 text-muted">No interrogation sessions found in the system.</p>
53
+ <p>Interrogation questions are generated from the individual case detail pages.</p>
54
+ <a href="{{ url_for(\'manage_cases\') }}" class="btn btn-primary">Go to Cases</a>
55
+ </div>
56
+ {% endif %}
57
+ </div>
58
+ </div>
59
+
60
+ {% endblock %}
61
+
62
+ {% block scripts %}
63
+ <script>
64
+ // Add any interrogations page specific JS here if needed
65
+ console.log("Interrogations page loaded");
66
+ </script>
67
+ {% endblock %}
68
+
app/templates/regional_guidelines.html ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base_layout.html" %}
2
+
3
+ {% block title %}Regional Guidelines - InterroGen{% endblock %}
4
+
5
+ {% block page_content %}
6
+ <div class="heading-container">
7
+ <h1 class="h3">Regional Guidelines</h1>
8
+ </div>
9
+
10
+ <div class="card">
11
+ <div class="card-header">
12
+ <i class="bi bi-globe me-2"></i>Guidelines by Country/Region
13
+ </div>
14
+ <div class="card-body">
15
+ <p class="text-muted">This section will provide specific legal and procedural guidelines relevant to interrogations and reporting for different countries and regions.</p>
16
+
17
+ {% if countries %}
18
+ <div class="list-group">
19
+ {% for country in countries %}
20
+ <a href="#" class="list-group-item list-group-item-action">
21
+ {{ country.name }} ({{ country.region }})
22
+ {# Placeholder for link to specific guideline details for this country #}
23
+ <small class="d-block text-muted">Details for {{ country.name }} coming soon.</small>
24
+ </a>
25
+ {% endfor %}
26
+ </div>
27
+ {% else %}
28
+ <p class="text-muted">No countries have been configured in the system yet.</p>
29
+ {% endif %}
30
+
31
+ <h5 class="mt-4">Content to be Added:</h5>
32
+ <ul>
33
+ <li>Details on legal frameworks for selected countries.</li>
34
+ <li>Best practices for interrogation in different jurisdictions.</li>
35
+ <li>Reporting standards and requirements by region.</li>
36
+ <li>Cultural sensitivity notes.</li>
37
+ </ul>
38
+ <p><em>This page is currently under development. Full content will be available soon.</em></p>
39
+ </div>
40
+ </div>
41
+
42
+ {% endblock %}
43
+
44
+ {% block scripts %}
45
+ <script>
46
+ // Add any regional guidelines page specific JS here if needed
47
+ console.log("Regional Guidelines page loaded");
48
+ </script>
49
+ {% endblock %}
50
+
app/templates/report_detail.html ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base_layout.html" %}
2
+
3
+ {% block title %}Report Details - InterroGen{% endblock %}
4
+
5
+ {% block head_extra %}
6
+ <style>
7
+ .json-display {
8
+ background-color: #f8f9fa;
9
+ border: 1px solid #dee2e6;
10
+ padding: 15px;
11
+ border-radius: 5px;
12
+ white-space: pre-wrap;
13
+ word-wrap: break-word;
14
+ max-height: 600px; /* Increased height */
15
+ overflow-y: auto;
16
+ }
17
+ .report-summary-pre {
18
+ background-color: #f8f9fa;
19
+ border: 1px solid #eee;
20
+ padding: 10px;
21
+ border-radius: 4px;
22
+ white-space: pre-wrap;
23
+ word-wrap: break-word;
24
+ }
25
+ </style>
26
+ {% endblock %}
27
+
28
+ {% block page_content %}
29
+ <div class="heading-container">
30
+ <h1 class="h3">Report Details for Case: <a href="{{ url_for('view_case', case_id=report.case.id) }}">{{ report.case.case_id_display }}</a></h1>
31
+ <div>
32
+ <a href="{{ url_for('view_case', case_id=report.case.id) }}" class="btn btn-outline-secondary me-2"><i class="bi bi-arrow-left-circle me-1"></i>Back to Case</a>
33
+ <a href="{{ url_for('reports_list_page') }}" class="btn btn-outline-secondary"><i class="bi bi-list-task me-1"></i>All Reports</a>
34
+ </div>
35
+ </div>
36
+
37
+ <div class="card mb-3">
38
+ <div class="card-header">
39
+ <i class="bi bi-clipboard-data me-2"></i>Report Information
40
+ </div>
41
+ <div class="card-body">
42
+ <dl class="row">
43
+ <dt class="col-sm-3">Report ID:</dt>
44
+ <dd class="col-sm-9">{{ report.id }}</dd>
45
+
46
+ <dt class="col-sm-3">Generated At:</dt>
47
+ <dd class="col-sm-9">{{ report.generated_at.strftime('%Y-%m-%d %H:%M:%S') }}</dd>
48
+
49
+ <dt class="col-sm-3">Country Context:</dt>
50
+ <dd class="col-sm-9">{{ report.report_country_context.name if report.report_country_context else 'N/A' }}</dd>
51
+ </dl>
52
+ <hr>
53
+ <h6>Brief Summary (from LLM via DB):</h6>
54
+ <div class="report-summary-pre">{{ report.report_content_summary if report.report_content_summary else 'No summary available.' }}</div>
55
+
56
+ <h6 class="mt-3">Recommendations (from LLM via DB):</h6>
57
+ <div class="report-summary-pre">{{ report.recommendations if report.recommendations else 'No recommendations available.' }}</div>
58
+ </div>
59
+ </div>
60
+
61
+ <div class="card mb-3">
62
+ <div class="card-header">
63
+ <i class="bi bi-braces me-2"></i>Full LLM Generated Report (Raw JSON Output)
64
+ </div>
65
+ <div class="card-body">
66
+ <p class="text-muted small">This is the raw JSON data received from the language model. The application attempts to parse this for structured display and for populating the summary and recommendations above.</p>
67
+ <div class="json-display">
68
+ {% if report_data and not report_data.error %}
69
+ {{ report_data | tojson(indent=2) }}
70
+ {% elif report_data and report_data.error %}
71
+ <div class="alert alert-warning">Error parsing report data: {{ report_data.error }}</div>
72
+ <pre>{{ report_data.raw }}</pre>
73
+ {% else %}
74
+ <pre>{{ report.llm_json_output }}</pre>
75
+ {% endif %}
76
+ </div>
77
+ </div>
78
+ </div>
79
+
80
+ {% endblock %}
81
+
82
+ {% block scripts %}
83
+ <script>
84
+ // Add any report detail specific JS here if needed
85
+ console.log("Report detail page loaded for report ID: {{ report.id }}");
86
+ </script>
87
+ {% endblock %}
88
+
app/templates/reports_list.html ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base_layout.html" %}
2
+
3
+ {% block title %}All Reports - InterroGen{% endblock %}
4
+
5
+ {% block page_content %}
6
+ <div class="heading-container">
7
+ <h1 class="h3">All Generated Reports</h1>
8
+ {# Optional: Add a button to quickly navigate to case creation or other relevant actions #}
9
+ <a href="{{ url_for(\'manage_cases\', action=\'new\') }}" class="btn btn-primary"><i class="bi bi-plus-circle-fill me-1"></i>Create New Case</a>
10
+ </div>
11
+
12
+ <div class="card">
13
+ <div class="card-header">
14
+ <i class="bi bi-archive-fill me-2"></i>List of Reports
15
+ </div>
16
+ <div class="card-body p-0">
17
+ {% if reports %}
18
+ <div class="table-responsive">
19
+ <table class="table table-hover mb-0">
20
+ <thead>
21
+ <tr>
22
+ <th>Report ID</th>
23
+ <th>Case ID</th>
24
+ <th>Suspect</th>
25
+ <th>Country Context</th>
26
+ <th>Generated At</th>
27
+ <th>Actions</th>
28
+ </tr>
29
+ </thead>
30
+ <tbody>
31
+ {% for report in reports | sort(attribute=\'generated_at\', reverse=True) %}
32
+ <tr>
33
+ <td>{{ report.id }}</td>
34
+ <td><a href="{{ url_for(\'view_case\', case_id=report.case.id) }}">{{ report.case.case_id_display }}</a></td>
35
+ <td>{{ report.case.suspect_name }}</td>
36
+ <td>{{ report.report_country_context.name if report.report_country_context else \'N/A\' }}</td>
37
+ <td>{{ report.generated_at.strftime(\'%Y-%m-%d %H:%M\') }}</td>
38
+ <td>
39
+ <a href="{{ url_for(\'view_report\', report_id=report.id) }}" class="btn btn-sm btn-outline-primary">View Report</a>
40
+ </td>
41
+ </tr>
42
+ {% endfor %}
43
+ </tbody>
44
+ </table>
45
+ </div>
46
+ {% else %}
47
+ <div class="text-center p-5">
48
+ <i class="bi bi-journal-x fs-1 text-muted"></i>
49
+ <p class="mt-3 text-muted">No reports found in the system.</p>
50
+ <p>Reports are generated from individual case detail pages after interrogation questions have been processed.</p>
51
+ <a href="{{ url_for(\'manage_cases\') }}" class="btn btn-primary">Go to Cases</a>
52
+ </div>
53
+ {% endif %}
54
+ </div>
55
+ </div>
56
+
57
+ {% endblock %}
58
+
59
+ {% block scripts %}
60
+ <script>
61
+ // Add any reports list page specific JS here if needed
62
+ console.log("Reports list page loaded");
63
+ </script>
64
+ {% endblock %}
65
+
app/templates/settings.html ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base_layout.html" %}
2
+
3
+ {% block title %}Settings - InterroGen{% endblock %}
4
+
5
+ {% block page_content %}
6
+ <div class="heading-container">
7
+ <h1 class="h3">Application Settings</h1>
8
+ </div>
9
+
10
+ <div class="card">
11
+ <div class="card-header">
12
+ <i class="bi bi-gear-fill me-2"></i>Configuration
13
+ </div>
14
+ <div class="card-body">
15
+ <p class="text-muted">This section will allow administrators to configure various aspects of the InterroGen application.</p>
16
+
17
+ <h5 class="mt-4">Potential Settings:</h5>
18
+ <ul>
19
+ <li>User management and authentication settings.</li>
20
+ <li>LLM provider and model selection (if applicable in the future).</li>
21
+ <li>Default country/region settings.</li>
22
+ <li>Notification preferences.</li>
23
+ <li>Data retention policies.</li>
24
+ </ul>
25
+ <p><em>This page is currently under development. Full settings management will be available soon.</em></p>
26
+ </div>
27
+ </div>
28
+
29
+ {% endblock %}
30
+
31
+ {% block scripts %}
32
+ <script>
33
+ // Add any settings page specific JS here if needed
34
+ console.log("Settings page loaded");
35
+ </script>
36
+ {% endblock %}
37
+
app/templates/team.html ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base_layout.html" %}
2
+
3
+ {% block title %}Team - InterroGen{% endblock %}
4
+
5
+ {% block page_content %}
6
+ <div class="heading-container">
7
+ <h1 class="h3">Team Management</h1>
8
+ </div>
9
+
10
+ <div class="card">
11
+ <div class="card-header">
12
+ <i class="bi bi-people-fill me-2"></i>Team Members
13
+ </div>
14
+ <div class="card-body">
15
+ <p class="text-muted">This section will allow for managing team members, roles, and permissions within the InterroGen application.</p>
16
+
17
+ <h5 class="mt-4">Features to be Implemented:</h5>
18
+ <ul>
19
+ <li>View list of team members.</li>
20
+ <li>Add new team members.</li>
21
+ <li>Edit team member details and roles.</li>
22
+ <li>Define user roles and permissions.</li>
23
+ </ul>
24
+ <p><em>This page is currently under development. Full functionality will be available soon.</em></p>
25
+ </div>
26
+ </div>
27
+
28
+ {% endblock %}
29
+
30
+ {% block scripts %}
31
+ <script>
32
+ // Add any team page specific JS here if needed
33
+ console.log("Team page loaded");
34
+ </script>
35
+ {% endblock %}
36
+
app/utils/__pycache__/groq_client.cpython-311.pyc ADDED
Binary file (10.7 kB). View file
 
app/utils/groq_client.py ADDED
@@ -0,0 +1,192 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ from groq import Groq
4
+ from flask import current_app
5
+
6
+ class GroqClient:
7
+ def __init__(self, api_key=None):
8
+ self.api_key = api_key or current_app.config.get("GROQ_API_KEY")
9
+ if not self.api_key:
10
+ raise ValueError("Groq API key not found. Please set GROQ_API_KEY in config.")
11
+ self.client = Groq(api_key=self.api_key)
12
+ self.model = "llama-3.1-70b-versatile" # Corrected model name based on common Groq offerings, will use this for now.
13
+ # User specified llama-3.3-70b-versatile, which might be a typo or a very new model.
14
+ # If llama-3.1-70b-versatile is not available, I will try to find the closest match or inform the user.
15
+
16
+ def generate_interrogation_questions(self, case_details: str, suspect_profile: str, evidence_summary: str) -> list[str]:
17
+ """
18
+ Generates at least 10 complex, cross-investigation style interrogation questions.
19
+ """
20
+ prompt = f"""You are an expert interrogation question writer for law enforcement agencies.
21
+ Based on the following case information, generate at least 10 complex and insightful interrogation questions designed to uncover the truth, check for inconsistencies, and probe the suspect's knowledge and involvement. The questions should be suitable for a formal interrogation setting and encourage detailed responses rather than simple yes/no answers. Employ cross-investigation techniques where appropriate.
22
+
23
+ Case Details: {case_details}
24
+ Suspect Profile: {suspect_profile}
25
+ Evidence Summary: {evidence_summary}
26
+
27
+ Generate exactly 10 to 15 questions. Each question should be on a new line. Do not number the questions.
28
+ Example Question Format:
29
+ Can you explain your whereabouts on the night of the incident in detail, starting from 6 PM until midnight?
30
+ How do you account for the discrepancies between your statement and the evidence found at the scene?
31
+ What is your relationship with the other individuals mentioned in this case?
32
+
33
+ Generated Questions:
34
+ """
35
+ try:
36
+ chat_completion = self.client.chat.completions.create(
37
+ messages=[
38
+ {
39
+ "role": "system",
40
+ "content": "You are an expert interrogation question writer."
41
+ },
42
+ {
43
+ "role": "user",
44
+ "content": prompt,
45
+ }
46
+ ],
47
+ model=self.model,
48
+ temperature=0.7,
49
+ max_tokens=1024,
50
+ top_p=1,
51
+ stop=None,
52
+ )
53
+ response_content = chat_completion.choices[0].message.content
54
+ questions = [q.strip() for q in response_content.strip().split("\n") if q.strip()]
55
+ # Ensure at least 10 questions, if not, add some generic ones or re-prompt (simplified for now)
56
+ if len(questions) < 10:
57
+ # Fallback or error handling needed here if LLM doesn't provide enough
58
+ print(f"Warning: LLM generated only {len(questions)} questions. Expected at least 10.")
59
+ # Add generic placeholders if needed, or raise an error
60
+ # For now, we'll return what we have, but this should be more robust
61
+ return questions[:15] # Return up to 15 questions
62
+ except Exception as e:
63
+ current_app.logger.error(f"Error generating interrogation questions from Groq: {e}")
64
+ # Fallback: return generic questions if API fails
65
+ return [
66
+ "Can you describe your activities on the day of the incident?",
67
+ "What is your relationship with the victim/complainant?",
68
+ "Were you present at the location where the incident occurred?",
69
+ "Do you have any information that could help this investigation?",
70
+ "Is there anything you would like to add or clarify regarding your previous statements?",
71
+ "Can you provide any alibi for the time of the incident?",
72
+ "Who else might have information relevant to this case?",
73
+ "What was your initial reaction upon learning about this incident?",
74
+ "Have you discussed this matter with anyone else? If so, with whom?",
75
+ "Is there any reason someone might falsely implicate you in this matter?"
76
+ ]
77
+
78
+ def generate_report_and_recommendations(self, interrogation_summary: str, profile_details: str, evidence_summary: str, selected_country_name: str) -> str:
79
+ """
80
+ Generates a structured investigation report and recommendations in JSON format.
81
+ The JSON should follow a generic investigation agency standard.
82
+ """
83
+ prompt = f"""You are an AI assistant for a law enforcement agency, tasked with generating a structured investigation report.
84
+ Based on the provided information, generate a comprehensive investigation report in JSON format. The report should follow a globally accepted standard structure for investigation reports.
85
+ The JSON output must include these top-level keys: "caseSummary", "incidentDetails", "personsInvolved", "evidenceCollected", "interrogationSummary", "findingsAndInferences", "recommendations".
86
+ Under "recommendations", provide actionable steps and considerations, taking into account that the legal context is for '{selected_country_name}'.
87
+
88
+ Interrogation Summary (includes questions, answers, and observations): {interrogation_summary}
89
+ Suspect Profile Details: {profile_details}
90
+ Evidence Summary: {evidence_summary}
91
+ Country for Legal Context of Recommendations: {selected_country_name}
92
+
93
+ Ensure the entire output is a single, valid JSON object. Do not include any text outside the JSON structure.
94
+
95
+ Example JSON structure (fill with relevant details based on input):
96
+ {{
97
+ "caseSummary": {{ "caseID": "C-XXXX", "crimeType": "Type of Crime", "status": "Current Status", "briefOverview": "A brief overview of the case." }},
98
+ "incidentDetails": {{ "date": "YYYY-MM-DD", "time": "HH:MM", "location": "Location of incident", "narrative": "Detailed narrative of the incident." }},
99
+ "personsInvolved": {{ "suspects": [{{ "name": "Suspect Name", "role": "Suspect", "details": "Relevant details" }}], "victims": [{{ "name": "Victim Name", "role": "Victim", "details": "Relevant details" }}], "witnesses": [{{ "name": "Witness Name", "role": "Witness", "details": "Relevant details" }}] }},
100
+ "evidenceCollected": {{ "physical": ["Item 1", "Item 2"], "testimonial": ["Statement 1"], "digital": ["File A.jpg"] }},
101
+ "interrogationSummary": {{ "subjectName": "Suspect Name", "dateOfInterrogation": "YYYY-MM-DD", "keyStatements": ["Statement A", "Statement B"], "observations": "Behavioral observations during interrogation." }},
102
+ "findingsAndInferences": {{ "keyFindings": ["Finding 1", "Finding 2"], "inferences": ["Inference A", "Inference B"], "investigatorAnalysis": "Overall analysis by the investigator." }},
103
+ "recommendations": {{ "chargesToBeFiled": ["Charge 1 (Specific to {selected_country_name} law)"], "furtherActions": ["Action A", "Action B (Consider {selected_country_name} procedures)"], "legalConsiderations_{selected_country_name}": "Specific legal points relevant to {selected_country_name}." }}
104
+ }}
105
+ """
106
+ try:
107
+ chat_completion = self.client.chat.completions.create(
108
+ messages=[
109
+ {
110
+ "role": "system",
111
+ "content": "You are an AI assistant that generates structured investigation reports in JSON format."
112
+ },
113
+ {
114
+ "role": "user",
115
+ "content": prompt,
116
+ }
117
+ ],
118
+ model=self.model,
119
+ response_format={"type": "json_object"}, # Request JSON output
120
+ temperature=0.5,
121
+ max_tokens=4096, # Allow for larger JSON output
122
+ top_p=1,
123
+ stop=None,
124
+ )
125
+ json_output = chat_completion.choices[0].message.content
126
+ # Validate if the output is valid JSON
127
+ try:
128
+ json.loads(json_output)
129
+ return json_output
130
+ except json.JSONDecodeError as je:
131
+ current_app.logger.error(f"Groq returned invalid JSON for report: {je}\nContent: {json_output}")
132
+ # Fallback: return an error JSON or a template
133
+ error_json = {{
134
+ "error": "Failed to generate valid JSON report from LLM.",
135
+ "details": str(je),
136
+ "received_content": json_output
137
+ }}
138
+ return json.dumps(error_json)
139
+
140
+ except Exception as e:
141
+ current_app.logger.error(f"Error generating report from Groq: {e}")
142
+ # Fallback: return an error JSON
143
+ error_json = {{
144
+ "error": "Failed to generate report due to API error.",
145
+ "details": str(e)
146
+ }}
147
+ return json.dumps(error_json)
148
+
149
+ # Example usage (for testing outside Flask app context):
150
+ if __name__ == '__main__':
151
+ # This part won't run directly without a Flask app context for current_app.config
152
+ # For standalone testing, you'd pass the API key directly or mock current_app
153
+ print("This script is intended to be used within a Flask application context.")
154
+ # Mock current_app for standalone testing
155
+ class MockApp:
156
+ def __init__(self):
157
+ self.config = {"GROQ_API_KEY": "YOUR_GROQ_API_KEY_HERE"} # Replace with your key for testing
158
+ self.logger = lambda: None # Mock logger
159
+ self.logger.error = print
160
+
161
+ # current_app = MockApp() # Uncomment and set key for standalone test
162
+
163
+ # if current_app.config["GROQ_API_KEY"] != "YOUR_GROQ_API_KEY_HERE":
164
+ # client = GroqClient(api_key=current_app.config["GROQ_API_KEY"])
165
+
166
+ # print("--- Testing Question Generation ---")
167
+ # questions = client.generate_interrogation_questions(
168
+ # case_details="A case involving financial fraud where large sums of money were embezzled from a company over 6 months.",
169
+ # suspect_profile="The company's CFO, has access to all financial records, known to have gambling debts.",
170
+ # evidence_summary="Suspicious transactions traced to an offshore account linked to the CFO. No direct confession yet."
171
+ # )
172
+ # print("Generated Questions:")
173
+ # for q_idx, q_text in enumerate(questions):
174
+ # print(f"{q_idx+1}. {q_text}")
175
+
176
+ # print("\n--- Testing Report Generation ---")
177
+ # report_json = client.generate_report_and_recommendations(
178
+ # interrogation_summary="Suspect was evasive. Denied knowledge of offshore accounts initially, but became visibly nervous when presented with transaction details. Key Q: 'Can you explain these transfers to the Cayman Islands account?' A: 'I... I don't recall those specific transactions right now.'",
179
+ # profile_details="John Doe, CFO, 15 years with company. Recently divorced, known to frequent casinos.",
180
+ # evidence_summary="Bank statements show regular transfers to account XZY123 in Cayman Islands. Digital forensics found deleted emails discussing large cash withdrawals.",
181
+ # selected_country_name="USA"
182
+ # )
183
+ # print("Generated Report (JSON):")
184
+ # print(report_json)
185
+ # try:
186
+ # parsed_report = json.loads(report_json)
187
+ # print("\nReport JSON is valid.")
188
+ # except json.JSONDecodeError:
189
+ # print("\nReport JSON is INVALID.")
190
+ # else:
191
+ # print("Please set your GROQ_API_KEY in the MockApp for standalone testing.")
192
+