Upload 3 files
Browse files- Dockerfile +14 -0
- app.py +916 -0
- requirements.txt +5 -0
Dockerfile
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.10-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
COPY requirements.txt .
|
| 6 |
+
RUN pip install -r requirements.txt
|
| 7 |
+
|
| 8 |
+
COPY app.py .
|
| 9 |
+
|
| 10 |
+
EXPOSE 7860
|
| 11 |
+
|
| 12 |
+
ENV FLASK_APP=app.py
|
| 13 |
+
|
| 14 |
+
CMD ["python", "app.py"]
|
app.py
ADDED
|
@@ -0,0 +1,916 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from flask import Flask, request, jsonify, render_template_string, session, redirect
|
| 2 |
+
import urllib.request
|
| 3 |
+
import re
|
| 4 |
+
import sqlite3
|
| 5 |
+
import datetime
|
| 6 |
+
|
| 7 |
+
app = Flask(__name__)
|
| 8 |
+
app.secret_key = 'ctteen-admin-secret-2024'
|
| 9 |
+
|
| 10 |
+
ADMIN_PASSWORD = "Okan4715!"
|
| 11 |
+
DB_PATH = "analyses.db"
|
| 12 |
+
|
| 13 |
+
ANTISEMITIC_PATTERNS = [
|
| 14 |
+
{
|
| 15 |
+
"keyword": "jewish conspiracy",
|
| 16 |
+
"explanation": "This phrase promotes the false and dangerous conspiracy theory that Jewish people secretly control world events.",
|
| 17 |
+
"trending": True,
|
| 18 |
+
"trending_note": "This trope has surged on social media following recent global events.",
|
| 19 |
+
"counter": "Jewish people do not operate secret conspiracies. This is a centuries-old antisemitic myth used to scapegoat Jewish communities and has been debunked repeatedly by historians and scholars."
|
| 20 |
+
},
|
| 21 |
+
{
|
| 22 |
+
"keyword": "jews control",
|
| 23 |
+
"explanation": "This phrase falsely claims Jewish people have secret control over institutions like banks, media, or governments.",
|
| 24 |
+
"trending": True,
|
| 25 |
+
"trending_note": "Variants of this conspiracy theory are among the most widely spread antisemitic tropes online.",
|
| 26 |
+
"counter": "The claim that Jewish people control major institutions is a harmful stereotype with no basis in fact. It echoes propaganda historically used to justify persecution and genocide."
|
| 27 |
+
},
|
| 28 |
+
{
|
| 29 |
+
"keyword": "jewish agenda",
|
| 30 |
+
"explanation": "This implies Jewish people have a secret plan to manipulate society, a classic antisemitic conspiracy theory.",
|
| 31 |
+
"trending": False,
|
| 32 |
+
"trending_note": "",
|
| 33 |
+
"counter": "There is no Jewish agenda. This phrase falsely portrays Jewish people as a unified scheming group rather than a diverse community of individuals with varied beliefs and values."
|
| 34 |
+
},
|
| 35 |
+
{
|
| 36 |
+
"keyword": "holocaust never happened",
|
| 37 |
+
"explanation": "Holocaust denial rejects the historical fact that six million Jewish people were systematically murdered by Nazi Germany.",
|
| 38 |
+
"trending": True,
|
| 39 |
+
"trending_note": "Holocaust denial content has increased significantly on unmoderated platforms.",
|
| 40 |
+
"counter": "The Holocaust is one of the most thoroughly documented events in human history, confirmed by thousands of survivors, military records, and physical evidence. Denying it is both factually wrong and deeply harmful."
|
| 41 |
+
},
|
| 42 |
+
{
|
| 43 |
+
"keyword": "holocaust denial",
|
| 44 |
+
"explanation": "Denying the Holocaust erases the genocide of six million Jewish people and causes immense harm to survivors and their families.",
|
| 45 |
+
"trending": True,
|
| 46 |
+
"trending_note": "Holocaust denial content has increased significantly on unmoderated platforms.",
|
| 47 |
+
"counter": "The Holocaust is one of the most thoroughly documented events in human history. Denying it has been criminalized in many countries and causes immense pain to survivors and their descendants."
|
| 48 |
+
},
|
| 49 |
+
{
|
| 50 |
+
"keyword": "zionist conspiracy",
|
| 51 |
+
"explanation": "Using Zionist as a substitute for Jewish people to push conspiracy theories is a recognizable form of antisemitism.",
|
| 52 |
+
"trending": True,
|
| 53 |
+
"trending_note": "This framing has become increasingly common as a way to disguise antisemitism as political commentary.",
|
| 54 |
+
"counter": "Zionism is the belief in Jewish self-determination. Using it as a stand-in for antisemitic conspiracy theories conflates political criticism with ethnic hatred and targets Jewish identity."
|
| 55 |
+
},
|
| 56 |
+
{
|
| 57 |
+
"keyword": "jewish bankers",
|
| 58 |
+
"explanation": "This phrase invokes the antisemitic stereotype that Jewish people manipulate global finances for personal gain.",
|
| 59 |
+
"trending": False,
|
| 60 |
+
"trending_note": "",
|
| 61 |
+
"counter": "The greedy Jewish banker trope is a centuries-old antisemitic stereotype with no factual basis. It was used extensively in Nazi propaganda to justify persecution and remains deeply harmful today."
|
| 62 |
+
},
|
| 63 |
+
{
|
| 64 |
+
"keyword": "jewish media control",
|
| 65 |
+
"explanation": "This phrase falsely claims Jewish people secretly control media to manipulate public opinion.",
|
| 66 |
+
"trending": True,
|
| 67 |
+
"trending_note": "Claims about Jewish media control have spiked across multiple social platforms recently.",
|
| 68 |
+
"counter": "Jewish people do not control the media. This conspiracy theory has been used for over a century to justify discrimination and violence against Jewish communities worldwide."
|
| 69 |
+
},
|
| 70 |
+
{
|
| 71 |
+
"keyword": "kill jews",
|
| 72 |
+
"explanation": "This is a direct and explicit call for violence against Jewish people.",
|
| 73 |
+
"trending": False,
|
| 74 |
+
"trending_note": "",
|
| 75 |
+
"counter": "This is an explicit incitement to violence against Jewish people and should be reported to law enforcement and the platform immediately. This is not protected speech."
|
| 76 |
+
},
|
| 77 |
+
{
|
| 78 |
+
"keyword": "death to jews",
|
| 79 |
+
"explanation": "This is an explicit call for violence and genocide against Jewish people.",
|
| 80 |
+
"trending": False,
|
| 81 |
+
"trending_note": "",
|
| 82 |
+
"counter": "This is an explicit threat of violence against Jewish people. Report this to law enforcement and the platform hosting it immediately. This content may be illegal in your jurisdiction."
|
| 83 |
+
},
|
| 84 |
+
{
|
| 85 |
+
"keyword": "dirty jew",
|
| 86 |
+
"explanation": "This is a racial slur that dehumanizes Jewish people using degrading language.",
|
| 87 |
+
"trending": False,
|
| 88 |
+
"trending_note": "",
|
| 89 |
+
"counter": "This is a dehumanizing slur targeting Jewish people. Language like this has historically preceded acts of violence and discrimination against Jewish communities."
|
| 90 |
+
},
|
| 91 |
+
{
|
| 92 |
+
"keyword": "filthy jew",
|
| 93 |
+
"explanation": "This is a dehumanizing slur that uses degrading language to attack Jewish identity.",
|
| 94 |
+
"trending": False,
|
| 95 |
+
"trending_note": "",
|
| 96 |
+
"counter": "Dehumanizing slurs targeting Jewish people have historically been used to justify violence and discrimination. This language should be reported and challenged wherever it appears."
|
| 97 |
+
},
|
| 98 |
+
{
|
| 99 |
+
"keyword": "stupid jew",
|
| 100 |
+
"explanation": "This is a slur combining ethnic hatred with degrading language targeting Jewish people.",
|
| 101 |
+
"trending": False,
|
| 102 |
+
"trending_note": "",
|
| 103 |
+
"counter": "Ethnic slurs targeting Jewish people are a form of hate speech regardless of intent. This language contributes to a culture of discrimination and should be reported."
|
| 104 |
+
},
|
| 105 |
+
{
|
| 106 |
+
"keyword": "jews are evil",
|
| 107 |
+
"explanation": "Attributing evil as an inherent trait of all Jewish people is classic antisemitic dehumanization.",
|
| 108 |
+
"trending": False,
|
| 109 |
+
"trending_note": "",
|
| 110 |
+
"counter": "Attributing negative moral traits to an entire ethnic or religious group is prejudice. Jewish people are a diverse community and cannot be characterized as a monolith."
|
| 111 |
+
},
|
| 112 |
+
{
|
| 113 |
+
"keyword": "jews are greedy",
|
| 114 |
+
"explanation": "This perpetuates the antisemitic stereotype that Jewish people are inherently dishonest or obsessed with money.",
|
| 115 |
+
"trending": False,
|
| 116 |
+
"trending_note": "",
|
| 117 |
+
"counter": "Attributing greed to an entire ethnic or religious group is prejudice. This stereotype has been used to justify discrimination against Jewish people for centuries."
|
| 118 |
+
},
|
| 119 |
+
{
|
| 120 |
+
"keyword": "jews are subhuman",
|
| 121 |
+
"explanation": "This is explicit dehumanization of Jewish people, the kind of language that directly precedes genocide.",
|
| 122 |
+
"trending": False,
|
| 123 |
+
"trending_note": "",
|
| 124 |
+
"counter": "Describing any group of people as subhuman is among the most dangerous forms of hate speech. This language was central to Nazi propaganda and must be reported to law enforcement immediately."
|
| 125 |
+
},
|
| 126 |
+
{
|
| 127 |
+
"keyword": "all jews are",
|
| 128 |
+
"explanation": "Attributing any negative characteristic to all Jewish people as a monolithic group is antisemitic stereotyping.",
|
| 129 |
+
"trending": False,
|
| 130 |
+
"trending_note": "",
|
| 131 |
+
"counter": "Jewish people are an incredibly diverse community spanning many nationalities, backgrounds, and beliefs. Negative generalizations about the entire group are prejudice, plain and simple."
|
| 132 |
+
},
|
| 133 |
+
{
|
| 134 |
+
"keyword": "jewish manipulation",
|
| 135 |
+
"explanation": "This accuses Jewish people as a group of deceptively controlling others, a classic antisemitic trope.",
|
| 136 |
+
"trending": False,
|
| 137 |
+
"trending_note": "",
|
| 138 |
+
"counter": "Accusing Jewish people of collective manipulation is a harmful stereotype rooted in antisemitic conspiracy theories. It falsely portrays an entire community as inherently deceptive."
|
| 139 |
+
},
|
| 140 |
+
{
|
| 141 |
+
"keyword": "jewish problem",
|
| 142 |
+
"explanation": "This phrase mirrors Nazi rhetoric about Jewish people being a problem to be solved, language that preceded genocide.",
|
| 143 |
+
"trending": False,
|
| 144 |
+
"trending_note": "",
|
| 145 |
+
"counter": "The framing of Jewish people as a problem is language directly drawn from Nazi ideology that led to the Holocaust. This rhetoric is extremely dangerous and should be reported immediately."
|
| 146 |
+
},
|
| 147 |
+
{
|
| 148 |
+
"keyword": "hate jews",
|
| 149 |
+
"explanation": "An explicit expression of hatred toward Jewish people as a group.",
|
| 150 |
+
"trending": False,
|
| 151 |
+
"trending_note": "",
|
| 152 |
+
"counter": "Expressions of hatred toward any ethnic or religious group contribute to discrimination and violence. This content should be reported to the platform immediately."
|
| 153 |
+
},
|
| 154 |
+
{
|
| 155 |
+
"keyword": "jew is a",
|
| 156 |
+
"explanation": "Using Jew as a pejorative descriptor is a form of antisemitic slur usage.",
|
| 157 |
+
"trending": False,
|
| 158 |
+
"trending_note": "",
|
| 159 |
+
"counter": "Using Jewish identity as a negative descriptor is a form of antisemitism. This language should be challenged and reported."
|
| 160 |
+
},
|
| 161 |
+
{
|
| 162 |
+
"keyword": "jew is evil",
|
| 163 |
+
"explanation": "Attributing evil to Jewish identity is explicit antisemitic dehumanization.",
|
| 164 |
+
"trending": False,
|
| 165 |
+
"trending_note": "",
|
| 166 |
+
"counter": "Attributing negative moral traits to Jewish identity is antisemitism. This language has historically been used to justify persecution of Jewish communities."
|
| 167 |
+
}
|
| 168 |
+
]
|
| 169 |
+
|
| 170 |
+
ANTISEMITIC_KEYWORDS = [p["keyword"] for p in ANTISEMITIC_PATTERNS]
|
| 171 |
+
|
| 172 |
+
JEWISH_CONTEXT_WORDS = [
|
| 173 |
+
"jew ", " jew", "jewish", "zionist", "synagogue",
|
| 174 |
+
"rabbi", "kosher", "torah", "talmud", "holocaust",
|
| 175 |
+
"antisemit", "semit", "yarmulke", "menorah", "passover",
|
| 176 |
+
"hanukkah", "bar mitzvah", "bat mitzvah"
|
| 177 |
+
]
|
| 178 |
+
|
| 179 |
+
def init_db():
|
| 180 |
+
conn = sqlite3.connect(DB_PATH)
|
| 181 |
+
c = conn.cursor()
|
| 182 |
+
c.execute('''CREATE TABLE IF NOT EXISTS analyses (
|
| 183 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 184 |
+
timestamp TEXT, input_type TEXT, input_preview TEXT,
|
| 185 |
+
result_type TEXT, confidence REAL, flags TEXT
|
| 186 |
+
)''')
|
| 187 |
+
conn.commit()
|
| 188 |
+
conn.close()
|
| 189 |
+
|
| 190 |
+
def save_analysis(input_type, input_preview, result_type, confidence, flags):
|
| 191 |
+
conn = sqlite3.connect(DB_PATH)
|
| 192 |
+
c = conn.cursor()
|
| 193 |
+
c.execute('INSERT INTO analyses (timestamp, input_type, input_preview, result_type, confidence, flags) VALUES (?,?,?,?,?,?)',
|
| 194 |
+
(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'), input_type, input_preview[:300], result_type, confidence, ', '.join(flags)))
|
| 195 |
+
conn.commit()
|
| 196 |
+
conn.close()
|
| 197 |
+
|
| 198 |
+
def get_stats():
|
| 199 |
+
conn = sqlite3.connect(DB_PATH)
|
| 200 |
+
c = conn.cursor()
|
| 201 |
+
c.execute('SELECT COUNT(*) FROM analyses')
|
| 202 |
+
total = c.fetchone()[0]
|
| 203 |
+
c.execute("SELECT COUNT(*) FROM analyses WHERE result_type='antisemitic'")
|
| 204 |
+
antisemitic = c.fetchone()[0]
|
| 205 |
+
c.execute("SELECT COUNT(*) FROM analyses WHERE result_type='hate'")
|
| 206 |
+
hate = c.fetchone()[0]
|
| 207 |
+
c.execute("SELECT COUNT(*) FROM analyses WHERE result_type='clean'")
|
| 208 |
+
clean = c.fetchone()[0]
|
| 209 |
+
conn.close()
|
| 210 |
+
return {'total': total, 'antisemitic': antisemitic, 'hate': hate, 'clean': clean}
|
| 211 |
+
|
| 212 |
+
def get_all_analyses():
|
| 213 |
+
conn = sqlite3.connect(DB_PATH)
|
| 214 |
+
c = conn.cursor()
|
| 215 |
+
c.execute('SELECT * FROM analyses ORDER BY id DESC LIMIT 500')
|
| 216 |
+
rows = c.fetchall()
|
| 217 |
+
conn.close()
|
| 218 |
+
return rows
|
| 219 |
+
|
| 220 |
+
from transformers import pipeline
|
| 221 |
+
classifier = pipeline("text-classification", model="facebook/roberta-hate-speech-dynabench-r4-target")
|
| 222 |
+
|
| 223 |
+
def classify_text(text):
|
| 224 |
+
result = classifier(text[:512])[0]
|
| 225 |
+
label = result['label']
|
| 226 |
+
score = round(result['score'] * 100, 2)
|
| 227 |
+
text_lower = text.lower()
|
| 228 |
+
matched_patterns = [p for p in ANTISEMITIC_PATTERNS if p["keyword"] in text_lower]
|
| 229 |
+
triggered_keywords = [p["keyword"] for p in matched_patterns]
|
| 230 |
+
has_jewish_context = any(word in text_lower for word in JEWISH_CONTEXT_WORDS)
|
| 231 |
+
is_antisemitic = len(matched_patterns) > 0 or (label == "hate" and has_jewish_context)
|
| 232 |
+
is_general_hate = label == "hate" and not is_antisemitic
|
| 233 |
+
if is_antisemitic:
|
| 234 |
+
explanations = []
|
| 235 |
+
counters = []
|
| 236 |
+
trending_notes = []
|
| 237 |
+
is_trending = False
|
| 238 |
+
for p in matched_patterns:
|
| 239 |
+
explanations.append({"phrase": p["keyword"], "explanation": p["explanation"]})
|
| 240 |
+
counters.append(p["counter"])
|
| 241 |
+
if p["trending"]:
|
| 242 |
+
is_trending = True
|
| 243 |
+
trending_notes.append(p["trending_note"])
|
| 244 |
+
if not matched_patterns:
|
| 245 |
+
explanations.append({"phrase": "context detected", "explanation": "The AI model detected antisemitic language based on context and tone."})
|
| 246 |
+
counters.append("Antisemitic language causes real harm to Jewish communities. If you encounter this content online, report it to the platform and the ADL.")
|
| 247 |
+
return {
|
| 248 |
+
'type': 'antisemitic', 'icon': '⚠', 'title': 'Antisemitic Content Detected',
|
| 249 |
+
'confidence': score, 'flags': triggered_keywords, 'explanations': explanations,
|
| 250 |
+
'counter': counters[0] if counters else "",
|
| 251 |
+
'is_trending': is_trending,
|
| 252 |
+
'trending_note': trending_notes[0] if trending_notes else "",
|
| 253 |
+
'message': 'This text contains antisemitic language specifically targeting Jewish people.'
|
| 254 |
+
}
|
| 255 |
+
elif is_general_hate:
|
| 256 |
+
return {
|
| 257 |
+
'type': 'hate', 'icon': '!', 'title': 'General Hate Speech Detected',
|
| 258 |
+
'confidence': score, 'flags': [], 'explanations': [], 'counter': '',
|
| 259 |
+
'is_trending': False, 'trending_note': '',
|
| 260 |
+
'message': 'This text contains hateful language targeting a group of people, but does not appear to be specifically antisemitic. All forms of hate speech are harmful and should be reported.'
|
| 261 |
+
}
|
| 262 |
+
else:
|
| 263 |
+
return {
|
| 264 |
+
'type': 'clean', 'icon': '✓', 'title': 'No Hate Content Detected',
|
| 265 |
+
'confidence': score, 'flags': [], 'explanations': [], 'counter': '',
|
| 266 |
+
'is_trending': False, 'trending_note': '',
|
| 267 |
+
'message': 'This text appears to be free of antisemitic or hateful language.'
|
| 268 |
+
}
|
| 269 |
+
|
| 270 |
+
MAIN_HTML = """
|
| 271 |
+
<!DOCTYPE html>
|
| 272 |
+
<html lang="en">
|
| 273 |
+
<head>
|
| 274 |
+
<meta charset="UTF-8">
|
| 275 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 276 |
+
<title>Shield — Antisemitism Detector</title>
|
| 277 |
+
<link href="https://fonts.googleapis.com/css2?family=DM+Serif+Display:ital@0;1&family=DM+Sans:wght@300;400;500;600&display=swap" rel="stylesheet">
|
| 278 |
+
<style>
|
| 279 |
+
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
|
| 280 |
+
:root {
|
| 281 |
+
--bg: #f7f4ef; --bg2: #eee9e0; --surface: #ffffff; --border: #d8d0c4;
|
| 282 |
+
--text: #1a1510; --text2: #6b6358; --text3: #9c9189;
|
| 283 |
+
--accent: #1e3a5f; --accent2: #2d5aa0; --gold: #c4962a;
|
| 284 |
+
--danger: #8b1a1a; --danger-bg: #fdf0f0; --danger-border: #e8b4b4;
|
| 285 |
+
--warn: #7a4a00; --warn-bg: #fdf6e8; --warn-border: #e8d4a0;
|
| 286 |
+
--ok: #1a4a2a; --ok-bg: #f0faf3; --ok-border: #a8d4b4;
|
| 287 |
+
--trending: #4a1a6b; --trending-bg: #f5f0fd; --trending-border: #c4a8e8;
|
| 288 |
+
}
|
| 289 |
+
body { font-family: 'DM Sans', sans-serif; background: var(--bg); color: var(--text); min-height: 100vh; }
|
| 290 |
+
nav { background: var(--accent); padding: 0 48px; height: 64px; display: flex; align-items: center; justify-content: space-between; }
|
| 291 |
+
.nav-brand { display: flex; align-items: center; gap: 10px; }
|
| 292 |
+
.nav-star { width: 32px; height: 32px; background: rgba(255,255,255,0.15); border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 16px; color: white; }
|
| 293 |
+
.nav-title { font-family: 'DM Serif Display', serif; font-size: 20px; color: white; }
|
| 294 |
+
.nav-tagline { font-size: 12px; color: rgba(255,255,255,0.5); font-weight: 300; }
|
| 295 |
+
.hero { background: var(--accent); padding: 72px 48px 80px; position: relative; overflow: hidden; }
|
| 296 |
+
.hero::after { content: '✡'; position: absolute; right: 80px; top: 50%; transform: translateY(-50%); font-size: 180px; color: rgba(255,255,255,0.04); }
|
| 297 |
+
.hero-label { font-size: 11px; font-weight: 600; letter-spacing: 2px; text-transform: uppercase; color: var(--gold); margin-bottom: 16px; }
|
| 298 |
+
.hero h1 { font-family: 'DM Serif Display', serif; font-size: 52px; color: white; line-height: 1.15; max-width: 600px; margin-bottom: 20px; }
|
| 299 |
+
.hero h1 em { font-style: italic; color: rgba(255,255,255,0.7); }
|
| 300 |
+
.hero p { font-size: 16px; color: rgba(255,255,255,0.6); max-width: 480px; line-height: 1.7; font-weight: 300; }
|
| 301 |
+
.hero-share { display: inline-flex; align-items: center; gap: 8px; margin-top: 28px; background: rgba(255,255,255,0.12); border: 1px solid rgba(255,255,255,0.25); color: white; font-size: 13px; font-weight: 500; font-family: 'DM Sans', sans-serif; padding: 10px 20px; border-radius: 8px; cursor: pointer; transition: background 0.2s; }
|
| 302 |
+
.hero-share:hover { background: rgba(255,255,255,0.2); }
|
| 303 |
+
.share-icon { font-size: 14px; }
|
| 304 |
+
.stats-bar { background: var(--bg2); border-bottom: 1px solid var(--border); padding: 0 48px; display: flex; }
|
| 305 |
+
.stat-item { padding: 20px 40px 20px 0; margin-right: 40px; border-right: 1px solid var(--border); }
|
| 306 |
+
.stat-item:last-child { border-right: none; }
|
| 307 |
+
.stat-num { font-family: 'DM Serif Display', serif; font-size: 28px; color: var(--accent); }
|
| 308 |
+
.stat-label { font-size: 11px; color: var(--text3); text-transform: uppercase; letter-spacing: 1px; margin-top: 2px; }
|
| 309 |
+
.main { max-width: 860px; margin: 0 auto; padding: 48px 24px 80px; }
|
| 310 |
+
.section-label { font-size: 11px; font-weight: 600; letter-spacing: 2px; text-transform: uppercase; color: var(--text3); margin-bottom: 16px; }
|
| 311 |
+
.card { background: var(--surface); border: 1px solid var(--border); border-radius: 16px; padding: 32px; margin-bottom: 24px; box-shadow: 0 2px 8px rgba(0,0,0,0.04); }
|
| 312 |
+
.tabs { display: flex; border: 1px solid var(--border); border-radius: 10px; padding: 4px; background: var(--bg); margin-bottom: 24px; width: fit-content; }
|
| 313 |
+
.tab { padding: 8px 24px; border-radius: 7px; border: none; background: transparent; color: var(--text2); font-size: 13px; font-family: 'DM Sans', sans-serif; font-weight: 500; cursor: pointer; transition: all 0.2s; }
|
| 314 |
+
.tab.active { background: var(--surface); color: var(--accent); box-shadow: 0 1px 4px rgba(0,0,0,0.1); }
|
| 315 |
+
.tab-content { display: none; }
|
| 316 |
+
.tab-content.active { display: block; }
|
| 317 |
+
textarea, .url-input { width: 100%; background: var(--bg); border: 1.5px solid var(--border); border-radius: 10px; color: var(--text); font-size: 15px; font-family: 'DM Sans', sans-serif; font-weight: 300; padding: 16px; outline: none; transition: border-color 0.2s; }
|
| 318 |
+
textarea { resize: none; height: 140px; }
|
| 319 |
+
.url-input { height: 52px; }
|
| 320 |
+
textarea:focus, .url-input:focus { border-color: var(--accent2); background: white; }
|
| 321 |
+
textarea::placeholder, .url-input::placeholder { color: var(--text3); }
|
| 322 |
+
.url-note { font-size: 12px; color: var(--text3); margin-top: 8px; font-weight: 300; }
|
| 323 |
+
.btn { display: flex; align-items: center; justify-content: center; width: 100%; background: var(--accent); border: none; color: white; font-size: 14px; font-weight: 500; font-family: 'DM Sans', sans-serif; padding: 15px; border-radius: 10px; cursor: pointer; margin-top: 16px; transition: background 0.2s, transform 0.1s; }
|
| 324 |
+
.btn:hover { background: var(--accent2); }
|
| 325 |
+
.btn:active { transform: scale(0.99); }
|
| 326 |
+
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
| 327 |
+
.loading { text-align: center; padding: 24px; display: none; color: var(--text2); font-size: 14px; }
|
| 328 |
+
.spinner { width: 24px; height: 24px; border: 2px solid var(--border); border-top-color: var(--accent); border-radius: 50%; animation: spin 0.8s linear infinite; margin: 0 auto 12px; }
|
| 329 |
+
@keyframes spin { to { transform: rotate(360deg); } }
|
| 330 |
+
.result-box { display: none; border-radius: 12px; padding: 24px; margin-top: 16px; animation: fadeIn 0.3s ease; }
|
| 331 |
+
@keyframes fadeIn { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
|
| 332 |
+
.result-box.antisemitic { background: var(--danger-bg); border: 1.5px solid var(--danger-border); }
|
| 333 |
+
.result-box.hate { background: var(--warn-bg); border: 1.5px solid var(--warn-border); }
|
| 334 |
+
.result-box.clean { background: var(--ok-bg); border: 1.5px solid var(--ok-border); }
|
| 335 |
+
.result-header { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; }
|
| 336 |
+
.result-icon { width: 40px; height: 40px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 18px; font-weight: 700; flex-shrink: 0; }
|
| 337 |
+
.antisemitic .result-icon { background: var(--danger); color: white; }
|
| 338 |
+
.hate .result-icon { background: var(--warn); color: white; }
|
| 339 |
+
.clean .result-icon { background: var(--ok); color: white; }
|
| 340 |
+
.result-title { font-family: 'DM Serif Display', serif; font-size: 20px; }
|
| 341 |
+
.antisemitic .result-title { color: var(--danger); }
|
| 342 |
+
.hate .result-title { color: var(--warn); }
|
| 343 |
+
.clean .result-title { color: var(--ok); }
|
| 344 |
+
.result-type-badge { display: inline-block; font-size: 10px; font-weight: 600; letter-spacing: 1.5px; text-transform: uppercase; padding: 3px 10px; border-radius: 20px; margin-bottom: 14px; }
|
| 345 |
+
.antisemitic .result-type-badge { background: var(--danger); color: white; }
|
| 346 |
+
.hate .result-type-badge { background: var(--warn); color: white; }
|
| 347 |
+
.clean .result-type-badge { background: var(--ok); color: white; }
|
| 348 |
+
.confidence-row { display: flex; align-items: center; gap: 12px; margin-bottom: 16px; }
|
| 349 |
+
.confidence-label { font-size: 12px; color: var(--text3); width: 80px; flex-shrink: 0; }
|
| 350 |
+
.confidence-bar { flex: 1; background: rgba(0,0,0,0.08); border-radius: 4px; height: 5px; overflow: hidden; }
|
| 351 |
+
.confidence-fill { height: 100%; border-radius: 4px; transition: width 0.6s ease; }
|
| 352 |
+
.antisemitic .confidence-fill { background: var(--danger); }
|
| 353 |
+
.hate .confidence-fill { background: var(--warn); }
|
| 354 |
+
.clean .confidence-fill { background: var(--ok); }
|
| 355 |
+
.confidence-value { font-size: 13px; font-weight: 600; width: 44px; text-align: right; flex-shrink: 0; }
|
| 356 |
+
.result-msg { font-size: 14px; color: var(--text2); line-height: 1.65; font-weight: 300; margin-bottom: 16px; }
|
| 357 |
+
.trending-banner { background: var(--trending-bg); border: 1px solid var(--trending-border); border-radius: 8px; padding: 12px 16px; margin-bottom: 14px; display: flex; align-items: flex-start; gap: 10px; }
|
| 358 |
+
.trending-icon { font-size: 16px; flex-shrink: 0; margin-top: 1px; }
|
| 359 |
+
.trending-text { font-size: 13px; color: var(--trending); line-height: 1.5; }
|
| 360 |
+
.trending-text strong { font-weight: 600; }
|
| 361 |
+
.explanation-block { margin-bottom: 14px; }
|
| 362 |
+
.explanation-title { font-size: 11px; font-weight: 600; color: var(--text3); text-transform: uppercase; letter-spacing: 1px; margin-bottom: 10px; }
|
| 363 |
+
.explanation-item { background: rgba(139,26,26,0.06); border-left: 3px solid var(--danger); border-radius: 0 8px 8px 0; padding: 12px 14px; margin-bottom: 8px; }
|
| 364 |
+
.explanation-phrase { font-size: 13px; font-weight: 600; color: var(--danger); margin-bottom: 4px; font-family: 'DM Serif Display', serif; }
|
| 365 |
+
.explanation-text { font-size: 13px; color: var(--text2); line-height: 1.55; font-weight: 300; }
|
| 366 |
+
.counter-block { background: rgba(30,58,95,0.06); border-left: 3px solid var(--accent); border-radius: 0 8px 8px 0; padding: 14px; margin-bottom: 16px; }
|
| 367 |
+
.counter-title { font-size: 11px; font-weight: 600; color: var(--accent); text-transform: uppercase; letter-spacing: 1px; margin-bottom: 8px; }
|
| 368 |
+
.counter-text { font-size: 13px; color: var(--text2); line-height: 1.65; font-weight: 300; }
|
| 369 |
+
.copy-counter { display: inline-flex; align-items: center; gap: 6px; margin-top: 10px; background: transparent; border: 1px solid var(--border); color: var(--accent); font-size: 12px; font-family: 'DM Sans', sans-serif; font-weight: 500; padding: 6px 12px; border-radius: 6px; cursor: pointer; transition: background 0.2s; }
|
| 370 |
+
.copy-counter:hover { background: var(--bg); }
|
| 371 |
+
.copy-counter.copied { color: var(--ok); border-color: var(--ok-border); }
|
| 372 |
+
.report-section { margin-top: 4px; }
|
| 373 |
+
.report-title { font-size: 11px; font-weight: 600; color: var(--text3); text-transform: uppercase; letter-spacing: 1px; margin-bottom: 10px; }
|
| 374 |
+
.report-buttons { display: flex; gap: 8px; flex-wrap: wrap; }
|
| 375 |
+
.report-btn { display: inline-flex; align-items: center; gap: 6px; color: white; font-size: 12px; font-weight: 500; font-family: 'DM Sans', sans-serif; text-decoration: none; padding: 8px 14px; border-radius: 8px; transition: opacity 0.2s; }
|
| 376 |
+
.report-btn:hover { opacity: 0.85; }
|
| 377 |
+
.report-btn.adl { background: #1e3a5f; }
|
| 378 |
+
.report-btn.twitter { background: #000000; }
|
| 379 |
+
.report-btn.facebook { background: #1877f2; }
|
| 380 |
+
.report-btn.stopas { background: #8b1a1a; }
|
| 381 |
+
.history-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; }
|
| 382 |
+
.history-clear { background: transparent; border: 1px solid var(--border); color: var(--text3); font-size: 12px; font-family: 'DM Sans', sans-serif; padding: 6px 14px; border-radius: 8px; cursor: pointer; transition: all 0.2s; }
|
| 383 |
+
.history-clear:hover { border-color: var(--danger); color: var(--danger); }
|
| 384 |
+
.history-empty { text-align: center; color: var(--text3); font-size: 14px; padding: 32px; font-weight: 300; }
|
| 385 |
+
.history-item { display: flex; align-items: center; gap: 14px; padding: 12px 16px; background: var(--bg); border: 1px solid var(--border); border-radius: 10px; margin-bottom: 8px; }
|
| 386 |
+
.history-badge { font-size: 11px; font-weight: 600; padding: 3px 10px; border-radius: 20px; white-space: nowrap; }
|
| 387 |
+
.history-badge.antisemitic { background: var(--danger-bg); color: var(--danger); border: 1px solid var(--danger-border); }
|
| 388 |
+
.history-badge.hate { background: var(--warn-bg); color: var(--warn); border: 1px solid var(--warn-border); }
|
| 389 |
+
.history-badge.clean { background: var(--ok-bg); color: var(--ok); border: 1px solid var(--ok-border); }
|
| 390 |
+
.history-text { font-size: 13px; color: var(--text2); flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 300; }
|
| 391 |
+
.history-time { font-size: 11px; color: var(--text3); white-space: nowrap; }
|
| 392 |
+
.callout-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-top: 24px; }
|
| 393 |
+
.callout-card { border-radius: 14px; padding: 28px; }
|
| 394 |
+
.callout-card.mission { background: var(--accent); }
|
| 395 |
+
.callout-card.action { background: var(--gold); }
|
| 396 |
+
.callout-card h4 { font-family: 'DM Serif Display', serif; font-size: 20px; margin-bottom: 10px; }
|
| 397 |
+
.callout-card.mission h4 { color: white; }
|
| 398 |
+
.callout-card.action h4 { color: #3a2500; }
|
| 399 |
+
.callout-card p { font-size: 14px; line-height: 1.65; font-weight: 300; }
|
| 400 |
+
.callout-card.mission p { color: rgba(255,255,255,0.7); }
|
| 401 |
+
.callout-card.action p { color: rgba(58,37,0,0.8); }
|
| 402 |
+
.callout-card a { display: inline-flex; align-items: center; gap: 6px; margin-top: 16px; font-size: 13px; font-weight: 500; text-decoration: none; padding: 8px 16px; border-radius: 8px; transition: opacity 0.2s; }
|
| 403 |
+
.callout-card a:hover { opacity: 0.85; }
|
| 404 |
+
.callout-card.mission a { background: rgba(255,255,255,0.15); color: white; border: 1px solid rgba(255,255,255,0.25); }
|
| 405 |
+
.callout-card.action a { background: rgba(58,37,0,0.12); color: #3a2500; border: 1px solid rgba(58,37,0,0.2); }
|
| 406 |
+
.info-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
|
| 407 |
+
.info-item { background: var(--bg); border: 1px solid var(--border); border-radius: 10px; padding: 18px; }
|
| 408 |
+
.info-item h4 { font-size: 13px; font-weight: 600; color: var(--accent); margin-bottom: 6px; }
|
| 409 |
+
.info-item p { font-size: 13px; color: var(--text2); line-height: 1.6; font-weight: 300; }
|
| 410 |
+
.scraped-preview { background: var(--bg); border: 1px solid var(--border); border-radius: 8px; padding: 12px; margin-top: 12px; font-size: 12px; color: var(--text2); line-height: 1.5; display: none; max-height: 72px; overflow: hidden; font-weight: 300; }
|
| 411 |
+
.share-toast { position: fixed; bottom: 32px; left: 50%; transform: translateX(-50%); background: var(--accent); color: white; font-size: 13px; font-weight: 500; padding: 12px 24px; border-radius: 10px; opacity: 0; transition: opacity 0.3s; pointer-events: none; z-index: 999; }
|
| 412 |
+
.share-toast.show { opacity: 1; }
|
| 413 |
+
footer { background: var(--accent); padding: 32px 48px; text-align: center; }
|
| 414 |
+
footer p { font-size: 13px; color: rgba(255,255,255,0.4); font-weight: 300; }
|
| 415 |
+
footer a { color: rgba(255,255,255,0.6); text-decoration: none; }
|
| 416 |
+
footer a:hover { color: white; }
|
| 417 |
+
</style>
|
| 418 |
+
</head>
|
| 419 |
+
<body>
|
| 420 |
+
|
| 421 |
+
<nav>
|
| 422 |
+
<div class="nav-brand">
|
| 423 |
+
<div class="nav-star">✡</div>
|
| 424 |
+
<span class="nav-title">Shield</span>
|
| 425 |
+
</div>
|
| 426 |
+
<span class="nav-tagline">AI-Powered Antisemitism Detection</span>
|
| 427 |
+
</nav>
|
| 428 |
+
|
| 429 |
+
<div class="hero">
|
| 430 |
+
<div class="hero-label">Built for Jewish Communities</div>
|
| 431 |
+
<h1>Detect Hate.<br><em>Protect Communities.</em></h1>
|
| 432 |
+
<p>Paste any text or scan any URL to instantly identify antisemitic content using advanced artificial intelligence.</p>
|
| 433 |
+
<button class="hero-share" onclick="shareApp()">
|
| 434 |
+
<span class="share-icon">⬆</span> Share This Tool
|
| 435 |
+
</button>
|
| 436 |
+
</div>
|
| 437 |
+
|
| 438 |
+
<div class="stats-bar">
|
| 439 |
+
<div class="stat-item"><div class="stat-num" id="statTotal">0</div><div class="stat-label">Analyzed This Session</div></div>
|
| 440 |
+
<div class="stat-item"><div class="stat-num" id="statAntisemitic">0</div><div class="stat-label">Antisemitic Found</div></div>
|
| 441 |
+
<div class="stat-item"><div class="stat-num" id="statHate">0</div><div class="stat-label">General Hate Found</div></div>
|
| 442 |
+
<div class="stat-item"><div class="stat-num" id="statClean">0</div><div class="stat-label">Clean</div></div>
|
| 443 |
+
</div>
|
| 444 |
+
|
| 445 |
+
<div class="main">
|
| 446 |
+
<div class="card">
|
| 447 |
+
<div class="section-label">Analyze Content</div>
|
| 448 |
+
<div class="tabs">
|
| 449 |
+
<button class="tab active" onclick="switchTab('text', this)">Paste Text</button>
|
| 450 |
+
<button class="tab" onclick="switchTab('url', this)">Scan URL</button>
|
| 451 |
+
</div>
|
| 452 |
+
<div class="tab-content active" id="tab-text">
|
| 453 |
+
<textarea id="inputText" placeholder="Paste any text, comment, social media post, or message here to analyze it..."></textarea>
|
| 454 |
+
<button class="btn" onclick="analyzeText()" id="textBtn">Analyze Text</button>
|
| 455 |
+
</div>
|
| 456 |
+
<div class="tab-content" id="tab-url">
|
| 457 |
+
<input class="url-input" id="inputUrl" type="url" placeholder="https://example.com/article-or-post" />
|
| 458 |
+
<p class="url-note">The tool will fetch and analyze the text content from the URL you provide.</p>
|
| 459 |
+
<div class="scraped-preview" id="scrapedPreview"></div>
|
| 460 |
+
<button class="btn" onclick="analyzeUrl()" id="urlBtn">Scan URL</button>
|
| 461 |
+
</div>
|
| 462 |
+
<div class="loading" id="loading">
|
| 463 |
+
<div class="spinner"></div>
|
| 464 |
+
<span id="loadingMsg">Analyzing with AI...</span>
|
| 465 |
+
</div>
|
| 466 |
+
<div class="result-box" id="resultBox">
|
| 467 |
+
<div class="result-header">
|
| 468 |
+
<div class="result-icon" id="resultIcon"></div>
|
| 469 |
+
<div class="result-title" id="resultTitle"></div>
|
| 470 |
+
</div>
|
| 471 |
+
<div class="result-type-badge" id="resultBadge"></div>
|
| 472 |
+
<div class="confidence-row">
|
| 473 |
+
<span class="confidence-label">Confidence</span>
|
| 474 |
+
<div class="confidence-bar"><div class="confidence-fill" id="confidenceFill"></div></div>
|
| 475 |
+
<span class="confidence-value" id="confidenceValue"></span>
|
| 476 |
+
</div>
|
| 477 |
+
<div class="result-msg" id="resultMsg"></div>
|
| 478 |
+
<div id="trendingBanner" class="trending-banner" style="display:none">
|
| 479 |
+
<span class="trending-icon">📈</span>
|
| 480 |
+
<div class="trending-text"><strong>Trending Trope:</strong> <span id="trendingNote"></span></div>
|
| 481 |
+
</div>
|
| 482 |
+
<div id="explanationBlock" class="explanation-block" style="display:none">
|
| 483 |
+
<div class="explanation-title">Why This Is Antisemitic</div>
|
| 484 |
+
<div id="explanationItems"></div>
|
| 485 |
+
</div>
|
| 486 |
+
<div id="counterBlock" class="counter-block" style="display:none">
|
| 487 |
+
<div class="counter-title">How To Respond</div>
|
| 488 |
+
<div class="counter-text" id="counterText"></div>
|
| 489 |
+
<button class="copy-counter" onclick="copyCounter()" id="copyBtn">Copy Response</button>
|
| 490 |
+
</div>
|
| 491 |
+
<div id="reportSection" class="report-section" style="display:none">
|
| 492 |
+
<div class="report-title">Report This Content</div>
|
| 493 |
+
<div class="report-buttons">
|
| 494 |
+
<a href="https://www.adl.org/report-incident" target="_blank" class="report-btn adl">Report to ADL →</a>
|
| 495 |
+
<a href="https://help.twitter.com/en/safety-and-security/report-abusive-behavior" target="_blank" class="report-btn twitter">Report on X/Twitter</a>
|
| 496 |
+
<a href="https://www.facebook.com/help/181495968648557" target="_blank" class="report-btn facebook">Report on Facebook</a>
|
| 497 |
+
<a href="https://www.stopantisemitism.org/report" target="_blank" class="report-btn stopas">StopAntisemitism.org</a>
|
| 498 |
+
</div>
|
| 499 |
+
</div>
|
| 500 |
+
</div>
|
| 501 |
+
</div>
|
| 502 |
+
|
| 503 |
+
<div class="card">
|
| 504 |
+
<div class="history-header">
|
| 505 |
+
<div class="section-label" style="margin-bottom:0">Your Session History</div>
|
| 506 |
+
<button class="history-clear" onclick="clearHistory()">Clear</button>
|
| 507 |
+
</div>
|
| 508 |
+
<div id="historyList">
|
| 509 |
+
<div class="history-empty">No analyses yet this session. Start by analyzing some text above.</div>
|
| 510 |
+
</div>
|
| 511 |
+
</div>
|
| 512 |
+
|
| 513 |
+
<div class="callout-grid">
|
| 514 |
+
<div class="callout-card mission">
|
| 515 |
+
<div style="font-size:28px;margin-bottom:14px">✡</div>
|
| 516 |
+
<h4>Why We Built This</h4>
|
| 517 |
+
<p>Antisemitic incidents have risen sharply in recent years. Jewish students, communities, and organizations need tools to identify and document hate before it spreads and causes real harm.</p>
|
| 518 |
+
<a href="https://www.adl.org/resources/report/audit-antisemitic-incidents-2023" target="_blank">View ADL Incident Report →</a>
|
| 519 |
+
</div>
|
| 520 |
+
<div class="callout-card action">
|
| 521 |
+
<div style="font-size:28px;margin-bottom:14px">⚡</div>
|
| 522 |
+
<h4>What To Do Next</h4>
|
| 523 |
+
<p>If you find antisemitic content, screenshot it immediately — platforms often remove content quickly. Then report it to the platform directly and to the ADL using the link below.</p>
|
| 524 |
+
<a href="https://www.adl.org/report-incident" target="_blank">Report to ADL →</a>
|
| 525 |
+
</div>
|
| 526 |
+
</div>
|
| 527 |
+
|
| 528 |
+
<div class="card" style="margin-top:24px">
|
| 529 |
+
<div class="section-label">How It Works</div>
|
| 530 |
+
<div class="info-grid">
|
| 531 |
+
<div class="info-item">
|
| 532 |
+
<h4>Two-Layer Detection</h4>
|
| 533 |
+
<p>Combines a RoBERTa AI model trained on thousands of examples with a specialized antisemitism pattern database for maximum accuracy.</p>
|
| 534 |
+
</div>
|
| 535 |
+
<div class="info-item">
|
| 536 |
+
<h4>Antisemitism vs. General Hate</h4>
|
| 537 |
+
<p>Specifically distinguishes between content targeting Jewish people and general hate speech so you always know exactly what you are dealing with.</p>
|
| 538 |
+
</div>
|
| 539 |
+
<div class="info-item">
|
| 540 |
+
<h4>URL Scanning</h4>
|
| 541 |
+
<p>Paste any public URL and the tool will fetch and analyze the text content automatically — articles, forums, social posts, and more.</p>
|
| 542 |
+
</div>
|
| 543 |
+
<div class="info-item">
|
| 544 |
+
<h4>Private Session History</h4>
|
| 545 |
+
<p>Your analysis history is private to your current session only and never shared with other users. Refresh the page to start a clean session.</p>
|
| 546 |
+
</div>
|
| 547 |
+
</div>
|
| 548 |
+
</div>
|
| 549 |
+
</div>
|
| 550 |
+
|
| 551 |
+
<div class="share-toast" id="shareToast">Link copied to clipboard!</div>
|
| 552 |
+
|
| 553 |
+
<footer>
|
| 554 |
+
<p>Built to support Jewish communities and combat antisemitism | <a href="https://www.adl.org" target="_blank">ADL</a> | <a href="https://www.stopantisemitism.org" target="_blank">StopAntisemitism.org</a></p>
|
| 555 |
+
</footer>
|
| 556 |
+
|
| 557 |
+
<script>
|
| 558 |
+
let sessionHistory = [];
|
| 559 |
+
let sessionStats = { total: 0, antisemitic: 0, hate: 0, clean: 0 };
|
| 560 |
+
let currentCounter = '';
|
| 561 |
+
|
| 562 |
+
function updateStats() {
|
| 563 |
+
document.getElementById('statTotal').textContent = sessionStats.total;
|
| 564 |
+
document.getElementById('statAntisemitic').textContent = sessionStats.antisemitic;
|
| 565 |
+
document.getElementById('statHate').textContent = sessionStats.hate;
|
| 566 |
+
document.getElementById('statClean').textContent = sessionStats.clean;
|
| 567 |
+
}
|
| 568 |
+
|
| 569 |
+
function switchTab(tab, el) {
|
| 570 |
+
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
| 571 |
+
document.querySelectorAll('.tab-content').forEach(t => t.classList.remove('active'));
|
| 572 |
+
el.classList.add('active');
|
| 573 |
+
document.getElementById('tab-' + tab).classList.add('active');
|
| 574 |
+
document.getElementById('resultBox').style.display = 'none';
|
| 575 |
+
}
|
| 576 |
+
|
| 577 |
+
function showResult(data) {
|
| 578 |
+
const box = document.getElementById('resultBox');
|
| 579 |
+
box.className = 'result-box ' + data.type;
|
| 580 |
+
document.getElementById('resultIcon').textContent = data.icon;
|
| 581 |
+
document.getElementById('resultTitle').textContent = data.title;
|
| 582 |
+
const badgeLabels = { antisemitic: 'Antisemitic Content', hate: 'General Hate Speech', clean: 'No Hate Detected' };
|
| 583 |
+
document.getElementById('resultBadge').textContent = badgeLabels[data.type];
|
| 584 |
+
document.getElementById('resultBadge').className = 'result-type-badge';
|
| 585 |
+
document.getElementById('confidenceFill').style.width = data.confidence + '%';
|
| 586 |
+
document.getElementById('confidenceValue').textContent = data.confidence + '%';
|
| 587 |
+
document.getElementById('resultMsg').textContent = data.message;
|
| 588 |
+
const trendingBanner = document.getElementById('trendingBanner');
|
| 589 |
+
if (data.is_trending && data.trending_note) {
|
| 590 |
+
document.getElementById('trendingNote').textContent = data.trending_note;
|
| 591 |
+
trendingBanner.style.display = 'flex';
|
| 592 |
+
} else { trendingBanner.style.display = 'none'; }
|
| 593 |
+
const explanationBlock = document.getElementById('explanationBlock');
|
| 594 |
+
if (data.explanations && data.explanations.length > 0) {
|
| 595 |
+
explanationBlock.style.display = 'block';
|
| 596 |
+
document.getElementById('explanationItems').innerHTML = data.explanations.map(e => `
|
| 597 |
+
<div class="explanation-item">
|
| 598 |
+
<div class="explanation-phrase">"${e.phrase}"</div>
|
| 599 |
+
<div class="explanation-text">${e.explanation}</div>
|
| 600 |
+
</div>
|
| 601 |
+
`).join('');
|
| 602 |
+
} else { explanationBlock.style.display = 'none'; }
|
| 603 |
+
const counterBlock = document.getElementById('counterBlock');
|
| 604 |
+
if (data.counter) {
|
| 605 |
+
currentCounter = data.counter;
|
| 606 |
+
document.getElementById('counterText').textContent = data.counter;
|
| 607 |
+
counterBlock.style.display = 'block';
|
| 608 |
+
document.getElementById('copyBtn').textContent = 'Copy Response';
|
| 609 |
+
document.getElementById('copyBtn').className = 'copy-counter';
|
| 610 |
+
} else { counterBlock.style.display = 'none'; }
|
| 611 |
+
document.getElementById('reportSection').style.display = data.type === 'antisemitic' ? 'block' : 'none';
|
| 612 |
+
box.style.display = 'block';
|
| 613 |
+
}
|
| 614 |
+
|
| 615 |
+
function copyCounter() {
|
| 616 |
+
navigator.clipboard.writeText(currentCounter).then(() => {
|
| 617 |
+
const btn = document.getElementById('copyBtn');
|
| 618 |
+
btn.textContent = 'Copied!';
|
| 619 |
+
btn.className = 'copy-counter copied';
|
| 620 |
+
setTimeout(() => { btn.textContent = 'Copy Response'; btn.className = 'copy-counter'; }, 2000);
|
| 621 |
+
});
|
| 622 |
+
}
|
| 623 |
+
|
| 624 |
+
function shareApp() {
|
| 625 |
+
const url = window.location.href;
|
| 626 |
+
if (navigator.share) {
|
| 627 |
+
navigator.share({ title: 'Shield — Antisemitism Detector', text: 'Use this free AI tool to detect antisemitic content online. Built to protect Jewish communities.', url: url });
|
| 628 |
+
} else {
|
| 629 |
+
navigator.clipboard.writeText(url).then(() => {
|
| 630 |
+
const toast = document.getElementById('shareToast');
|
| 631 |
+
toast.classList.add('show');
|
| 632 |
+
setTimeout(() => toast.classList.remove('show'), 3000);
|
| 633 |
+
});
|
| 634 |
+
}
|
| 635 |
+
}
|
| 636 |
+
|
| 637 |
+
function addToHistory(preview, data) {
|
| 638 |
+
const now = new Date();
|
| 639 |
+
const time = now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
| 640 |
+
sessionHistory.unshift({ preview: preview.substring(0, 60) + (preview.length > 60 ? '...' : ''), type: data.type, title: data.title, time });
|
| 641 |
+
if (sessionHistory.length > 30) sessionHistory.pop();
|
| 642 |
+
sessionStats.total++;
|
| 643 |
+
if (data.type === 'antisemitic') sessionStats.antisemitic++;
|
| 644 |
+
else if (data.type === 'hate') sessionStats.hate++;
|
| 645 |
+
else sessionStats.clean++;
|
| 646 |
+
updateStats();
|
| 647 |
+
renderHistory();
|
| 648 |
+
}
|
| 649 |
+
|
| 650 |
+
function renderHistory() {
|
| 651 |
+
const list = document.getElementById('historyList');
|
| 652 |
+
if (sessionHistory.length === 0) {
|
| 653 |
+
list.innerHTML = '<div class="history-empty">No analyses yet this session. Start by analyzing some text above.</div>';
|
| 654 |
+
return;
|
| 655 |
+
}
|
| 656 |
+
list.innerHTML = sessionHistory.map(h => `
|
| 657 |
+
<div class="history-item">
|
| 658 |
+
<span class="history-badge ${h.type}">${h.title}</span>
|
| 659 |
+
<span class="history-text">${h.preview}</span>
|
| 660 |
+
<span class="history-time">${h.time}</span>
|
| 661 |
+
</div>
|
| 662 |
+
`).join('');
|
| 663 |
+
}
|
| 664 |
+
|
| 665 |
+
function clearHistory() {
|
| 666 |
+
sessionHistory = [];
|
| 667 |
+
sessionStats = { total: 0, antisemitic: 0, hate: 0, clean: 0 };
|
| 668 |
+
updateStats();
|
| 669 |
+
renderHistory();
|
| 670 |
+
}
|
| 671 |
+
|
| 672 |
+
async function analyzeText() {
|
| 673 |
+
const text = document.getElementById('inputText').value.trim();
|
| 674 |
+
if (!text) { alert('Please enter some text.'); return; }
|
| 675 |
+
document.getElementById('textBtn').disabled = true;
|
| 676 |
+
document.getElementById('loading').style.display = 'block';
|
| 677 |
+
document.getElementById('loadingMsg').textContent = 'Analyzing with AI...';
|
| 678 |
+
document.getElementById('resultBox').style.display = 'none';
|
| 679 |
+
try {
|
| 680 |
+
const resp = await fetch('/analyze', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({text}) });
|
| 681 |
+
const data = await resp.json();
|
| 682 |
+
showResult(data);
|
| 683 |
+
addToHistory(text, data);
|
| 684 |
+
} catch(e) { alert('Error. Please try again.'); }
|
| 685 |
+
document.getElementById('loading').style.display = 'none';
|
| 686 |
+
document.getElementById('textBtn').disabled = false;
|
| 687 |
+
}
|
| 688 |
+
|
| 689 |
+
async function analyzeUrl() {
|
| 690 |
+
const url = document.getElementById('inputUrl').value.trim();
|
| 691 |
+
if (!url) { alert('Please enter a URL.'); return; }
|
| 692 |
+
document.getElementById('urlBtn').disabled = true;
|
| 693 |
+
document.getElementById('loading').style.display = 'block';
|
| 694 |
+
document.getElementById('loadingMsg').textContent = 'Fetching and analyzing URL...';
|
| 695 |
+
document.getElementById('resultBox').style.display = 'none';
|
| 696 |
+
try {
|
| 697 |
+
const resp = await fetch('/analyze-url', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({url}) });
|
| 698 |
+
const data = await resp.json();
|
| 699 |
+
if (data.error) { alert(data.error); }
|
| 700 |
+
else {
|
| 701 |
+
document.getElementById('scrapedPreview').textContent = 'Fetched: ' + data.scraped_text;
|
| 702 |
+
document.getElementById('scrapedPreview').style.display = 'block';
|
| 703 |
+
showResult(data);
|
| 704 |
+
addToHistory(url, data);
|
| 705 |
+
}
|
| 706 |
+
} catch(e) { alert('Error scanning URL. Please try again.'); }
|
| 707 |
+
document.getElementById('loading').style.display = 'none';
|
| 708 |
+
document.getElementById('urlBtn').disabled = false;
|
| 709 |
+
}
|
| 710 |
+
|
| 711 |
+
document.getElementById('inputText').addEventListener('keydown', function(e) {
|
| 712 |
+
if (e.ctrlKey && e.key === 'Enter') analyzeText();
|
| 713 |
+
});
|
| 714 |
+
</script>
|
| 715 |
+
</body>
|
| 716 |
+
</html>
|
| 717 |
+
"""
|
| 718 |
+
|
| 719 |
+
ADMIN_LOGIN_HTML = """
|
| 720 |
+
<!DOCTYPE html>
|
| 721 |
+
<html lang="en">
|
| 722 |
+
<head>
|
| 723 |
+
<meta charset="UTF-8">
|
| 724 |
+
<title>Admin — Shield</title>
|
| 725 |
+
<link href="https://fonts.googleapis.com/css2?family=DM+Serif+Display&family=DM+Sans:wght@300;400;500&display=swap" rel="stylesheet">
|
| 726 |
+
<style>
|
| 727 |
+
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
|
| 728 |
+
body { font-family: 'DM Sans', sans-serif; background: #f7f4ef; display: flex; align-items: center; justify-content: center; min-height: 100vh; }
|
| 729 |
+
.card { background: white; border: 1px solid #d8d0c4; border-radius: 16px; padding: 48px; width: 360px; box-shadow: 0 4px 24px rgba(0,0,0,0.06); }
|
| 730 |
+
h1 { font-family: 'DM Serif Display', serif; font-size: 28px; color: #1e3a5f; margin-bottom: 8px; }
|
| 731 |
+
p { font-size: 13px; color: #9c9189; margin-bottom: 28px; font-weight: 300; }
|
| 732 |
+
input { width: 100%; background: #f7f4ef; border: 1.5px solid #d8d0c4; border-radius: 8px; color: #1a1510; font-size: 15px; font-family: 'DM Sans', sans-serif; padding: 12px 14px; outline: none; margin-bottom: 14px; }
|
| 733 |
+
input:focus { border-color: #2d5aa0; background: white; }
|
| 734 |
+
button { width: 100%; background: #1e3a5f; border: none; color: white; font-size: 14px; font-weight: 500; font-family: 'DM Sans', sans-serif; padding: 13px; border-radius: 8px; cursor: pointer; }
|
| 735 |
+
button:hover { background: #2d5aa0; }
|
| 736 |
+
.error { color: #8b1a1a; font-size: 13px; margin-bottom: 12px; display: none; }
|
| 737 |
+
</style>
|
| 738 |
+
</head>
|
| 739 |
+
<body>
|
| 740 |
+
<div class="card">
|
| 741 |
+
<h1>Admin Panel</h1>
|
| 742 |
+
<p>Shield — Antisemitism Detector</p>
|
| 743 |
+
<div class="error" id="err">Incorrect password. Try again.</div>
|
| 744 |
+
<input type="password" id="pw" placeholder="Enter admin password" />
|
| 745 |
+
<button onclick="login()">Sign In</button>
|
| 746 |
+
</div>
|
| 747 |
+
<script>
|
| 748 |
+
async function login() {
|
| 749 |
+
const pw = document.getElementById('pw').value;
|
| 750 |
+
const resp = await fetch('/admin/login', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({password: pw}) });
|
| 751 |
+
const data = await resp.json();
|
| 752 |
+
if (data.success) { window.location.href = '/admin/dashboard'; }
|
| 753 |
+
else { document.getElementById('err').style.display = 'block'; }
|
| 754 |
+
}
|
| 755 |
+
document.getElementById('pw').addEventListener('keydown', e => { if (e.key === 'Enter') login(); });
|
| 756 |
+
</script>
|
| 757 |
+
</body>
|
| 758 |
+
</html>
|
| 759 |
+
"""
|
| 760 |
+
|
| 761 |
+
ADMIN_DASHBOARD_HTML = """
|
| 762 |
+
<!DOCTYPE html>
|
| 763 |
+
<html lang="en">
|
| 764 |
+
<head>
|
| 765 |
+
<meta charset="UTF-8">
|
| 766 |
+
<title>Admin Dashboard — Shield</title>
|
| 767 |
+
<link href="https://fonts.googleapis.com/css2?family=DM+Serif+Display&family=DM+Sans:wght@300;400;500;600&display=swap" rel="stylesheet">
|
| 768 |
+
<style>
|
| 769 |
+
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
|
| 770 |
+
body { font-family: 'DM Sans', sans-serif; background: #f7f4ef; color: #1a1510; }
|
| 771 |
+
nav { background: #1e3a5f; padding: 0 40px; height: 60px; display: flex; align-items: center; justify-content: space-between; }
|
| 772 |
+
.nav-title { font-family: 'DM Serif Display', serif; font-size: 18px; color: white; }
|
| 773 |
+
.nav-right { display: flex; gap: 16px; align-items: center; }
|
| 774 |
+
.nav-right a { color: rgba(255,255,255,0.6); font-size: 13px; text-decoration: none; }
|
| 775 |
+
.nav-right a:hover { color: white; }
|
| 776 |
+
.logout { background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.2); color: white !important; padding: 6px 16px; border-radius: 6px; font-size: 12px; }
|
| 777 |
+
.main { max-width: 1100px; margin: 0 auto; padding: 40px 24px; }
|
| 778 |
+
h2 { font-family: 'DM Serif Display', serif; font-size: 32px; color: #1e3a5f; margin-bottom: 8px; }
|
| 779 |
+
.subtitle { font-size: 14px; color: #9c9189; margin-bottom: 32px; font-weight: 300; }
|
| 780 |
+
.stats-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; margin-bottom: 32px; }
|
| 781 |
+
.stat-card { background: white; border: 1px solid #d8d0c4; border-radius: 12px; padding: 20px 24px; }
|
| 782 |
+
.stat-num { font-family: 'DM Serif Display', serif; font-size: 36px; color: #1e3a5f; }
|
| 783 |
+
.stat-num.danger { color: #8b1a1a; }
|
| 784 |
+
.stat-num.warn { color: #7a4a00; }
|
| 785 |
+
.stat-num.ok { color: #1a4a2a; }
|
| 786 |
+
.stat-label { font-size: 12px; color: #9c9189; margin-top: 4px; text-transform: uppercase; letter-spacing: 1px; }
|
| 787 |
+
.table-card { background: white; border: 1px solid #d8d0c4; border-radius: 12px; overflow: hidden; }
|
| 788 |
+
.table-header { padding: 20px 24px; border-bottom: 1px solid #d8d0c4; display: flex; align-items: center; justify-content: space-between; }
|
| 789 |
+
.table-header h3 { font-size: 15px; font-weight: 600; color: #1e3a5f; }
|
| 790 |
+
.table-header span { font-size: 12px; color: #9c9189; }
|
| 791 |
+
table { width: 100%; border-collapse: collapse; }
|
| 792 |
+
th { padding: 12px 20px; text-align: left; font-size: 11px; font-weight: 600; color: #9c9189; text-transform: uppercase; letter-spacing: 1px; border-bottom: 1px solid #d8d0c4; background: #f7f4ef; }
|
| 793 |
+
td { padding: 14px 20px; font-size: 13px; border-bottom: 1px solid #eee9e0; vertical-align: top; }
|
| 794 |
+
tr:last-child td { border-bottom: none; }
|
| 795 |
+
tr:hover td { background: #f7f4ef; }
|
| 796 |
+
.badge { display: inline-block; font-size: 11px; font-weight: 600; padding: 3px 10px; border-radius: 20px; }
|
| 797 |
+
.badge.antisemitic { background: #fdf0f0; color: #8b1a1a; border: 1px solid #e8b4b4; }
|
| 798 |
+
.badge.hate { background: #fdf6e8; color: #7a4a00; border: 1px solid #e8d4a0; }
|
| 799 |
+
.badge.clean { background: #f0faf3; color: #1a4a2a; border: 1px solid #a8d4b4; }
|
| 800 |
+
.preview-text { max-width: 320px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: #6b6358; font-weight: 300; }
|
| 801 |
+
.flags-cell { font-size: 12px; color: #8b1a1a; font-weight: 300; }
|
| 802 |
+
.empty { text-align: center; padding: 48px; color: #9c9189; font-size: 14px; }
|
| 803 |
+
</style>
|
| 804 |
+
</head>
|
| 805 |
+
<body>
|
| 806 |
+
<nav>
|
| 807 |
+
<span class="nav-title">Shield — Admin</span>
|
| 808 |
+
<div class="nav-right">
|
| 809 |
+
<a href="/">View Site</a>
|
| 810 |
+
<a href="/admin/logout" class="logout">Sign Out</a>
|
| 811 |
+
</div>
|
| 812 |
+
</nav>
|
| 813 |
+
<div class="main">
|
| 814 |
+
<h2>Dashboard</h2>
|
| 815 |
+
<p class="subtitle">Complete history of all analyses ever performed on Shield.</p>
|
| 816 |
+
<div class="stats-grid">
|
| 817 |
+
<div class="stat-card"><div class="stat-num">{{ stats.total }}</div><div class="stat-label">Total Analyzed</div></div>
|
| 818 |
+
<div class="stat-card"><div class="stat-num danger">{{ stats.antisemitic }}</div><div class="stat-label">Antisemitic</div></div>
|
| 819 |
+
<div class="stat-card"><div class="stat-num warn">{{ stats.hate }}</div><div class="stat-label">General Hate</div></div>
|
| 820 |
+
<div class="stat-card"><div class="stat-num ok">{{ stats.clean }}</div><div class="stat-label">Clean</div></div>
|
| 821 |
+
</div>
|
| 822 |
+
<div class="table-card">
|
| 823 |
+
<div class="table-header">
|
| 824 |
+
<h3>Full Analysis History</h3>
|
| 825 |
+
<span>{{ stats.total }} total records</span>
|
| 826 |
+
</div>
|
| 827 |
+
{% if analyses %}
|
| 828 |
+
<table>
|
| 829 |
+
<thead>
|
| 830 |
+
<tr><th>#</th><th>Timestamp</th><th>Type</th><th>Result</th><th>Confidence</th><th>Input Preview</th><th>Flagged Phrases</th></tr>
|
| 831 |
+
</thead>
|
| 832 |
+
<tbody>
|
| 833 |
+
{% for row in analyses %}
|
| 834 |
+
<tr>
|
| 835 |
+
<td style="color:#9c9189">{{ row[0] }}</td>
|
| 836 |
+
<td style="white-space:nowrap;color:#6b6358">{{ row[1] }}</td>
|
| 837 |
+
<td style="font-size:12px;color:#6b6358">{{ row[2] }}</td>
|
| 838 |
+
<td><span class="badge {{ row[4] }}">{{ row[4] }}</span></td>
|
| 839 |
+
<td>{{ row[5] }}%</td>
|
| 840 |
+
<td><div class="preview-text">{{ row[3] }}</div></td>
|
| 841 |
+
<td><div class="flags-cell">{{ row[6] if row[6] else '—' }}</div></td>
|
| 842 |
+
</tr>
|
| 843 |
+
{% endfor %}
|
| 844 |
+
</tbody>
|
| 845 |
+
</table>
|
| 846 |
+
{% else %}
|
| 847 |
+
<div class="empty">No analyses yet.</div>
|
| 848 |
+
{% endif %}
|
| 849 |
+
</div>
|
| 850 |
+
</div>
|
| 851 |
+
</body>
|
| 852 |
+
</html>
|
| 853 |
+
"""
|
| 854 |
+
|
| 855 |
+
@app.route('/')
|
| 856 |
+
def home():
|
| 857 |
+
return render_template_string(MAIN_HTML)
|
| 858 |
+
|
| 859 |
+
@app.route('/analyze', methods=['POST'])
|
| 860 |
+
def analyze():
|
| 861 |
+
data = request.get_json()
|
| 862 |
+
text = data.get('text', '').strip()
|
| 863 |
+
if not text:
|
| 864 |
+
return jsonify({'error': 'No text provided'})
|
| 865 |
+
result = classify_text(text)
|
| 866 |
+
save_analysis('text', text, result['type'], result['confidence'], result['flags'])
|
| 867 |
+
return jsonify(result)
|
| 868 |
+
|
| 869 |
+
@app.route('/analyze-url', methods=['POST'])
|
| 870 |
+
def analyze_url():
|
| 871 |
+
data = request.get_json()
|
| 872 |
+
url = data.get('url', '').strip()
|
| 873 |
+
if not url:
|
| 874 |
+
return jsonify({'error': 'No URL provided'})
|
| 875 |
+
try:
|
| 876 |
+
req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'})
|
| 877 |
+
with urllib.request.urlopen(req, timeout=10) as response:
|
| 878 |
+
html = response.read().decode('utf-8', errors='ignore')
|
| 879 |
+
clean = re.sub(r'<[^>]+>', ' ', html)
|
| 880 |
+
clean = re.sub(r'\s+', ' ', clean).strip()
|
| 881 |
+
clean = clean[:2000]
|
| 882 |
+
result = classify_text(clean)
|
| 883 |
+
result['scraped_text'] = clean[:200] + '...'
|
| 884 |
+
save_analysis('url', url, result['type'], result['confidence'], result['flags'])
|
| 885 |
+
return jsonify(result)
|
| 886 |
+
except Exception as e:
|
| 887 |
+
return jsonify({'error': f'Could not scan URL: {str(e)}'})
|
| 888 |
+
|
| 889 |
+
@app.route('/admin')
|
| 890 |
+
def admin():
|
| 891 |
+
return render_template_string(ADMIN_LOGIN_HTML)
|
| 892 |
+
|
| 893 |
+
@app.route('/admin/login', methods=['POST'])
|
| 894 |
+
def admin_login():
|
| 895 |
+
data = request.get_json()
|
| 896 |
+
if data.get('password') == ADMIN_PASSWORD:
|
| 897 |
+
session['admin'] = True
|
| 898 |
+
return jsonify({'success': True})
|
| 899 |
+
return jsonify({'success': False})
|
| 900 |
+
|
| 901 |
+
@app.route('/admin/dashboard')
|
| 902 |
+
def admin_dashboard():
|
| 903 |
+
if not session.get('admin'):
|
| 904 |
+
return redirect('/admin')
|
| 905 |
+
stats = get_stats()
|
| 906 |
+
analyses = get_all_analyses()
|
| 907 |
+
return render_template_string(ADMIN_DASHBOARD_HTML, stats=stats, analyses=analyses)
|
| 908 |
+
|
| 909 |
+
@app.route('/admin/logout')
|
| 910 |
+
def admin_logout():
|
| 911 |
+
session.pop('admin', None)
|
| 912 |
+
return redirect('/admin')
|
| 913 |
+
|
| 914 |
+
if __name__ == '__main__':
|
| 915 |
+
init_db()
|
| 916 |
+
app.run(host='0.0.0.0', port=7860)
|
requirements.txt
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
flask
|
| 2 |
+
transformers
|
| 3 |
+
torch
|
| 4 |
+
requests
|
| 5 |
+
beautifulsoup4
|