| """ |
| app.py — Cognitive Distortion Classifier / emphasisit (FastAPI, HuggingFace Spaces) |
| Neon green redesign — consistent with Coalitus platform theme. |
| """ |
|
|
| import json |
| from pathlib import Path |
|
|
| import torch |
| import uvicorn |
| from fastapi import FastAPI, HTTPException |
| from fastapi.middleware.cors import CORSMiddleware |
| from fastapi.responses import HTMLResponse |
| from pydantic import BaseModel, Field |
| from transformers import AutoModelForSequenceClassification, AutoTokenizer |
|
|
| |
|
|
| BASE = Path(__file__).parent |
|
|
| with open(BASE / "label_config.json") as f: |
| label_cfg = json.load(f) |
|
|
| ID2LABEL: dict[int, str] = {int(k): v for k, v in label_cfg["id2label"].items()} |
| NUM_LABELS: int = len(ID2LABEL) |
|
|
| tokenizer = AutoTokenizer.from_pretrained(str(BASE)) |
| model = AutoModelForSequenceClassification.from_pretrained(str(BASE)) |
| model.eval() |
|
|
| |
|
|
| EMOJI: dict[str, str] = { |
| "overgeneralization": "🔁", |
| "catastrophizing": "💥", |
| "black_and_white": "⚫⚪", |
| "self_blame": "👤", |
| "mind_reading": "🔮", |
| } |
|
|
| DESCRIPTIONS: dict[str, str] = { |
| "overgeneralization": "Drawing sweeping conclusions from a single event ('I always fail', 'Nobody ever helps me').", |
| "catastrophizing": "Expecting the worst possible outcome ('This is a disaster', 'Everything is ruined').", |
| "black_and_white": "Seeing situations as all-or-nothing with no middle ground.", |
| "self_blame": "Taking excessive personal responsibility for things outside your control.", |
| "mind_reading": "Assuming you know what others think without direct evidence.", |
| } |
|
|
| |
|
|
| class PredictRequest(BaseModel): |
| text: str |
| threshold: float = Field(default=0.005, ge=0.001, le=0.02) |
|
|
| model_config = {"json_schema_extra": { |
| "example": { |
| "text": "I always mess everything up. I can't do anything right.", |
| "threshold": 0.005, |
| } |
| }} |
|
|
|
|
| class DistortionScore(BaseModel): |
| label: str |
| pretty: str |
| emoji: str |
| description: str |
| score: float |
| detected: bool |
|
|
|
|
| class PredictResponse(BaseModel): |
| any_detected: bool |
| threshold: float |
| detected: list[DistortionScore] |
| all_scores: list[DistortionScore] |
|
|
|
|
| |
|
|
| def run_inference(text: str, threshold: float) -> PredictResponse: |
| inputs = tokenizer( |
| text, return_tensors="pt", |
| truncation=True, max_length=128, padding=True, |
| ) |
| with torch.no_grad(): |
| probs = torch.sigmoid(model(**inputs).logits).squeeze() |
|
|
| all_scores = [ |
| DistortionScore( |
| label = ID2LABEL[i], |
| pretty = ID2LABEL[i].replace("_", " ").title(), |
| emoji = EMOJI[ID2LABEL[i]], |
| description = DESCRIPTIONS[ID2LABEL[i]], |
| score = round(float(p), 6), |
| detected = float(p) >= threshold, |
| ) |
| for i, p in enumerate(probs) |
| ] |
| all_scores.sort(key=lambda x: x.score, reverse=True) |
| detected = [s for s in all_scores if s.detected] |
|
|
| return PredictResponse( |
| any_detected = len(detected) > 0, |
| threshold = threshold, |
| detected = detected, |
| all_scores = all_scores, |
| ) |
|
|
|
|
| |
|
|
| app = FastAPI( |
| title = "Cognitive Distortion Classifier API", |
| description = "Detects 5 CBT cognitive distortions in free-text using sigmoid multi-label classification.", |
| version = "1.0.0", |
| ) |
|
|
| app.add_middleware( |
| CORSMiddleware, |
| allow_origins = ["*"], |
| allow_methods = ["GET", "POST"], |
| allow_headers = ["*"], |
| ) |
|
|
|
|
| @app.post("/predict", response_model=PredictResponse, summary="Detect cognitive distortions") |
| def predict(req: PredictRequest) -> PredictResponse: |
| if not req.text.strip(): |
| raise HTTPException(status_code=422, detail="text must not be empty") |
| try: |
| return run_inference(req.text, req.threshold) |
| except Exception as exc: |
| raise HTTPException(status_code=500, detail=str(exc)) from exc |
|
|
|
|
| @app.get("/labels", summary="All supported distortion labels with metadata") |
| def list_labels() -> dict: |
| return { |
| label: { |
| "pretty": label.replace("_", " ").title(), |
| "emoji": EMOJI[label], |
| "description": DESCRIPTIONS[label], |
| } |
| for label in ID2LABEL.values() |
| } |
|
|
|
|
| @app.get("/health") |
| def health() -> dict: |
| return {"status": "ok"} |
|
|
|
|
| |
|
|
| FRONTEND_HTML = """<!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"/> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"/> |
| <title>Cognitive Distortion Detector</title> |
| <link rel="preconnect" href="https://fonts.googleapis.com"/> |
| <link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;600;700;900&family=Share+Tech+Mono&family=DM+Sans:wght@300;400;500&display=swap" rel="stylesheet"/> |
| <style> |
| :root { |
| --void: #010608; |
| --deep: #030d0a; |
| --panel: #050f0b; |
| --card: #071410; |
| --card-alt: #091a14; |
| --border: #0d2318; |
| --green: #39ff8e; |
| --green-mid: #00e66e; |
| --green-dim: #39ff8e55; |
| --green-glow: #39ff8e22; |
| --green-bg: #39ff8e0d; |
| --amber: #ffb830; |
| --amber-dim: #ffb83044; |
| --amber-bg: #ffb8300d; |
| --text-hi: #e0ffe8; |
| --text-mid: #5a9e72; |
| --text-dim: #1e4030; |
| } |
| |
| *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } |
| |
| body { |
| font-family: 'DM Sans', sans-serif; |
| background: var(--void); |
| color: var(--text-hi); |
| min-height: 100vh; |
| padding: 2rem 1rem 5rem; |
| overflow-x: hidden; |
| background-image: |
| radial-gradient(ellipse 55% 35% at 10% 5%, #39ff8e09 0%, transparent 60%), |
| radial-gradient(ellipse 45% 35% at 90% 90%, #39ff8e07 0%, transparent 55%), |
| radial-gradient(#0d2318 1px, transparent 1px); |
| background-size: 100% 100%, 100% 100%, 24px 24px; |
| } |
| |
| body::before { |
| content: ''; |
| position: fixed; inset: 0; pointer-events: none; z-index: 9999; |
| background: repeating-linear-gradient(0deg, transparent, transparent 2px, #00000012 2px, #00000012 4px); |
| } |
| |
| .container { max-width: 920px; margin: 0 auto; position: relative; } |
| |
| /* ── Header ── */ |
| .header { |
| margin-bottom: 2.5rem; |
| padding-bottom: 1.5rem; |
| border-bottom: 1px solid var(--border); |
| position: relative; |
| } |
| .header::after { |
| content: ''; |
| position: absolute; bottom: -1px; left: 0; |
| width: 200px; height: 1px; |
| background: var(--green); |
| box-shadow: 0 0 14px var(--green), 0 0 40px var(--green-dim); |
| } |
| .eyebrow { |
| font-family: 'Share Tech Mono', monospace; |
| font-size: 0.68rem; |
| letter-spacing: 0.25em; |
| color: var(--green); |
| text-shadow: 0 0 8px var(--green); |
| margin-bottom: 0.7rem; |
| text-transform: uppercase; |
| } |
| .header h1 { |
| font-family: 'Orbitron', monospace; |
| font-size: clamp(1.2rem, 2.5vw, 1.75rem); |
| font-weight: 900; |
| letter-spacing: 0.06em; |
| color: var(--text-hi); |
| text-shadow: 0 0 24px var(--green-dim); |
| margin-bottom: 0.5rem; |
| } |
| .header h1 em { |
| font-style: normal; |
| color: var(--green); |
| text-shadow: 0 0 16px var(--green), 0 0 50px var(--green-dim); |
| } |
| .header p { color: var(--text-mid); font-size: 0.85rem; line-height: 1.75; max-width: 600px; font-weight: 300; } |
| |
| .notice { |
| margin-top: 1rem; |
| padding: 0.75rem 1rem; |
| border-left: 3px solid var(--green-dim); |
| background: var(--green-bg); |
| font-size: 0.75rem; |
| color: var(--text-mid); |
| line-height: 1.7; |
| clip-path: polygon(0 0, 100% 0, 100% calc(100% - 8px), calc(100% - 8px) 100%, 0 100%); |
| } |
| .notice strong { color: var(--text-hi); } |
| code { |
| color: var(--green); |
| background: var(--green-bg); |
| border-radius: 2px; |
| padding: 1px 5px; |
| font-family: 'Share Tech Mono', monospace; |
| font-size: 0.7rem; |
| } |
| |
| /* ── Two-column input layout ── */ |
| .input-row { |
| display: grid; |
| grid-template-columns: 3fr 2fr; |
| gap: 1rem; |
| margin-bottom: 1rem; |
| align-items: start; |
| } |
| @media (max-width: 680px) { .input-row { grid-template-columns: 1fr; } } |
| |
| .card { |
| background: var(--card); |
| border: 1px solid var(--border); |
| border-radius: 3px; |
| padding: 1.25rem; |
| clip-path: polygon(0 0, calc(100% - 16px) 0, 100% 16px, 100% 100%, 16px 100%, 0 calc(100% - 16px)); |
| transition: border-color 0.3s; |
| } |
| .card:focus-within { border-color: var(--green-dim); } |
| |
| .field-label { |
| font-family: 'Orbitron', monospace; |
| font-size: 0.58rem; |
| font-weight: 700; |
| letter-spacing: 0.2em; |
| text-transform: uppercase; |
| color: var(--green); |
| text-shadow: 0 0 6px var(--green-dim); |
| margin-bottom: 0.65rem; |
| display: flex; |
| align-items: center; |
| gap: 0.5rem; |
| } |
| .field-label::after { content: ''; flex: 1; height: 1px; background: linear-gradient(90deg, var(--border), transparent); } |
| |
| textarea { |
| width: 100%; |
| background: var(--card-alt); |
| border: 1px solid var(--border); |
| border-radius: 2px; |
| color: var(--text-hi); |
| font-family: 'Share Tech Mono', monospace; |
| font-size: 0.82rem; |
| line-height: 1.7; |
| padding: 0.9rem 1rem; |
| resize: vertical; |
| min-height: 140px; |
| outline: none; |
| caret-color: var(--green); |
| transition: border-color 0.2s, box-shadow 0.2s; |
| clip-path: polygon(0 0, calc(100% - 10px) 0, 100% 10px, 100% 100%, 10px 100%, 0 calc(100% - 10px)); |
| } |
| textarea::placeholder { color: var(--text-dim); } |
| textarea:focus { |
| border-color: var(--green-mid); |
| box-shadow: 0 0 0 1px var(--green-dim), 0 0 20px var(--green-glow); |
| } |
| |
| /* ── Threshold card ── */ |
| .threshold-card { |
| background: var(--card); |
| border: 1px solid var(--border); |
| border-radius: 3px; |
| padding: 1.25rem; |
| display: flex; |
| flex-direction: column; |
| gap: 0.9rem; |
| clip-path: polygon(0 0, calc(100% - 16px) 0, 100% 16px, 100% 100%, 16px 100%, 0 calc(100% - 16px)); |
| } |
| |
| .slider-val-row { display: flex; justify-content: space-between; align-items: baseline; } |
| .slider-val { |
| font-family: 'Orbitron', monospace; |
| font-size: 1.4rem; |
| font-weight: 900; |
| color: var(--green); |
| text-shadow: 0 0 16px var(--green), 0 0 40px var(--green-dim); |
| } |
| .slider-sub { |
| font-family: 'Share Tech Mono', monospace; |
| font-size: 0.62rem; |
| color: var(--text-dim); |
| letter-spacing: 0.05em; |
| } |
| .slider-hint { font-size: 0.73rem; color: var(--text-mid); line-height: 1.6; font-weight: 300; } |
| |
| /* Slider track + thumb */ |
| .slider-wrap { position: relative; height: 6px; } |
| .slider-track-bg { |
| position: absolute; inset: 0; |
| background: var(--border); |
| border-radius: 2px; |
| } |
| .slider-track-fill { |
| position: absolute; top: 0; left: 0; height: 100%; |
| background: var(--green); |
| border-radius: 2px; |
| box-shadow: 0 0 8px var(--green), 0 0 20px var(--green-dim); |
| transition: width 0.05s; |
| pointer-events: none; |
| } |
| input[type="range"] { |
| -webkit-appearance: none; appearance: none; |
| position: absolute; inset: 0; |
| width: 100%; height: 100%; |
| background: transparent; outline: none; cursor: pointer; |
| margin: 0; z-index: 1; |
| } |
| input[type="range"]::-webkit-slider-thumb { |
| -webkit-appearance: none; |
| width: 14px; height: 14px; |
| border-radius: 50%; |
| background: var(--void); |
| border: 2px solid var(--green); |
| box-shadow: 0 0 8px var(--green), 0 0 22px var(--green-dim), inset 0 0 4px var(--green-dim); |
| cursor: pointer; |
| transition: transform 0.15s, box-shadow 0.15s; |
| } |
| input[type="range"]::-webkit-slider-thumb:hover { |
| transform: scale(1.3); |
| box-shadow: 0 0 14px var(--green), 0 0 40px var(--green-dim); |
| } |
| input[type="range"]::-moz-range-thumb { |
| width: 14px; height: 14px; border-radius: 50%; |
| background: var(--void); border: 2px solid var(--green); |
| box-shadow: 0 0 8px var(--green); cursor: pointer; |
| } |
| |
| /* ── Run button ── */ |
| .btn-run { |
| width: 100%; |
| padding: 0.85rem 1.5rem; |
| font-family: 'Orbitron', monospace; |
| font-size: 0.75rem; |
| font-weight: 700; |
| letter-spacing: 0.18em; |
| text-transform: uppercase; |
| color: var(--void); |
| background: var(--green); |
| border: none; |
| border-radius: 2px; |
| cursor: pointer; |
| clip-path: polygon(0 0, calc(100% - 12px) 0, 100% 12px, 100% 100%, 12px 100%, 0 calc(100% - 12px)); |
| box-shadow: 0 0 20px var(--green-dim), 0 0 60px var(--green-glow); |
| transition: box-shadow 0.2s, opacity 0.2s, transform 0.1s; |
| position: relative; |
| overflow: hidden; |
| } |
| .btn-run::after { |
| content: ''; |
| position: absolute; inset: 0; |
| background: linear-gradient(90deg, transparent, #ffffff25, transparent); |
| transform: translateX(-100%); |
| transition: transform 0.5s; |
| } |
| .btn-run:hover::after { transform: translateX(100%); } |
| .btn-run:hover { box-shadow: 0 0 30px var(--green), 0 0 80px var(--green-dim); } |
| .btn-run:active { transform: scale(0.99); } |
| .btn-run:disabled { opacity: 0.35; cursor: not-allowed; box-shadow: none; } |
| |
| /* ── Results ── */ |
| .results-row { |
| display: grid; |
| grid-template-columns: 1fr 1fr; |
| gap: 1rem; |
| margin-bottom: 1rem; |
| } |
| @media (max-width: 680px) { .results-row { grid-template-columns: 1fr; } } |
| |
| .result-panel { |
| background: var(--card); |
| border: 1px solid var(--border); |
| border-radius: 3px; |
| padding: 1.1rem 1.25rem; |
| display: none; |
| clip-path: polygon(0 0, calc(100% - 14px) 0, 100% 14px, 100% 100%, 14px 100%, 0 calc(100% - 14px)); |
| } |
| .result-panel.visible { display: block; } |
| |
| .panel-title { |
| font-family: 'Orbitron', monospace; |
| font-size: 0.58rem; |
| font-weight: 700; |
| letter-spacing: 0.2em; |
| text-transform: uppercase; |
| color: var(--green); |
| text-shadow: 0 0 6px var(--green-dim); |
| margin-bottom: 1rem; |
| display: flex; |
| align-items: center; |
| gap: 0.5rem; |
| } |
| .panel-title::after { content: ''; flex: 1; height: 1px; background: linear-gradient(90deg, var(--border), transparent); } |
| |
| /* Detected distortion card */ |
| .distortion-item { |
| background: var(--green-bg); |
| border: 1px solid var(--green-dim); |
| border-left: 3px solid var(--green); |
| border-radius: 2px; |
| padding: 0.85rem 1rem; |
| margin-bottom: 0.6rem; |
| clip-path: polygon(0 0, calc(100% - 8px) 0, 100% 8px, 100% 100%, 8px 100%, 0 calc(100% - 8px)); |
| box-shadow: 0 0 12px var(--green-glow); |
| } |
| .distortion-item:last-child { margin-bottom: 0; } |
| .d-name { |
| font-family: 'Orbitron', monospace; |
| font-size: 0.75rem; |
| font-weight: 700; |
| color: var(--green); |
| text-shadow: 0 0 8px var(--green-dim); |
| letter-spacing: 0.05em; |
| margin-bottom: 0.3rem; |
| } |
| .d-desc { |
| font-size: 0.74rem; |
| color: var(--text-mid); |
| line-height: 1.6; |
| font-weight: 300; |
| margin-bottom: 0.3rem; |
| } |
| .d-score { |
| font-family: 'Share Tech Mono', monospace; |
| font-size: 0.7rem; |
| color: var(--green); |
| opacity: 0.65; |
| } |
| |
| /* Clean result */ |
| .clean-box { |
| background: var(--green-bg); |
| border: 1px solid var(--green-dim); |
| border-left: 3px solid var(--green); |
| border-radius: 2px; |
| padding: 1rem 1.1rem; |
| box-shadow: 0 0 16px var(--green-glow); |
| clip-path: polygon(0 0, calc(100% - 10px) 0, 100% 10px, 100% 100%, 10px 100%, 0 calc(100% - 10px)); |
| } |
| .clean-box h3 { |
| font-family: 'Orbitron', monospace; |
| font-size: 0.78rem; |
| font-weight: 700; |
| color: var(--green); |
| text-shadow: 0 0 8px var(--green-dim); |
| margin-bottom: 0.4rem; |
| letter-spacing: 0.05em; |
| } |
| .clean-box p { font-size: 0.78rem; color: var(--text-mid); line-height: 1.65; font-weight: 300; } |
| |
| /* Score bars */ |
| .prob-row { margin-bottom: 0.7rem; } |
| .prob-row:last-child { margin-bottom: 0; } |
| .prob-header { |
| display: flex; justify-content: space-between; |
| font-size: 0.74rem; color: var(--text-mid); margin-bottom: 0.3rem; |
| font-family: 'Share Tech Mono', monospace; |
| } |
| .prob-score { color: var(--text-hi); font-weight: 700; } |
| .prob-bar-bg { background: var(--border); border-radius: 1px; height: 4px; overflow: hidden; } |
| .prob-bar-fill { |
| height: 100%; border-radius: 1px; background: var(--green); |
| box-shadow: 0 0 6px var(--green-dim); |
| transition: width 0.55s cubic-bezier(0.4,0,0.2,1); |
| } |
| |
| /* Examples */ |
| .section { margin-top: 2rem; } |
| .section-hdr { |
| font-family: 'Orbitron', monospace; |
| font-size: 0.58rem; font-weight: 700; letter-spacing: 0.2em; |
| text-transform: uppercase; color: var(--green); |
| text-shadow: 0 0 6px var(--green-dim); margin-bottom: 0.75rem; |
| } |
| .chip { |
| display: inline-block; margin: 0 0.35rem 0.45rem 0; |
| padding: 0.35rem 0.8rem; |
| font-family: 'Share Tech Mono', monospace; font-size: 0.69rem; |
| color: var(--text-mid); background: var(--card); border: 1px solid var(--border); |
| border-radius: 1px; cursor: pointer; transition: all 0.15s; |
| clip-path: polygon(0 0, calc(100% - 6px) 0, 100% 6px, 100% 100%, 6px 100%, 0 calc(100% - 6px)); |
| } |
| .chip:hover { color: var(--green); border-color: var(--green-dim); background: var(--green-bg); box-shadow: 0 0 10px var(--green-glow); } |
| |
| hr { border: none; border-top: 1px solid var(--border); margin: 2rem 0; } |
| .ref-table { width: 100%; border-collapse: collapse; font-size: 0.75rem; } |
| .ref-table th { |
| font-family: 'Share Tech Mono', monospace; color: var(--green); |
| text-align: left; padding: 7px 12px; border-bottom: 1px solid var(--green-dim); letter-spacing: 0.05em; |
| } |
| .ref-table td { color: var(--text-mid); padding: 7px 12px; border-bottom: 1px solid var(--border); font-weight: 300; } |
| .ref-table td em { color: var(--green); font-style: normal; } |
| .ref-table tr:hover td { color: var(--green); background: var(--green-bg); } |
| |
| ::-webkit-scrollbar { width: 5px; } |
| ::-webkit-scrollbar-track { background: var(--void); } |
| ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; } |
| ::-webkit-scrollbar-thumb:hover { background: var(--green-dim); } |
| </style> |
| </head> |
| <body> |
| <div class="container"> |
| |
| <div class="header"> |
| <div class="eyebrow">// coalitus · cbt analysis · v1.0</div> |
| <h1>Cognitive Distortion <em>Detector</em></h1> |
| <p>Analyses text for five CBT cognitive distortions: overgeneralization, catastrophizing, black-and-white thinking, self-blame, and mind-reading.</p> |
| <div class="notice"> |
| <strong>⚕️ Educational tool only</strong> — not a substitute for professional mental health support. |
| Raw scores are typically <code>0.002–0.009</code> (conservative classifier head). |
| Results reflect <em>relative</em> confidence rather than absolute certainty. |
| </div> |
| </div> |
| |
| <div class="input-row"> |
| <div class="card"> |
| <div class="field-label">Enter your text</div> |
| <textarea id="text-input" placeholder="e.g. I always ruin everything. Nobody will ever trust me again."></textarea> |
| </div> |
| <div class="threshold-card"> |
| <div class="field-label">Detection Threshold</div> |
| <div class="slider-val-row"> |
| <span class="slider-val" id="threshold-val">0.005</span> |
| <span class="slider-sub">sensitivity</span> |
| </div> |
| <div class="slider-wrap"> |
| <div class="slider-track-bg"></div> |
| <div class="slider-track-fill" id="slider-fill" style="width:20%"></div> |
| <input type="range" id="threshold-slider" min="0.001" max="0.020" step="0.001" value="0.005" |
| oninput="updateThreshold(this.value)"/> |
| </div> |
| <div class="slider-hint">Lower = more sensitive.<br/>Typical scores: 0.002–0.009</div> |
| <button class="btn-run" id="run-btn" onclick="runAnalyse()">⚡ Analyse Text</button> |
| </div> |
| </div> |
| |
| <div class="results-row"> |
| <div class="result-panel" id="detected-panel"> |
| <div class="panel-title" id="detected-title">Detected Distortions</div> |
| <div id="detected-list"></div> |
| </div> |
| <div class="result-panel" id="scores-panel"> |
| <div class="panel-title">Raw Scores</div> |
| <div id="scores-list"></div> |
| </div> |
| </div> |
| |
| <div class="section"> |
| <div class="section-hdr">// try an example</div> |
| <div id="chips"></div> |
| </div> |
| |
| <hr/> |
| <div class="section-hdr">// distortion reference</div> |
| <table class="ref-table"> |
| <thead><tr><th>Distortion</th><th>What It Looks Like</th></tr></thead> |
| <tbody> |
| <tr><td>🔁 Overgeneralization</td><td>"I <em>always</em> fail", "Nobody <em>ever</em> helps me"</td></tr> |
| <tr><td>💥 Catastrophizing</td> <td>"This is a <em>disaster</em>", "Everything is <em>ruined</em>"</td></tr> |
| <tr><td>⚫⚪ Black & White</td><td>"Either perfect or a complete failure"</td></tr> |
| <tr><td>👤 Self-Blame</td> <td>"It's <em>all my fault</em>" even when it isn't</td></tr> |
| <tr><td>🔮 Mind Reading</td> <td>"They <em>must</em> think I'm stupid"</td></tr> |
| </tbody> |
| </table> |
| |
| </div> |
| <script> |
| const EXAMPLES = [ |
| "I always mess everything up. I can't do anything right.", |
| "This is going to be a complete disaster. Everything will fall apart.", |
| "Either I succeed perfectly or I'm a total failure — there's no in between.", |
| "It's all my fault the team didn't hit the target.", |
| "They didn't reply — they must be angry at me.", |
| "It was a hard week but I handled most of it pretty well.", |
| ]; |
| |
| function updateThreshold(val) { |
| document.getElementById('threshold-val').textContent = parseFloat(val).toFixed(3); |
| const pct = ((val - 0.001) / (0.020 - 0.001)) * 100; |
| document.getElementById('slider-fill').style.width = pct + '%'; |
| } |
| |
| const chipsEl = document.getElementById('chips'); |
| EXAMPLES.forEach(ex => { |
| const chip = document.createElement('span'); |
| chip.className = 'chip'; |
| chip.textContent = ex.length > 55 ? ex.slice(0, 55) + '…' : ex; |
| chip.title = ex; |
| chip.onclick = () => { document.getElementById('text-input').value = ex; runAnalyse(); }; |
| chipsEl.appendChild(chip); |
| }); |
| |
| async function runAnalyse() { |
| const text = document.getElementById('text-input').value.trim(); |
| const threshold = parseFloat(document.getElementById('threshold-slider').value); |
| if (!text) return; |
| |
| const btn = document.getElementById('run-btn'); |
| btn.disabled = true; |
| btn.textContent = '⏳ Analysing...'; |
| |
| try { |
| const res = await fetch('/predict', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ text, threshold }), |
| }); |
| if (!res.ok) throw new Error('Server error ' + res.status); |
| renderResults(await res.json()); |
| } catch (err) { |
| document.getElementById('detected-panel').classList.add('visible'); |
| document.getElementById('detected-list').innerHTML = |
| '<div class="d-name" style="color:#ff3c5a">// ERROR</div>' + |
| '<div class="d-desc">' + err.message + '</div>'; |
| } finally { |
| btn.disabled = false; |
| btn.textContent = '⚡ Analyse Text'; |
| } |
| } |
| |
| function renderResults(data) { |
| const detectedPanel = document.getElementById('detected-panel'); |
| const detectedList = document.getElementById('detected-list'); |
| const detectedTitle = document.getElementById('detected-title'); |
| detectedPanel.classList.add('visible'); |
| |
| if (!data.any_detected) { |
| detectedTitle.textContent = '// result'; |
| detectedList.innerHTML = ` |
| <div class="clean-box"> |
| <h3>✅ No Distortions Detected</h3> |
| <p>Thinking pattern appears healthy at this threshold.<br/> |
| Try lowering the threshold slider if you expected a result.</p> |
| </div>`; |
| } else { |
| detectedTitle.textContent = '// detected (' + data.detected.length + ')'; |
| detectedList.innerHTML = data.detected.map(d => ` |
| <div class="distortion-item"> |
| <div class="d-name">${d.emoji} ${d.pretty}</div> |
| <div class="d-desc">${d.description}</div> |
| <div class="d-score">// score: ${d.score.toFixed(5)}</div> |
| </div>`).join(''); |
| } |
| |
| const maxScore = Math.max(...data.all_scores.map(s => s.score)); |
| document.getElementById('scores-panel').classList.add('visible'); |
| document.getElementById('scores-list').innerHTML = data.all_scores.map(s => { |
| const barPct = maxScore > 0 ? ((s.score / maxScore) * 100).toFixed(1) : 0; |
| const hi = s.detected ? ';color:var(--green);text-shadow:0 0 6px var(--green)' : ''; |
| return ` |
| <div class="prob-row"> |
| <div class="prob-header" style="${hi}"> |
| <span>${s.emoji} ${s.pretty}</span> |
| <span class="prob-score">${s.score.toFixed(5)}</span> |
| </div> |
| <div class="prob-bar-bg"> |
| <div class="prob-bar-fill" style="width:${barPct}%;${s.detected ? '' : 'opacity:0.3'}"></div> |
| </div> |
| </div>`; |
| }).join(''); |
| } |
| |
| document.getElementById('text-input').addEventListener('keydown', e => { |
| if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) runAnalyse(); |
| }); |
| </script> |
| </body> |
| </html>""" |
|
|
|
|
| @app.get("/", include_in_schema=False) |
| def index() -> HTMLResponse: |
| return HTMLResponse(content=FRONTEND_HTML) |
|
|
|
|
| if __name__ == "__main__": |
| uvicorn.run(app, host="0.0.0.0", port=7860) |