.DS_Store ADDED
Binary file (6.15 kB). View file
 
.gitattributes CHANGED
@@ -33,4 +33,3 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
- app/frontend/logo.png filter=lfs diff=lfs merge=lfs -text
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
Dockerfile CHANGED
@@ -32,4 +32,4 @@ EXPOSE 7860
32
  # Run the FastAPI app from the backend directory so local imports resolve
33
  WORKDIR $HOME/app/app/backend
34
 
35
- CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
 
32
  # Run the FastAPI app from the backend directory so local imports resolve
33
  WORKDIR $HOME/app/app/backend
34
 
35
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
README.md CHANGED
@@ -1,11 +1,13 @@
1
  ---
2
- title: LionGuard
3
  emoji: 🦁
4
  colorFrom: red
5
  colorTo: yellow
6
- sdk: docker
7
- app_port: 7860
 
8
  pinned: false
 
9
  short_description: Localised Multilingual Moderation Classifier for Singapore
10
  ---
11
 
 
1
  ---
2
+ title: Lionguard 2 Demo
3
  emoji: 🦁
4
  colorFrom: red
5
  colorTo: yellow
6
+ sdk: gradio
7
+ sdk_version: 5.34.2
8
+ app_file: app.py
9
  pinned: false
10
+ header: mini
11
  short_description: Localised Multilingual Moderation Classifier for Singapore
12
  ---
13
 
app.py ADDED
@@ -0,0 +1,450 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ app.py
3
+ """
4
+
5
+ # Standard imports
6
+ import json
7
+ import os
8
+ import sys
9
+ import uuid
10
+ import asyncio
11
+ from datetime import datetime
12
+
13
+ # Third party imports
14
+ import openai
15
+ import gradio as gr
16
+ import gspread
17
+ from google.oauth2 import service_account
18
+ from transformers import AutoModel
19
+
20
+ # Local imports
21
+ from utils import get_embeddings
22
+
23
+ # --- Categories
24
+ CATEGORIES = {
25
+ "binary": ["binary"],
26
+ "hateful": ["hateful_l1", "hateful_l2"],
27
+ "insults": ["insults"],
28
+ "sexual": [
29
+ "sexual_l1",
30
+ "sexual_l2",
31
+ ],
32
+ "physical_violence": ["physical_violence"],
33
+ "self_harm": ["self_harm_l1", "self_harm_l2"],
34
+ "all_other_misconduct": [
35
+ "all_other_misconduct_l1",
36
+ "all_other_misconduct_l2",
37
+ ],
38
+ }
39
+
40
+ # --- OpenAI Setup ---
41
+ # Create both sync and async clients
42
+ client = openai.OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
43
+ async_client = openai.AsyncOpenAI(api_key=os.getenv("OPENAI_API_KEY"))
44
+
45
+ # --- Model Loading ---
46
+ def load_lionguard2():
47
+ model = AutoModel.from_pretrained("govtech/lionguard-2", trust_remote_code=True)
48
+ return model
49
+
50
+ model = load_lionguard2()
51
+
52
+ # --- Google Sheets Config ---
53
+ GOOGLE_SHEET_URL = os.environ.get("GOOGLE_SHEET_URL")
54
+ GOOGLE_CREDENTIALS = os.environ.get("GCP_SERVICE_ACCOUNT")
55
+ RESULTS_SHEET_NAME = "results"
56
+ VOTES_SHEET_NAME = "votes"
57
+ CHATBOT_SHEET_NAME = "chatbot"
58
+
59
+ def get_gspread_client():
60
+ credentials = service_account.Credentials.from_service_account_info(
61
+ json.loads(GOOGLE_CREDENTIALS),
62
+ scopes=[
63
+ "https://www.googleapis.com/auth/spreadsheets",
64
+ "https://www.googleapis.com/auth/drive",
65
+ ],
66
+ )
67
+ return gspread.authorize(credentials)
68
+
69
+ def save_results_data(row):
70
+ try:
71
+ gc = get_gspread_client()
72
+ sheet = gc.open_by_url(GOOGLE_SHEET_URL)
73
+ ws = sheet.worksheet(RESULTS_SHEET_NAME)
74
+ ws.append_row(list(row.values()))
75
+ except Exception as e:
76
+ print(f"Error saving results data: {e}")
77
+
78
+ def save_vote_data(text_id, agree):
79
+ try:
80
+ gc = get_gspread_client()
81
+ sheet = gc.open_by_url(GOOGLE_SHEET_URL)
82
+ ws = sheet.worksheet(VOTES_SHEET_NAME)
83
+ vote_row = {
84
+ "datetime": datetime.now().isoformat(),
85
+ "text_id": text_id,
86
+ "agree": agree
87
+ }
88
+ ws.append_row(list(vote_row.values()))
89
+ except Exception as e:
90
+ print(f"Error saving vote data: {e}")
91
+
92
+ def log_chatbot_data(row):
93
+ try:
94
+ gc = get_gspread_client()
95
+ sheet = gc.open_by_url(GOOGLE_SHEET_URL)
96
+ ws = sheet.worksheet(CHATBOT_SHEET_NAME)
97
+ ws.append_row([
98
+ row["datetime"], row["text_id"], row["text"], row["binary_score"],
99
+ row["hateful_l1_score"], row["hateful_l2_score"], row["insults_score"],
100
+ row["sexual_l1_score"], row["sexual_l2_score"], row["physical_violence_score"],
101
+ row["self_harm_l1_score"], row["self_harm_l2_score"], row["aom_l1_score"],
102
+ row["aom_l2_score"], row["openai_score"]
103
+ ])
104
+ except Exception as e:
105
+ print(f"Error saving chatbot data: {e}")
106
+
107
+ # --- Classifier logic ---
108
+
109
+ def format_score_with_style(score_str):
110
+ if score_str == "-":
111
+ return '<span style="color: #9ca3af;">-</span>'
112
+ try:
113
+ score = float(score_str)
114
+ percentage = int(score * 100)
115
+ if score < 0.4:
116
+ return f'<span style="color: #34d399; font-weight:600;">👌 {percentage}%</span>'
117
+ elif 0.4 <= score < 0.7:
118
+ return f'<span style="color: #fbbf24; font-weight:600;">⚠️ {percentage}%</span>'
119
+ else:
120
+ return f'<span style="color: #fca5a5; font-weight:600;">🚨 {percentage}%</span>'
121
+ except:
122
+ return score_str
123
+
124
+ def format_binary_score(score):
125
+ percentage = int(score * 100)
126
+ if score < 0.4:
127
+ return f'<div style="background:linear-gradient(135deg, #065f46 0%, #047857 100%); color:#34d399; padding:24px 0; border-radius:20px; text-align:center; font-weight:900; border:3px solid #10b981; font-size:24px; margin:24px 0; box-shadow:0 4px 24px rgba(0,0,0,0.3);">✅ Pass ({percentage}/100)</div>'
128
+ elif 0.4 <= score < 0.7:
129
+ return f'<div style="background:linear-gradient(135deg, #92400e 0%, #b45309 100%); color:#fbbf24; padding:24px 0; border-radius:20px; text-align:center; font-weight:900; border:3px solid #f59e0b; font-size:24px; margin:24px 0; box-shadow:0 4px 24px rgba(0,0,0,0.3);">⚠️ Warning ({percentage}/100)</div>'
130
+ else:
131
+ return f'<div style="background:linear-gradient(135deg, #991b1b 0%, #b91c1c 100%); color:#fca5a5; padding:24px 0; border-radius:20px; text-align:center; font-weight:900; border:3px solid #ef4444; font-size:24px; margin:24px 0; box-shadow:0 4px 24px rgba(0,0,0,0.3);">🚨 Fail ({percentage}/100)</div>'
132
+
133
+ def analyze_text(text):
134
+ if not text.strip():
135
+ empty_html = '<div style="text-align: center; color: #9ca3af; padding: 30px; font-style: italic;">Enter text to analyze</div>'
136
+ return empty_html, empty_html, "", ""
137
+ try:
138
+ text_id = str(uuid.uuid4())
139
+ embeddings = get_embeddings([text])
140
+ results = model.predict(embeddings)
141
+ binary_score = results.get('binary', [0.0])[0]
142
+
143
+ main_categories = ['hateful', 'insults', 'sexual', 'physical_violence', 'self_harm', 'all_other_misconduct']
144
+ categories_html = []
145
+ max_scores = {}
146
+ for category in main_categories:
147
+ subcategories = CATEGORIES[category]
148
+ category_name = category.replace('_', ' ').title()
149
+ category_emojis = {
150
+ 'Hateful': '🤬',
151
+ 'Insults': '💢',
152
+ 'Sexual': '🔞',
153
+ 'Physical Violence': '⚔️',
154
+ 'Self Harm': '☹️',
155
+ 'All Other Misconduct': '🙅‍♀️'
156
+ }
157
+ category_display = f"{category_emojis.get(category_name, '📝')} {category_name}"
158
+ level_scores = [results.get(subcategory_key, [0.0])[0] for subcategory_key in subcategories]
159
+ max_score = max(level_scores) if level_scores else 0.0
160
+ max_scores[category] = max_score
161
+ categories_html.append(f'''
162
+ <tr>
163
+ <td>{category_display}</td>
164
+ <td style="text-align: center;">{format_score_with_style(f"{max_score:.4f}")}</td>
165
+ </tr>
166
+ ''')
167
+
168
+ html_table = f'''
169
+ <table style="width:100%">
170
+ <thead>
171
+ <tr><th>Category</th><th>Score</th></tr>
172
+ </thead>
173
+ <tbody>
174
+ {''.join(categories_html)}
175
+ </tbody>
176
+ </table>
177
+ '''
178
+
179
+ # Save to Google Sheets if enabled
180
+ if GOOGLE_SHEET_URL and GOOGLE_CREDENTIALS:
181
+ results_row = {
182
+ "datetime": datetime.now().isoformat(),
183
+ "text_id": text_id,
184
+ "text": text,
185
+ "binary_score": binary_score,
186
+ }
187
+ for category in main_categories:
188
+ results_row[f"{category}_max"] = max_scores[category]
189
+ save_results_data(results_row)
190
+
191
+ voting_html = '<div>Help improve LionGuard2! Rate the analysis below.</div>'
192
+ return format_binary_score(binary_score), html_table, text_id, voting_html
193
+
194
+ except Exception as e:
195
+ error_msg = f"Error analyzing text: {str(e)}"
196
+ return f'<div style="color: #fca5a5;">❌ {error_msg}</div>', '', '', ''
197
+
198
+ def vote_thumbs_up(text_id):
199
+ if text_id and GOOGLE_SHEET_URL and GOOGLE_CREDENTIALS:
200
+ save_vote_data(text_id, True)
201
+ return '<div style="color: #34d399; font-weight:700;">🎉 Thank you!</div>'
202
+ return '<div>Voting not available or analysis not yet run.</div>'
203
+
204
+ def vote_thumbs_down(text_id):
205
+ if text_id and GOOGLE_SHEET_URL and GOOGLE_CREDENTIALS:
206
+ save_vote_data(text_id, False)
207
+ return '<div style="color: #fca5a5; font-weight:700;">📝 Thanks for the feedback!</div>'
208
+ return '<div>Voting not available or analysis not yet run.</div>'
209
+
210
+ # --- Guardrail Comparison logic (ASYNC VERSION) ---
211
+
212
+ async def get_openai_response_async(message, system_prompt="You are a helpful assistant."):
213
+ """Async version of OpenAI API call"""
214
+ try:
215
+ response = await async_client.chat.completions.create(
216
+ model="gpt-4.1-nano",
217
+ messages=[
218
+ {"role": "system", "content": system_prompt},
219
+ {"role": "user", "content": message}
220
+ ],
221
+ max_tokens=500,
222
+ temperature=0,
223
+ seed=42,
224
+ )
225
+ return response.choices[0].message.content
226
+ except Exception as e:
227
+ return f"Error: {str(e)}. Please check your OpenAI API key."
228
+
229
+ async def openai_moderation_async(message):
230
+ """Async version of OpenAI moderation"""
231
+ try:
232
+ response = await async_client.moderations.create(input=message)
233
+ return response.results[0].flagged
234
+ except Exception as e:
235
+ print(f"Error in OpenAI moderation: {e}")
236
+ return False
237
+
238
+ def lionguard_2_sync(message, threshold=0.5):
239
+ """LionGuard remains sync as it's using a local model"""
240
+ try:
241
+ embeddings = get_embeddings([message])
242
+ results = model.predict(embeddings)
243
+ binary_prob = results['binary'][0]
244
+ return binary_prob > threshold, binary_prob
245
+ except Exception as e:
246
+ print(f"Error in LionGuard 2: {e}")
247
+ return False, 0.0
248
+
249
+ async def process_no_moderation(message, history_no_mod):
250
+ """Process message without moderation"""
251
+ no_mod_response = await get_openai_response_async(message)
252
+ history_no_mod.append({"role": "user", "content": message})
253
+ history_no_mod.append({"role": "assistant", "content": no_mod_response})
254
+ return history_no_mod
255
+
256
+ async def process_openai_moderation(message, history_openai):
257
+ """Process message with OpenAI moderation"""
258
+ openai_flagged = await openai_moderation_async(message)
259
+ history_openai.append({"role": "user", "content": message})
260
+ if openai_flagged:
261
+ openai_response = "🚫 This message has been flagged by OpenAI moderation"
262
+ history_openai.append({"role": "assistant", "content": openai_response})
263
+ else:
264
+ openai_response = await get_openai_response_async(message)
265
+ history_openai.append({"role": "assistant", "content": openai_response})
266
+ return history_openai
267
+
268
+ async def process_lionguard(message, history_lg):
269
+ """Process message with LionGuard 2"""
270
+ # Run LionGuard sync check in thread pool to not block
271
+ loop = asyncio.get_event_loop()
272
+ lg_flagged, lg_score = await loop.run_in_executor(None, lionguard_2_sync, message, 0.5)
273
+
274
+ history_lg.append({"role": "user", "content": message})
275
+ if lg_flagged:
276
+ lg_response = "🚫 This message has been flagged by LionGuard 2"
277
+ history_lg.append({"role": "assistant", "content": lg_response})
278
+ else:
279
+ lg_response = await get_openai_response_async(message)
280
+ history_lg.append({"role": "assistant", "content": lg_response})
281
+ return history_lg, lg_score
282
+
283
+ async def process_message_async(message, history_no_mod, history_openai, history_lg):
284
+ """Process message concurrently across all three guardrails"""
285
+ if not message.strip():
286
+ return history_no_mod, history_openai, history_lg, ""
287
+
288
+ # Run all three processes concurrently using asyncio.gather
289
+ results = await asyncio.gather(
290
+ process_no_moderation(message, history_no_mod),
291
+ process_openai_moderation(message, history_openai),
292
+ process_lionguard(message, history_lg),
293
+ return_exceptions=True # Continue even if one fails
294
+ )
295
+
296
+ # Unpack results
297
+ history_no_mod = results[0] if not isinstance(results[0], Exception) else history_no_mod
298
+ history_openai = results[1] if not isinstance(results[1], Exception) else history_openai
299
+ history_lg_result = results[2] if not isinstance(results[2], Exception) else (history_lg, 0.0)
300
+ history_lg = history_lg_result[0]
301
+ lg_score = history_lg_result[1] if isinstance(history_lg_result, tuple) else 0.0
302
+
303
+ # --- Logging for chatbot worksheet (runs in background) ---
304
+ if GOOGLE_SHEET_URL and GOOGLE_CREDENTIALS:
305
+ try:
306
+ loop = asyncio.get_event_loop()
307
+ # Run logging in thread pool so it doesn't block
308
+ loop.run_in_executor(None, _log_chatbot_sync, message, lg_score)
309
+ except Exception as e:
310
+ print(f"Chatbot logging failed: {e}")
311
+
312
+ return history_no_mod, history_openai, history_lg, ""
313
+
314
+ def _log_chatbot_sync(message, lg_score):
315
+ """Sync helper for logging - runs in thread pool"""
316
+ try:
317
+ embeddings = get_embeddings([message])
318
+ results = model.predict(embeddings)
319
+ now = datetime.now().isoformat()
320
+ text_id = str(uuid.uuid4())
321
+ row = {
322
+ "datetime": now,
323
+ "text_id": text_id,
324
+ "text": message,
325
+ "binary_score": results.get("binary", [None])[0],
326
+ "hateful_l1_score": results.get(CATEGORIES['hateful'][0], [None])[0],
327
+ "hateful_l2_score": results.get(CATEGORIES['hateful'][1], [None])[0],
328
+ "insults_score": results.get(CATEGORIES['insults'][0], [None])[0],
329
+ "sexual_l1_score": results.get(CATEGORIES['sexual'][0], [None])[0],
330
+ "sexual_l2_score": results.get(CATEGORIES['sexual'][1], [None])[0],
331
+ "physical_violence_score": results.get(CATEGORIES['physical_violence'][0], [None])[0],
332
+ "self_harm_l1_score": results.get(CATEGORIES['self_harm'][0], [None])[0],
333
+ "self_harm_l2_score": results.get(CATEGORIES['self_harm'][1], [None])[0],
334
+ "aom_l1_score": results.get(CATEGORIES['all_other_misconduct'][0], [None])[0],
335
+ "aom_l2_score": results.get(CATEGORIES['all_other_misconduct'][1], [None])[0],
336
+ "openai_score": None
337
+ }
338
+ try:
339
+ openai_result = client.moderations.create(input=message)
340
+ row["openai_score"] = float(openai_result.results[0].category_scores.get("hate", 0.0))
341
+ except Exception:
342
+ row["openai_score"] = None
343
+
344
+ log_chatbot_data(row)
345
+ except Exception as e:
346
+ print(f"Error in sync logging: {e}")
347
+
348
+ def process_message(message, history_no_mod, history_openai, history_lg):
349
+ """Wrapper function for Gradio (converts async to sync)"""
350
+ return asyncio.run(process_message_async(message, history_no_mod, history_openai, history_lg))
351
+
352
+ def clear_all_chats():
353
+ return [], [], []
354
+
355
+ # ---- MAIN GRADIO UI ----
356
+
357
+ DISCLAIMER = """
358
+ <div style='background: #fbbf24; color: #1e293b; border-radius: 8px; padding: 14px; margin-bottom: 12px; font-size: 15px; font-weight:500;'>
359
+ ⚠️ LionGuard 2 may make mistakes. All entries are logged (anonymised) to improve the model.
360
+ </div>
361
+ """
362
+
363
+ with gr.Blocks(title="LionGuard 2 Demo", theme=gr.themes.Soft()) as demo:
364
+ gr.HTML("<h1 style='text-align:center'>LionGuard 2 Demo</h1>")
365
+
366
+ with gr.Tabs():
367
+ with gr.Tab("Classifier"):
368
+ gr.HTML(DISCLAIMER)
369
+ with gr.Row():
370
+ with gr.Column(scale=1, min_width=400):
371
+ text_input = gr.Textbox(
372
+ label="Enter text to analyze:",
373
+ placeholder="Type your text here...",
374
+ lines=8,
375
+ max_lines=16,
376
+ container=True
377
+ )
378
+ analyze_btn = gr.Button("Analyze", variant="primary")
379
+ with gr.Column(scale=1, min_width=400):
380
+ binary_output = gr.HTML(
381
+ value='<div style="text-align: center; color: #9ca3af; padding: 30px; font-style: italic; font-size:36px;">Enter text to analyze</div>'
382
+ )
383
+ category_table = gr.HTML(
384
+ value='<div style="text-align: center; color: #9ca3af; padding: 30px; font-style: italic;">Category scores will appear here after analysis</div>'
385
+ )
386
+ voting_feedback = gr.HTML(value="")
387
+ current_text_id = gr.Textbox(value="", visible=False)
388
+
389
+ with gr.Row(visible=False) as voting_buttons_row:
390
+ thumbs_up_btn = gr.Button("👍 Looks Accurate", variant="primary")
391
+ thumbs_down_btn = gr.Button("👎 Looks Wrong", variant="secondary")
392
+
393
+ def analyze_and_show_voting(text):
394
+ binary_score, category_table_val, text_id, voting_html = analyze_text(text)
395
+ show_vote = gr.update(visible=True) if text_id else gr.update(visible=False)
396
+ return binary_score, category_table_val, text_id, show_vote, "", ""
397
+
398
+ analyze_btn.click(
399
+ analyze_and_show_voting,
400
+ inputs=[text_input],
401
+ outputs=[binary_output, category_table, current_text_id, voting_buttons_row, voting_feedback, voting_feedback]
402
+ )
403
+ text_input.submit(
404
+ analyze_and_show_voting,
405
+ inputs=[text_input],
406
+ outputs=[binary_output, category_table, current_text_id, voting_buttons_row, voting_feedback, voting_feedback]
407
+ )
408
+ thumbs_up_btn.click(vote_thumbs_up, inputs=[current_text_id], outputs=[voting_feedback])
409
+ thumbs_down_btn.click(vote_thumbs_down, inputs=[current_text_id], outputs=[voting_feedback])
410
+
411
+ with gr.Tab("Guardrail Comparison"):
412
+ gr.HTML(DISCLAIMER)
413
+ with gr.Row():
414
+ with gr.Column(scale=1):
415
+ gr.Markdown("#### 🔵 No Moderation")
416
+ chatbot_no_mod = gr.Chatbot(height=650, label="No Moderation", show_label=False, bubble_full_width=False, type='messages')
417
+ with gr.Column(scale=1):
418
+ gr.Markdown("#### 🟠 OpenAI Moderation")
419
+ chatbot_openai = gr.Chatbot(height=650, label="OpenAI Moderation", show_label=False, bubble_full_width=False, type='messages')
420
+ with gr.Column(scale=1):
421
+ gr.Markdown("#### 🛡️ LionGuard 2")
422
+ chatbot_lg = gr.Chatbot(height=650, label="LionGuard 2", show_label=False, bubble_full_width=False, type='messages')
423
+ gr.Markdown("##### 💬 Send Message to All Models")
424
+ with gr.Row():
425
+ message_input = gr.Textbox(
426
+ placeholder="Type your message to compare responses...",
427
+ show_label=False,
428
+ scale=4
429
+ )
430
+ send_btn = gr.Button("Send", variant="primary", scale=1)
431
+ with gr.Row():
432
+ clear_btn = gr.Button("Clear All Chats", variant="stop")
433
+
434
+ send_btn.click(
435
+ process_message,
436
+ inputs=[message_input, chatbot_no_mod, chatbot_openai, chatbot_lg],
437
+ outputs=[chatbot_no_mod, chatbot_openai, chatbot_lg, message_input]
438
+ )
439
+ message_input.submit(
440
+ process_message,
441
+ inputs=[message_input, chatbot_no_mod, chatbot_openai, chatbot_lg],
442
+ outputs=[chatbot_no_mod, chatbot_openai, chatbot_lg, message_input]
443
+ )
444
+ clear_btn.click(
445
+ clear_all_chats,
446
+ outputs=[chatbot_no_mod, chatbot_openai, chatbot_lg]
447
+ )
448
+
449
+ if __name__ == "__main__":
450
+ demo.launch()
app/.DS_Store ADDED
Binary file (6.15 kB). View file
 
app/backend/__pycache__/models.cpython-313.pyc ADDED
Binary file (3.63 kB). View file
 
app/backend/__pycache__/services.cpython-313.pyc ADDED
Binary file (16.4 kB). View file
 
app/frontend/index.html CHANGED
@@ -3,22 +3,7 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <meta name="description" content="LionGuard: A content moderation tool designed for Singapore's multilingual environment.">
7
- <meta name="theme-color" content="#E14746">
8
- <title>LionGuard - Content Moderation</title>
9
- <link rel="icon" type="image/png" href="/static/logo.png">
10
-
11
- <!-- Fonts -->
12
- <link rel="preconnect" href="https://fonts.googleapis.com">
13
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
14
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Poppins:wght@500;600;700&display=swap" rel="stylesheet">
15
-
16
- <!-- Icons -->
17
- <link href='https://unpkg.com/boxicons@2.1.4/css/boxicons.min.css' rel='stylesheet'>
18
-
19
- <!-- Syntax Highlighting -->
20
- <link href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-tomorrow.min.css" rel="stylesheet" />
21
-
22
  <link rel="stylesheet" href="/static/style.css">
23
  </head>
24
  <body>
@@ -27,46 +12,42 @@
27
  <div class="container">
28
  <div class="header-content">
29
  <div class="logo-section">
30
- <img src="/static/logo.png" alt="LionGuard Logo" class="logo">
31
  <div class="logo-text">
32
- <h1>LionGuard</h1>
33
  <p>A content moderation tool designed for Singapore</p>
34
  </div>
35
  </div>
36
  <div class="header-controls">
37
  <nav class="tabs" aria-label="Primary navigation">
38
- <button class="tab nav-link" data-tab="get-started">
39
- <i class='bx bx-rocket tab-icon'></i>
40
- Get Started
41
- </button>
42
  <div class="nav-dropdown">
43
  <button
44
  class="tab dropdown-toggle"
45
  aria-haspopup="true"
46
  aria-expanded="false"
47
  >
48
- <i class='bx bx-grid-alt tab-icon'></i>
49
  Demos
50
- <i class='bx bx-chevron-down dropdown-caret'></i>
51
  </button>
52
  <div class="dropdown-menu" role="menu">
53
  <button class="tab dropdown-item active" data-tab="detector" role="menuitem">
54
- <i class='bx bx-search tab-icon'></i>
55
  Detector
56
  </button>
57
  <button class="tab dropdown-item" data-tab="chat" role="menuitem">
58
- <i class='bx bx-message-rounded-dots tab-icon'></i>
59
  Chatbot Guardrail
60
  </button>
61
  </div>
62
  </div>
63
  <button class="tab nav-link" data-tab="about">
64
- <i class='bx bx-info-circle tab-icon'></i>
65
  About
66
  </button>
67
  </nav>
68
  <button id="theme-toggle" class="theme-icon-button" aria-label="Toggle theme">
69
- <i class='bx bx-sun theme-icon'></i>
70
  </button>
71
  </div>
72
  </div>
@@ -79,7 +60,7 @@
79
  <div id="detector-content" class="tab-content active">
80
  <!-- Disclaimer -->
81
  <div class="warning-card">
82
- <i class='bx bx-error-circle'></i> Inputs are anonymised and logged to improve LionGuard's moderation models.
83
  </div>
84
 
85
  <!-- Model Selector -->
@@ -89,9 +70,9 @@
89
  </div>
90
  <div class="model-dropdown">
91
  <select id="model-select" class="model-select" aria-label="Detector guardrail model">
92
- <option value="lionguard-2.1" selected>LionGuard 2.1 (Gemini Embeddings, API)</option>
93
- <option value="lionguard-2">LionGuard 2 (OpenAI Embeddings, API)</option>
94
- <option value="lionguard-2-lite">LionGuard 2 Lite (Gemma Embeddings, Local)</option>
95
  </select>
96
  </div>
97
  </div>
@@ -107,7 +88,7 @@
107
  rows="10"
108
  ></textarea>
109
  <button id="analyze-btn" class="btn btn-primary">
110
- <i class='bx bx-search btn-icon'></i>
111
  Analyze
112
  </button>
113
  </div>
@@ -118,7 +99,7 @@
118
 
119
  <!-- Binary Score -->
120
  <div id="binary-result" class="binary-placeholder">
121
- <i class='bx bx-edit placeholder-icon'></i>
122
  <p>Enter text to analyze</p>
123
  </div>
124
 
@@ -132,11 +113,11 @@
132
  <p class="feedback-prompt">Does this look correct?</p>
133
  <div class="feedback-buttons">
134
  <button id="thumbs-up" class="btn btn-success">
135
- <i class='bx bx-like'></i>
136
  Yes
137
  </button>
138
  <button id="thumbs-down" class="btn btn-secondary">
139
- <i class='bx bx-dislike'></i>
140
  No
141
  </button>
142
  </div>
@@ -150,7 +131,7 @@
150
  <div id="chat-content" class="tab-content full-width-section">
151
  <!-- Disclaimer -->
152
  <div class="warning-card">
153
- <i class='bx bx-error-circle'></i> Inputs are anonymised and logged to improve LionGuard's moderation models.
154
  </div>
155
 
156
  <!-- Model Selector for Guardrail -->
@@ -160,9 +141,9 @@
160
  </div>
161
  <div class="model-dropdown">
162
  <select id="model-select-gc" class="model-select" aria-label="Chat guardrail model">
163
- <option value="lionguard-2.1" selected>LionGuard 2.1 (Gemini Embeddings, API)</option>
164
- <option value="lionguard-2">LionGuard 2 (OpenAI Embeddings, API)</option>
165
- <option value="lionguard-2-lite">LionGuard 2 Lite (Gemma Embeddings, Local)</option>
166
  </select>
167
  </div>
168
  </div>
@@ -172,7 +153,7 @@
172
  <!-- No Moderation -->
173
  <div class="chat-panel">
174
  <div class="chat-header">
175
- <i class='bx bxs-circle chat-icon' style="color: #3B82F6;"></i>
176
  <h4>No Moderation</h4>
177
  </div>
178
  <div id="chat-no-mod" class="chat-messages"></div>
@@ -181,17 +162,17 @@
181
  <!-- OpenAI Moderation -->
182
  <div class="chat-panel">
183
  <div class="chat-header">
184
- <i class='bx bxs-circle chat-icon' style="color: #F59E0B;"></i>
185
  <h4>OpenAI Moderation</h4>
186
  </div>
187
  <div id="chat-openai" class="chat-messages"></div>
188
  </div>
189
 
190
- <!-- LionGuard -->
191
  <div class="chat-panel">
192
  <div class="chat-header">
193
- <i class='bx bx-shield-quarter chat-icon' style="color: #E14746;"></i>
194
- <h4>LionGuard</h4>
195
  </div>
196
  <div id="chat-lionguard" class="chat-messages"></div>
197
  </div>
@@ -215,7 +196,7 @@
215
  <div id="about-content" class="tab-content">
216
  <!-- Hero Section -->
217
  <section class="about-intro-section">
218
- <p class="lead">LionGuard is a family of open-source content moderation models specifically designed for Singapore's multilingual environment. Optimized for Singapore’s linguistic mix, including Singlish, Mandarin, Malay, and Tamil, LionGuard delivers accurate moderation grounded in local usage and cultural nuance.</p>
219
  <p class="lead" style="font-style: italic;">Developed by <a href="https://www.tech.gov.sg/" target="_blank" style="color: var(--primary-red); text-decoration: none; font-weight: 600;">GovTech Singapore</a>.</p>
220
  </section>
221
 
@@ -223,18 +204,18 @@
223
  <section class="about-resources-grid">
224
  <!-- Models -->
225
  <div class="resource-card">
226
- <h3><i class='bx bx-cube-alt'></i> Open-Sourced Models</h3>
227
  <div class="resource-list">
228
- <a href="https://huggingface.co/govtech/lionguard-2.1" target="_blank">LionGuard 2.1</a>
229
- <a href="https://huggingface.co/govtech/lionguard-2" target="_blank">LionGuard 2</a>
230
- <a href="https://huggingface.co/govtech/lionguard-2-lite" target="_blank">LionGuard 2 Lite</a>
231
- <a href="https://huggingface.co/govtech/lionguard-v1" target="_blank">LionGuard 1</a>
232
  </div>
233
  </div>
234
 
235
  <!-- Datasets -->
236
  <div class="resource-card">
237
- <h3><i class='bx bx-data'></i> Open-Sourced Datasets</h3>
238
  <div class="resource-list">
239
  <a href="https://huggingface.co/datasets/govtech/lionguard-2-synthetic-instruct" target="_blank">Subset of Training Data</a>
240
  <a href="https://huggingface.co/datasets/govtech/RabakBench" target="_blank">RabakBench</a>
@@ -243,158 +224,33 @@
243
 
244
  <!-- Blog Posts -->
245
  <div class="resource-card">
246
- <h3><i class='bx bx-news'></i> Blog Posts</h3>
247
  <div class="resource-list">
248
- <a href="https://medium.com/dsaid-govtech/lionguard-2-8066d4e20d16" target="_blank">LionGuard 2</a>
249
- <a href="https://medium.com/dsaid-govtech/building-lionguard-a-contextualised-moderation-classifier-to-tackle-local-unsafe-content-8f68c8f13179" target="_blank">LionGuard</a>
250
  </div>
251
  </div>
252
 
253
  <!-- Papers -->
254
  <div class="resource-card">
255
- <h3><i class='bx bx-file'></i> Research Papers</h3>
256
  <div class="resource-list">
257
- <a href="https://arxiv.org/abs/2507.05980" target="_blank">LionGuard 2 (arXiv:2507.05980)</a>
258
  <a href="https://arxiv.org/abs/2507.15339" target="_blank">RabakBench (arXiv:2507.15339)</a>
259
- <a href="https://arxiv.org/abs/2407.10995" target="_blank">LionGuard 1 (arXiv:2407.10995)</a>
260
  </div>
261
  </div>
262
  </section>
263
  </div>
264
-
265
- <!-- Get Started Tab Content -->
266
- <div id="get-started-content" class="tab-content">
267
- <div class="get-started-container">
268
- <!-- Option 1 -->
269
- <div class="option-card full-width-card">
270
- <div class="option-header">
271
- <i class='bx bx-server option-icon'></i>
272
- <div>
273
- <h2>Option 1: Self-Host the Classifier</h2>
274
- <p>Integrate LionGuard directly into your applications. Toggle between models to see the implementation details.</p>
275
- </div>
276
- </div>
277
-
278
- <div class="code-section">
279
- <div class="code-tabs-header">
280
- <div class="code-tabs">
281
- <button class="code-tab active" data-code="lg2.1">LionGuard 2.1</button>
282
- <button class="code-tab" data-code="lg2">LionGuard 2</button>
283
- <button class="code-tab" data-code="lg2-lite">LionGuard 2 Lite</button>
284
- </div>
285
- <button id="copy-code-btn" class="copy-btn" title="Copy to clipboard">
286
- <i class='bx bx-copy'></i> Copy
287
- </button>
288
- </div>
289
-
290
- <div class="code-display">
291
- <!-- LionGuard 2.1 Snippet -->
292
- <div id="code-lg2.1" class="code-block active">
293
- <pre><code class="language-python">import os
294
- import numpy as np
295
- from transformers import AutoModel
296
- from google import genai
297
-
298
- # Load model directly from HF
299
- model = AutoModel.from_pretrained("govtech/lionguard-2.1", trust_remote_code=True)
300
-
301
- # Text to classify
302
- texts = ["hello", "world"]
303
-
304
- # Get embeddings (users to input their own Gemini API key)
305
- client = genai.Client(api_key=os.getenv("GEMINI_API_KEY"))
306
- response = client.models.embed_content(
307
- model="gemini-embedding-001",
308
- contents=texts
309
- )
310
- embeddings = np.array([emb.values for emb in response.embeddings])
311
-
312
- # Run inference
313
- results = model.predict(embeddings)</code></pre>
314
- </div>
315
-
316
- <!-- LionGuard 2 Snippet -->
317
- <div id="code-lg2" class="code-block">
318
- <pre><code class="language-python">import os
319
- import numpy as np
320
- from transformers import AutoModel
321
- from openai import OpenAI
322
-
323
- # Load model directly from HF
324
- model = AutoModel.from_pretrained(
325
- "govtech/lionguard-2",
326
- trust_remote_code=True
327
- )
328
-
329
- # Get OpenAI embeddings (users to input their own OpenAI API key)
330
- client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
331
- response = client.embeddings.create(
332
- input="Hello, world!", # users to input their own text
333
- model="text-embedding-3-large",
334
- dimensions=3072 # dimensions of the embedding
335
- )
336
- embeddings = np.array([data.embedding for data in response.data])
337
-
338
- # Run LionGuard 2
339
- results = model.predict(embeddings)</code></pre>
340
- </div>
341
-
342
- <!-- LionGuard 2 Lite Snippet -->
343
- <div id="code-lg2-lite" class="code-block">
344
- <pre><code class="language-python">import numpy as np
345
- from sentence_transformers import SentenceTransformer
346
- from transformers import AutoModel
347
-
348
- # Load model directly from Hub
349
- model = AutoModel.from_pretrained("govtech/lionguard-2-lite", trust_remote_code=True)
350
-
351
- # Download model from the 🤗 Hub
352
- embedding_model = SentenceTransformer("google/embeddinggemma-300m")
353
-
354
- # Text to classify
355
- texts = ["hello", "world"]
356
-
357
- # Add prompt instructions to generate embeddings that are optimized to classify texts according to preset labels
358
- formatted_texts = [f"task: classification | query: {c}" for c in texts]
359
- embeddings = embedding_model.encode(formatted_texts) # NOTE: use encode() instead of encode_documents()
360
-
361
- # Run inference
362
- results = model.predict(embeddings)</code></pre>
363
- </div>
364
- </div>
365
- </div>
366
- </div>
367
-
368
- <!-- Option 2 -->
369
- <div class="option-card full-width-card">
370
- <div class="option-header">
371
- <i class='bx bx-shield-quarter option-icon'></i>
372
- <div>
373
- <h2>Option 2: via AI Guardians - Sentinel</h2>
374
- <p>Managed service for Singapore public sector agencies.</p>
375
- </div>
376
- </div>
377
- <div class="option-action">
378
- <a href="https://www.aiguardian.gov.sg/" target="_blank" class="btn btn-primary">
379
- Visit AI Guardians
380
- </a>
381
- </div>
382
- </div>
383
- </div>
384
- </div>
385
  </main>
386
 
387
  <!-- Footer -->
388
  <footer class="footer">
389
  <div class="container">
390
- <p>LionGuard · Powered by <a href="https://www.tech.gov.sg/" target="_blank">GovTech</a></p>
391
  </div>
392
  </footer>
393
 
394
  <script src="/static/script.js"></script>
395
-
396
- <!-- Syntax Highlighting -->
397
- <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js"></script>
398
- <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-python.min.js"></script>
399
  </body>
400
  </html>
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Lionguard</title>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  <link rel="stylesheet" href="/static/style.css">
8
  </head>
9
  <body>
 
12
  <div class="container">
13
  <div class="header-content">
14
  <div class="logo-section">
15
+ <img src="/static/logo.png" alt="Lionguard Logo" class="logo">
16
  <div class="logo-text">
17
+ <h1>Lionguard</h1>
18
  <p>A content moderation tool designed for Singapore</p>
19
  </div>
20
  </div>
21
  <div class="header-controls">
22
  <nav class="tabs" aria-label="Primary navigation">
 
 
 
 
23
  <div class="nav-dropdown">
24
  <button
25
  class="tab dropdown-toggle"
26
  aria-haspopup="true"
27
  aria-expanded="false"
28
  >
29
+ <span class="tab-icon">🛠️</span>
30
  Demos
31
+ <span class="dropdown-caret">▾</span>
32
  </button>
33
  <div class="dropdown-menu" role="menu">
34
  <button class="tab dropdown-item active" data-tab="detector" role="menuitem">
35
+ <span class="tab-icon">🔍</span>
36
  Detector
37
  </button>
38
  <button class="tab dropdown-item" data-tab="chat" role="menuitem">
39
+ <span class="tab-icon">💬</span>
40
  Chatbot Guardrail
41
  </button>
42
  </div>
43
  </div>
44
  <button class="tab nav-link" data-tab="about">
45
+ <span class="tab-icon">ℹ️</span>
46
  About
47
  </button>
48
  </nav>
49
  <button id="theme-toggle" class="theme-icon-button" aria-label="Toggle theme">
50
+ <span class="theme-icon" aria-hidden="true">🌞</span>
51
  </button>
52
  </div>
53
  </div>
 
60
  <div id="detector-content" class="tab-content active">
61
  <!-- Disclaimer -->
62
  <div class="warning-card">
63
+ ⚠️ Inputs are anonymised and logged to improve Lionguard's moderation models.
64
  </div>
65
 
66
  <!-- Model Selector -->
 
70
  </div>
71
  <div class="model-dropdown">
72
  <select id="model-select" class="model-select" aria-label="Detector guardrail model">
73
+ <option value="lionguard-2.1" selected>Lionguard 2.1 (Gemini Embeddings, API)</option>
74
+ <option value="lionguard-2">Lionguard 2 (OpenAI Embeddings, API)</option>
75
+ <option value="lionguard-2-lite">Lionguard 2 Lite (Gemma Embeddings, Local)</option>
76
  </select>
77
  </div>
78
  </div>
 
88
  rows="10"
89
  ></textarea>
90
  <button id="analyze-btn" class="btn btn-primary">
91
+ <span class="btn-icon">🔍</span>
92
  Analyze
93
  </button>
94
  </div>
 
99
 
100
  <!-- Binary Score -->
101
  <div id="binary-result" class="binary-placeholder">
102
+ <div class="placeholder-icon">📝</div>
103
  <p>Enter text to analyze</p>
104
  </div>
105
 
 
113
  <p class="feedback-prompt">Does this look correct?</p>
114
  <div class="feedback-buttons">
115
  <button id="thumbs-up" class="btn btn-success">
116
+ <span>👍</span>
117
  Yes
118
  </button>
119
  <button id="thumbs-down" class="btn btn-secondary">
120
+ <span>👎</span>
121
  No
122
  </button>
123
  </div>
 
131
  <div id="chat-content" class="tab-content full-width-section">
132
  <!-- Disclaimer -->
133
  <div class="warning-card">
134
+ ⚠️ Inputs are anonymised and logged to improve Lionguard's moderation models.
135
  </div>
136
 
137
  <!-- Model Selector for Guardrail -->
 
141
  </div>
142
  <div class="model-dropdown">
143
  <select id="model-select-gc" class="model-select" aria-label="Chat guardrail model">
144
+ <option value="lionguard-2.1" selected>Lionguard 2.1 (Gemini Embeddings, API)</option>
145
+ <option value="lionguard-2">Lionguard 2 (OpenAI Embeddings, API)</option>
146
+ <option value="lionguard-2-lite">Lionguard 2 Lite (Gemma Embeddings, Local)</option>
147
  </select>
148
  </div>
149
  </div>
 
153
  <!-- No Moderation -->
154
  <div class="chat-panel">
155
  <div class="chat-header">
156
+ <span class="chat-icon">🔵</span>
157
  <h4>No Moderation</h4>
158
  </div>
159
  <div id="chat-no-mod" class="chat-messages"></div>
 
162
  <!-- OpenAI Moderation -->
163
  <div class="chat-panel">
164
  <div class="chat-header">
165
+ <span class="chat-icon">🟠</span>
166
  <h4>OpenAI Moderation</h4>
167
  </div>
168
  <div id="chat-openai" class="chat-messages"></div>
169
  </div>
170
 
171
+ <!-- Lionguard -->
172
  <div class="chat-panel">
173
  <div class="chat-header">
174
+ <span class="chat-icon">🛡️</span>
175
+ <h4>Lionguard</h4>
176
  </div>
177
  <div id="chat-lionguard" class="chat-messages"></div>
178
  </div>
 
196
  <div id="about-content" class="tab-content">
197
  <!-- Hero Section -->
198
  <section class="about-intro-section">
199
+ <p class="lead">Lionguard is a family of open-source content moderation models specifically designed for Singapore's multilingual environment. Optimized for Singapore’s linguistic mix, including Singlish, Mandarin, Malay, and Tamil, Lionguard delivers accurate moderation grounded in local usage and cultural nuance.</p>
200
  <p class="lead" style="font-style: italic;">Developed by <a href="https://www.tech.gov.sg/" target="_blank" style="color: var(--primary-red); text-decoration: none; font-weight: 600;">GovTech Singapore</a>.</p>
201
  </section>
202
 
 
204
  <section class="about-resources-grid">
205
  <!-- Models -->
206
  <div class="resource-card">
207
+ <h3>🤗 Open-Sourced Models</h3>
208
  <div class="resource-list">
209
+ <a href="https://huggingface.co/govtech/lionguard-2.1" target="_blank">Lionguard 2.1</a>
210
+ <a href="https://huggingface.co/govtech/lionguard-2" target="_blank">Lionguard 2</a>
211
+ <a href="https://huggingface.co/govtech/lionguard-2-lite" target="_blank">Lionguard 2 Lite</a>
212
+ <a href="https://huggingface.co/govtech/lionguard-v1" target="_blank">Lionguard 1</a>
213
  </div>
214
  </div>
215
 
216
  <!-- Datasets -->
217
  <div class="resource-card">
218
+ <h3>📊 Open-Sourced Datasets</h3>
219
  <div class="resource-list">
220
  <a href="https://huggingface.co/datasets/govtech/lionguard-2-synthetic-instruct" target="_blank">Subset of Training Data</a>
221
  <a href="https://huggingface.co/datasets/govtech/RabakBench" target="_blank">RabakBench</a>
 
224
 
225
  <!-- Blog Posts -->
226
  <div class="resource-card">
227
+ <h3>📝 Blog Posts</h3>
228
  <div class="resource-list">
229
+ <a href="https://medium.com/dsaid-govtech/lionguard-2-8066d4e20d16" target="_blank">Lionguard 2</a>
230
+ <a href="https://medium.com/dsaid-govtech/building-lionguard-a-contextualised-moderation-classifier-to-tackle-local-unsafe-content-8f68c8f13179" target="_blank">Lionguard</a>
231
  </div>
232
  </div>
233
 
234
  <!-- Papers -->
235
  <div class="resource-card">
236
+ <h3>📄 Research Papers</h3>
237
  <div class="resource-list">
238
+ <a href="https://arxiv.org/abs/2507.05980" target="_blank">Lionguard 2 (arXiv:2507.05980)</a>
239
  <a href="https://arxiv.org/abs/2507.15339" target="_blank">RabakBench (arXiv:2507.15339)</a>
240
+ <a href="https://arxiv.org/abs/2407.10995" target="_blank">Lionguard 1 (arXiv:2407.10995)</a>
241
  </div>
242
  </div>
243
  </section>
244
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
245
  </main>
246
 
247
  <!-- Footer -->
248
  <footer class="footer">
249
  <div class="container">
250
+ <p>Lionguard · Powered by <a href="https://www.tech.gov.sg/" target="_blank">GovTech</a></p>
251
  </div>
252
  </footer>
253
 
254
  <script src="/static/script.js"></script>
 
 
 
 
255
  </body>
256
  </html>
app/frontend/logo.png DELETED

Git LFS Details

  • SHA256: b6210607aec15b7e030c97b8af9ec88780fad85dead6562c3267e7b2516abfaa
  • Pointer size: 131 Bytes
  • Size of remote file: 212 kB
app/frontend/script.js CHANGED
@@ -29,12 +29,12 @@ function hideLoading(button, originalText) {
29
 
30
  function getScoreLevel(score) {
31
  if (score < 0.4) {
32
- return { className: 'good', icon: '<i class="bx bx-check-circle"></i>', title: 'Low risk' };
33
  }
34
  if (score < 0.7) {
35
- return { className: 'warn', icon: '<i class="bx bx-error"></i>', title: 'Needs review' };
36
  }
37
- return { className: 'bad', icon: '<i class="bx bx-error-circle"></i>', title: 'High risk' };
38
  }
39
 
40
  function formatScore(score) {
@@ -197,9 +197,9 @@ async function analyzeText() {
197
  const verdictClass = data.binary_verdict;
198
  const verdictText = verdictClass.charAt(0).toUpperCase() + verdictClass.slice(1);
199
  const verdictIcons = {
200
- 'pass': '<i class="bx bx-check-shield"></i>',
201
- 'warn': '<i class="bx bx-shield-minus"></i>',
202
- 'fail': '<i class="bx bx-shield-x"></i>'
203
  };
204
 
205
  binaryResult.innerHTML = `
@@ -403,9 +403,7 @@ function initThemeToggle() {
403
  const updateIcon = (isDark) => {
404
  themeToggle.setAttribute('aria-pressed', isDark ? 'true' : 'false');
405
  if (themeIcon) {
406
- // Toggle class for boxicons
407
- themeIcon.className = isDark ? 'bx bx-moon theme-icon' : 'bx bx-sun theme-icon';
408
- themeIcon.textContent = ''; // clear text content
409
  }
410
  };
411
 
@@ -424,68 +422,6 @@ function initThemeToggle() {
424
  });
425
  }
426
 
427
- // Code snippet tabs for Get Started
428
- function initCodeTabs() {
429
- const tabs = document.querySelectorAll('.code-tab');
430
- const blocks = document.querySelectorAll('.code-block');
431
-
432
- if (!tabs.length) return;
433
-
434
- tabs.forEach(tab => {
435
- tab.addEventListener('click', () => {
436
- // Remove active class from all tabs
437
- tabs.forEach(t => t.classList.remove('active'));
438
- // Add active class to clicked tab
439
- tab.classList.add('active');
440
-
441
- // Hide all blocks
442
- blocks.forEach(b => b.classList.remove('active'));
443
-
444
- // Show target block
445
- const targetId = `code-${tab.dataset.code}`;
446
- const targetBlock = document.getElementById(targetId);
447
- if (targetBlock) {
448
- targetBlock.classList.add('active');
449
- }
450
- });
451
- });
452
- }
453
-
454
- // Copy Code Functionality
455
- function initCopyButton() {
456
- const copyBtn = document.getElementById('copy-code-btn');
457
- if (!copyBtn) return;
458
-
459
- copyBtn.addEventListener('click', async () => {
460
- // Find active code block
461
- const activeBlock = document.querySelector('.code-block.active code');
462
- if (!activeBlock) return;
463
-
464
- const textToCopy = activeBlock.textContent;
465
-
466
- try {
467
- await navigator.clipboard.writeText(textToCopy);
468
-
469
- // Provide feedback
470
- const originalHtml = copyBtn.innerHTML;
471
- copyBtn.innerHTML = `<i class='bx bx-check'></i> Copied!`;
472
- copyBtn.classList.add('success');
473
-
474
- setTimeout(() => {
475
- copyBtn.innerHTML = originalHtml;
476
- copyBtn.classList.remove('success');
477
- }, 2000);
478
- } catch (err) {
479
- console.error('Failed to copy:', err);
480
- copyBtn.innerHTML = `<i class='bx bx-x'></i> Failed`;
481
-
482
- setTimeout(() => {
483
- copyBtn.innerHTML = `<i class='bx bx-copy'></i> Copy`;
484
- }, 2000);
485
- }
486
- });
487
- }
488
-
489
  // Initialize app
490
  document.addEventListener('DOMContentLoaded', () => {
491
  initTabs();
@@ -494,8 +430,6 @@ document.addEventListener('DOMContentLoaded', () => {
494
  initModelSelectorGC();
495
  initEventListeners();
496
  initThemeToggle();
497
- initCodeTabs();
498
- initCopyButton();
499
 
500
  console.log('LionGuard 2 app initialized');
501
  });
 
29
 
30
  function getScoreLevel(score) {
31
  if (score < 0.4) {
32
+ return { className: 'good', icon: '👌', title: 'Low risk' };
33
  }
34
  if (score < 0.7) {
35
+ return { className: 'warn', icon: '⚠️', title: 'Needs review' };
36
  }
37
+ return { className: 'bad', icon: '🚨', title: 'High risk' };
38
  }
39
 
40
  function formatScore(score) {
 
197
  const verdictClass = data.binary_verdict;
198
  const verdictText = verdictClass.charAt(0).toUpperCase() + verdictClass.slice(1);
199
  const verdictIcons = {
200
+ 'pass': '',
201
+ 'warn': '⚠️',
202
+ 'fail': '🚨'
203
  };
204
 
205
  binaryResult.innerHTML = `
 
403
  const updateIcon = (isDark) => {
404
  themeToggle.setAttribute('aria-pressed', isDark ? 'true' : 'false');
405
  if (themeIcon) {
406
+ themeIcon.textContent = isDark ? '🌙' : '🌞';
 
 
407
  }
408
  };
409
 
 
422
  });
423
  }
424
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
425
  // Initialize app
426
  document.addEventListener('DOMContentLoaded', () => {
427
  initTabs();
 
430
  initModelSelectorGC();
431
  initEventListeners();
432
  initThemeToggle();
 
 
433
 
434
  console.log('LionGuard 2 app initialized');
435
  });
app/frontend/style.css CHANGED
@@ -1,10 +1,5 @@
1
  /* LionGuard 2 - Warm, Inviting, and Bright Design */
2
-
3
- /* Icons alignment */
4
- .bx {
5
- vertical-align: middle;
6
- line-height: 1;
7
- }
8
 
9
  :root {
10
  /* Modern color palette inspired by logo */
@@ -110,7 +105,7 @@ main {
110
  margin-left: auto;
111
  display: flex;
112
  align-items: center;
113
- gap: 24px; /* Increased gap for better spacing */
114
  flex-wrap: wrap;
115
  justify-content: flex-end;
116
  background: transparent;
@@ -135,10 +130,8 @@ main {
135
 
136
  .logo-text h1 {
137
  font-family: 'Poppins', sans-serif;
138
- font-size: 1.5rem;
139
- font-weight: 700;
140
- color: var(--primary-dark);
141
- letter-spacing: -0.02em;
142
  margin-bottom: 0px;
143
  line-height: 1.2;
144
  }
@@ -328,7 +321,7 @@ body.dark-mode .toggle-icon-moon {
328
  display: inline-flex;
329
  align-items: center;
330
  flex-wrap: wrap;
331
- gap: 24px; /* Increased gap for better spacing */
332
  margin-bottom: 0;
333
  background: transparent;
334
  border: none;
@@ -380,12 +373,11 @@ body.dark-mode .toggle-icon-moon {
380
  }
381
 
382
  .tab-icon {
383
- font-size: 1.25rem;
384
- margin-bottom: 1px;
385
  }
386
 
387
  .dropdown-toggle {
388
- /* padding-right: 22px; removed to fix gap */
389
  }
390
 
391
  .dropdown-caret {
@@ -950,45 +942,39 @@ body.dark-mode .score-chip.bad {
950
  background: var(--warm-cream);
951
  border: 1px solid var(--warm-tan);
952
  border-radius: var(--border-radius);
953
- padding: 16px 20px; /* Increased padding */
954
- margin-top: 12px;
955
  text-align: center;
956
  }
957
 
958
  .feedback-prompt {
959
  color: var(--text-secondary);
960
- margin-bottom: 12px; /* Balanced spacing */
961
  font-weight: 500;
962
- font-size: 0.85rem; /* Slightly larger */
963
  }
964
 
965
  .feedback-buttons {
966
  display: flex;
967
- gap: 12px; /* More gap between buttons */
968
- margin-bottom: 0; /* Remove bottom margin, let padding handle it unless message appears */
969
  flex-wrap: wrap;
970
  justify-content: center;
971
  }
972
 
 
 
 
 
 
 
 
973
  .feedback-message {
974
- /* Removed default padding to avoid ghost space */
975
  border-radius: var(--border-radius-sm);
976
  font-weight: 600;
977
  text-align: center;
978
  font-size: 0.85rem;
979
- margin-top: 0;
980
- transition: all 0.3s ease;
981
- opacity: 0;
982
- height: 0;
983
- overflow: hidden;
984
- }
985
-
986
- .feedback-message.success,
987
- .feedback-message.info {
988
- padding: 10px;
989
- margin-top: 12px; /* Add space only when message appears */
990
- opacity: 1;
991
- height: auto;
992
  }
993
 
994
  .feedback-message.success {
@@ -1054,7 +1040,7 @@ body.dark-mode .score-chip.bad {
1054
  .chat-header h4 {
1055
  font-family: 'Poppins', sans-serif;
1056
  color: var(--primary-dark);
1057
- font-size: 1.05rem;
1058
  }
1059
 
1060
  .chat-messages {
@@ -1301,153 +1287,3 @@ body.dark-mode .score-chip.bad {
1301
  grid-template-columns: 1fr;
1302
  }
1303
  }
1304
-
1305
- /* Get Started Tab Styles */
1306
- .get-started-container {
1307
- max-width: 1000px;
1308
- margin: 0 auto;
1309
- display: flex;
1310
- flex-direction: column;
1311
- gap: 20px;
1312
- }
1313
-
1314
- .option-card.full-width-card {
1315
- width: 100%;
1316
- background: var(--soft-white);
1317
- border: 1px solid var(--warm-tan);
1318
- border-radius: var(--border-radius);
1319
- padding: 16px; /* Restored to a slightly larger padding */
1320
- box-shadow: var(--shadow-soft);
1321
- display: flex;
1322
- flex-direction: column;
1323
- gap: 12px; /* Slightly reduced gap */
1324
- }
1325
-
1326
- .option-header {
1327
- display: flex;
1328
- gap: 12px;
1329
- align-items: flex-start; /* Reverted to align-items: flex-start */
1330
- }
1331
-
1332
- .option-icon {
1333
- font-size: 1.5rem;
1334
- color: var(--primary-red);
1335
- background: var(--warm-cream);
1336
- padding: 8px;
1337
- border-radius: 50%;
1338
- }
1339
-
1340
- .option-header h2 {
1341
- font-family: 'Poppins', sans-serif;
1342
- font-size: 1.4rem;
1343
- color: var(--primary-dark);
1344
- margin-bottom: 4px;
1345
- }
1346
-
1347
- .option-header p {
1348
- color: var(--text-secondary);
1349
- font-size: 0.9rem;
1350
- }
1351
-
1352
- .code-section {
1353
- background: #2d2d2d; /* Prism Tomorrow bg match */
1354
- border-radius: var(--border-radius);
1355
- overflow: hidden;
1356
- margin-top: 4px;
1357
- }
1358
-
1359
- .code-tabs-header {
1360
- display: flex;
1361
- justify-content: space-between;
1362
- align-items: center;
1363
- background: #1f1f1f; /* Slightly darker */
1364
- border-bottom: 1px solid #3d3d3d;
1365
- padding-right: 10px;
1366
- }
1367
-
1368
- .code-tabs {
1369
- display: flex;
1370
- overflow-x: auto;
1371
- }
1372
-
1373
- .code-tab {
1374
- background: transparent;
1375
- border: none;
1376
- color: #94A3B8;
1377
- padding: 8px 16px;
1378
- cursor: pointer;
1379
- font-size: 0.8rem;
1380
- font-family: 'Inter', sans-serif;
1381
- font-weight: 500;
1382
- border-bottom: 2px solid transparent;
1383
- transition: all 0.2s ease;
1384
- white-space: nowrap;
1385
- }
1386
-
1387
- .code-tab:hover {
1388
- color: #E2E8F0;
1389
- background: rgba(255, 255, 255, 0.05);
1390
- }
1391
-
1392
- .code-tab.active {
1393
- color: #FFFFFF;
1394
- border-bottom-color: var(--primary-red);
1395
- background: rgba(255, 255, 255, 0.08);
1396
- }
1397
-
1398
- .copy-btn {
1399
- background: transparent;
1400
- border: 1px solid #475569;
1401
- color: #cbd5e1;
1402
- padding: 4px 10px;
1403
- border-radius: 4px;
1404
- font-size: 0.75rem;
1405
- cursor: pointer;
1406
- display: flex;
1407
- align-items: center;
1408
- gap: 6px;
1409
- transition: all 0.2s ease;
1410
- }
1411
-
1412
- .copy-btn:hover {
1413
- background: rgba(255, 255, 255, 0.1);
1414
- border-color: #94a3b8;
1415
- color: white;
1416
- }
1417
-
1418
- .code-display {
1419
- padding: 16px;
1420
- background: #2d2d2d; /* Match Prism theme */
1421
- overflow-x: auto;
1422
- }
1423
-
1424
- .code-block {
1425
- display: none;
1426
- }
1427
-
1428
- .code-block.active {
1429
- display: block;
1430
- animation: fadeIn 0.3s ease;
1431
- }
1432
-
1433
- /* Override Prism margins */
1434
- .code-block pre[class*="language-"] {
1435
- margin: 0 !important;
1436
- padding: 0 !important;
1437
- background: transparent !important;
1438
- text-shadow: none !important;
1439
- border-radius: 0 !important;
1440
- box-shadow: none !important;
1441
- }
1442
-
1443
- .code-block code[class*="language-"] {
1444
- font-family: 'Menlo', 'Monaco', 'Courier New', monospace !important;
1445
- font-size: 0.85rem !important;
1446
- line-height: 1.5 !important;
1447
- }
1448
-
1449
- .option-action {
1450
- display: flex;
1451
- justify-content: flex-start; /* Align button to the left */
1452
- margin-top: 0; /* Relies on parent gap */
1453
- }
 
1
  /* LionGuard 2 - Warm, Inviting, and Bright Design */
2
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Poppins:wght@600;700&display=swap');
 
 
 
 
 
3
 
4
  :root {
5
  /* Modern color palette inspired by logo */
 
105
  margin-left: auto;
106
  display: flex;
107
  align-items: center;
108
+ gap: 16px;
109
  flex-wrap: wrap;
110
  justify-content: flex-end;
111
  background: transparent;
 
130
 
131
  .logo-text h1 {
132
  font-family: 'Poppins', sans-serif;
133
+ font-size: 1.4rem;
134
+ color: var(--primary-red);
 
 
135
  margin-bottom: 0px;
136
  line-height: 1.2;
137
  }
 
321
  display: inline-flex;
322
  align-items: center;
323
  flex-wrap: wrap;
324
+ gap: 18px;
325
  margin-bottom: 0;
326
  background: transparent;
327
  border: none;
 
373
  }
374
 
375
  .tab-icon {
376
+ font-size: 1.2rem;
 
377
  }
378
 
379
  .dropdown-toggle {
380
+ padding-right: 22px;
381
  }
382
 
383
  .dropdown-caret {
 
942
  background: var(--warm-cream);
943
  border: 1px solid var(--warm-tan);
944
  border-radius: var(--border-radius);
945
+ padding: 10px 14px;
946
+ margin-top: 8px;
947
  text-align: center;
948
  }
949
 
950
  .feedback-prompt {
951
  color: var(--text-secondary);
952
+ margin-bottom: 4px;
953
  font-weight: 500;
954
+ font-size: 0.78rem;
955
  }
956
 
957
  .feedback-buttons {
958
  display: flex;
959
+ gap: 8px;
960
+ margin-bottom: 6px;
961
  flex-wrap: wrap;
962
  justify-content: center;
963
  }
964
 
965
+ .feedback-buttons .btn {
966
+ flex: 0 0 auto;
967
+ padding: 6px 12px;
968
+ font-size: 0.8rem;
969
+ justify-content: center;
970
+ }
971
+
972
  .feedback-message {
973
+ padding: 8px;
974
  border-radius: var(--border-radius-sm);
975
  font-weight: 600;
976
  text-align: center;
977
  font-size: 0.85rem;
 
 
 
 
 
 
 
 
 
 
 
 
 
978
  }
979
 
980
  .feedback-message.success {
 
1040
  .chat-header h4 {
1041
  font-family: 'Poppins', sans-serif;
1042
  color: var(--primary-dark);
1043
+ font-size: 0.95rem;
1044
  }
1045
 
1046
  .chat-messages {
 
1287
  grid-template-columns: 1fr;
1288
  }
1289
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
requirements.txt CHANGED
@@ -8,4 +8,5 @@ transformers==4.57.1
8
  google-genai==1.51.0
9
  sentence-transformers==5.1.2
10
  fastapi==0.115.0
11
- uvicorn[standard]==0.32.0
 
 
8
  google-genai==1.51.0
9
  sentence-transformers==5.1.2
10
  fastapi==0.115.0
11
+ uvicorn[standard]==0.32.0
12
+ gradio==5.34.2