Test_AI_writing / app.py
unknown
Claude Opus 5.5
PhonePe webhook (POST /phonepe/webhook): checks the SHA256 username:password header and settles the order from PhonePe's status
6f0b0af
Raw History Blame Contribute Delete
187 kB
"""
CS AI Writing Suite — one Space, three tools behind a navigation bar:
🚀 Smart Humanize detector finds AI-flagged sentences → humanizer rewrites only those → re-check (loop)
🔍 AI Detector highlighted AI-likelihood PDF report (RoBERTa detector)
✨ Humanizer Parrot-T5 paraphraser (whole text / whole document)
Access rule: pasting text is open to everyone; uploading a document requires sign-in.
"""
import html
import hashlib
import os
import threading
import secrets
from pathlib import Path
import re
import gradio as gr
import humanizer as H # loads the paraphraser (kept on CPU until a GPU call)
import detector as D # loads the detector (+ starts LibreOffice fetch)
import smart as S
import auth as A
import mailer as M
import push as PU
import dashboard as DB
import plans as P
import wallet as W
import time
import site_pages as SP
import report_flags as RF
import lt_grammar as LT
import textparse as TP
import privacy as PV
import ui_theme as UI
LT.warmup() # start LanguageTool in the background (free grammar check)
import clarity as CL
CL.warmup() # load the clarity-check model in the background
import lexsub as LX
LX.warmup() # synonym dictionary + vocabulary model in the background
from humanizer import BRAND, RAINBOW, LOGO_SRC, FAVICON_SRC, FAVICON_PATH, MODES, OUT_DIR
APP_NAME = os.getenv("APP_NAME", "CS AI Writing Suite")
SITE_URL = "https://cssoftwaresolutions.in"
# =====================================================================
# Shared UI helpers
# =====================================================================
def _stats_gauge(title, pct, caption, good_low=True):
if pct is None:
return H._gauge(title, None, caption)
return H._gauge(title, pct, caption, invert=not good_low)
def suite_panel(ai_before, ai_after, original, revised, labels=None, extra=None, sim_vocab=True):
lb = labels or ("AI · our detector, before", "Our AI detector on your original",
"AI · our detector, after", "Same detector on the rewritten text")
gauges = [_stats_gauge(lb[0], ai_before, lb[1]), _stats_gauge(lb[2], ai_after, lb[3])]
for e in (extra or []): # (title, value, caption[, neutral chip text])
gauges.append(H._gauge(e[0], e[1], e[2], fixed_color=BRAND["navy"], chip=e[3]) if len(e) > 3
else _stats_gauge(*e[:3]))
if sim_vocab:
sim = H.similarity_to_source(original, revised)
changed = H.text_change(original, revised)
gauges += [H._gauge("Similarity to source", sim, "Word n-gram overlap with your original"),
H._gauge("Vocabulary changed", changed, "Share of new wording", fixed_color=BRAND["navy"])]
cols = min(5, max(2, len(gauges)))
return f"""
<div class="cs-scores cs-cols-{cols}">
{"".join(gauges)}
</div>
<div class="cs-disclaimer">
<b>Indicators only.</b> The AI score comes from our own detector and can differ from other checkers.
Similarity is measured against your own text, not internet or journal databases.
Always review the output for meaning, technical terms and citations before use.
</div>"""
EMPTY = """<div class="cs-scores cs-empty"><div>Scores appear here after processing.</div></div>"""
def _locked(what):
if what == "dashboard": # not an upload: the customer's own history and files
return ('<div class="cs-lock"><div class="cs-lock-ico">🔒</div>'
'<div class="cs-lock-t">Sign in to see your dashboard</div>'
'<div class="cs-lock-s">Your runs, scores, downloads and plan are kept in your free account. '
'Pasting text works without signing in.</div></div>')
return (f'<div class="cs-lock"><div class="cs-lock-ico">{UI.LOCK_ICON}🔒</div>'
f'<div class="cs-lock-t">Sign in to upload {what}</div>'
f'<div class="cs-lock-s">A free account takes a minute, and your first document is free. '
f'No account is needed to paste text: use <b>Paste text</b> above.</div></div>')
def _userbar(user):
if not user:
return ('<div class="cs-userbar"><span class="cs-ub-pill"><i></i>Guest</span>'
f'<small>{P.FREE_TRIAL_DOCS} free document{"" if P.FREE_TRIAL_DOCS == 1 else "s"} after sign-up</small></div>')
return (f'<div class="cs-userbar" title="{html.escape(P.usage_line(user))}"><span><b>{html.escape(user["name"])}'
f'</b> {P.badge(user)}</span></div>')
def _profile(user):
if not user:
return ""
with A._db() as con:
row = con.execute("SELECT created, last_login, runs, text_runs FROM users WHERE email=?",
(user["email"],)).fetchone()
created, last, runs, text_runs = row or ("", "", 0, 0)
return (f'<div class="cs-auth-title">My account</div>'
f'<div class="cs-prof"><div><span>Name</span><b>{html.escape(user["name"])}</b></div>'
f'<div><span>Email</span><b>{html.escape(user["email"])}</b></div>'
f'<div><span>Member since</span><b>{(created or "")[:10]}</b></div>'
f'<div><span>Document runs</span><b>{runs or 0}</b></div>'
f'<div><span>Pasted-text runs</span><b>{text_runs or 0}</b></div>'
f'<div><span>Plan</span><b>{P.badge(user) or "Free"}</b></div>'
+ _limits_row(user) + '</div>'
+ f'<div class="cs-gauge-cap" style="text-align:left;margin-top:8px">{html.escape(P.usage_line(user))}</div>')
def _limits_row(user):
if A.is_admin(user):
return '<div><span>Daily limit</span><b>Unlimited (admin)</b></div>'
who = A._who(user, None)
parts = []
for kind, label in (("doc", "documents"), ("text", "texts")):
lim = A.LIMITS.get(("user", kind), 0)
if lim > 0:
parts.append(f"{A.usage_today(who, kind)}/{lim} {label}")
return f'<div><span>Used today</span><b>{" · ".join(parts)}</b></div>' if parts else ""
# =====================================================================
# Handlers — status panel streaming via H._run_live
# =====================================================================
def upsell_html(up):
"""Centred, animated pop-up for plan / sign-in / limit messages (never shown as an error)."""
kind = getattr(up, "kind", "upgrade")
icon = {"upgrade": "💎", "signin": "🔐", "limit": "⏳", "extra": "📄", "wallet": "💳"}.get(kind, "✨")
title = html.escape(getattr(up, "headline", "") or "Heads up")
text = html.escape(getattr(up, "text", "") or str(up))
st = P.PLANS["scholar"]
wallet_perks = [f"Add {', '.join(W.inr(x * 100) for x in W.TOPUPS)} with UPI, card or net banking (PhonePe)",
"Pay only for what each document uses", "The usual price is shown before every run",
"A run that fails costs nothing; your balance never expires"]
perks = {
"upgrade": wallet_perks,
"wallet": wallet_perks,
"signin": [f"Free trial document (up to {P.PLANS['free']['doc_words']:,} words)",
f"{P.FREE_DETECT_DAY} detector reports a day", "Run history & re-downloads", "No card needed"],
"extra": [f"Extra credits: {P.price_of('extra', 'INR')} / {P.price_of('extra', 'USD')} each",
"Continue straight away", f"Bought credits stay valid {P.CREDIT_MONTHS} months",
"Your monthly credits start again next month"],
}.get(kind, [])
go = {"upgrade": ("💳 Add money", "cs-go-pricing"), "extra": ("💳 Add money", "cs-go-pricing"),
"wallet": ("💳 Add money", "cs-go-pricing"),
"signin": ("🔐 Sign in / Create free account", "cs-go-account")}.get(kind)
perks_html = "".join(f'<li><span>✓</span>{html.escape(x)}</li>' for x in perks)
btns = (f'<button class="cs-up-go" data-cs-go="{go[1]}">{go[0]}</button>' if go else "") + \
f'<button class="cs-up-later" data-cs-close="1">{"Maybe later" if go else "OK"}</button>'
modal = (f'<div class="cs-upsell cs-up-{kind}" role="dialog" aria-modal="true" aria-label="{title}">'
'<div class="cs-up-backdrop" data-cs-close="1"></div>'
'<div class="cs-up-card"><div class="cs-up-ring"></div>'
'<button class="cs-up-x" data-cs-close="1" aria-label="Close">×</button>'
f'<div class="cs-up-icon">{icon}</div><div class="cs-up-title">{title}</div>'
f'<div class="cs-up-text">{text}</div>'
+ (f'<ul class="cs-up-perks">{perks_html}</ul>' if perks_html else "")
+ f'<div class="cs-up-actions">{btns}</div>'
'<i class="cs-sp s1">✦</i><i class="cs-sp s2">✦</i><i class="cs-sp s3">✧</i><i class="cs-sp s4">✦</i>'
'</div></div>')
link = (f' <a class="cs-up-link" data-cs-go="{go[1]}">{go[0]}</a>' if go else "")
return modal + f'<div class="cs-nudge">{icon} {text}{link}</div>'
# Text the detector already reads as human-written (0% AI) is left alone. The customer may still want it reworded,
# so the result comes with a pop-up that asks; "Rephrase anyway" presses a hidden button that runs the rewrite.
FORCED = __import__("contextvars").ContextVar("rephrase_anyway", default=False)
FORCE_VOCAB = float(os.getenv("REPHRASE_ANYWAY_VOCAB", "35")) # new wording aimed for in that rewrite (%)
FORCE_PASSES = int(os.getenv("REPHRASE_ANYWAY_PASSES", "2"))
def already_human_html(text_mode):
"""Pop-up + in-page reminder for a text / document that scored 0% AI and was therefore not changed."""
go = "cs-go-rephrase-text" if text_mode else "cs-go-rephrase-doc"
what = "text" if text_mode else "document"
modal = ('<div class="cs-upsell cs-up-human" role="dialog" aria-modal="true" '
f'aria-label="Your {what} already reads as human-written">'
'<div class="cs-up-backdrop" data-cs-close="1"></div>'
'<div class="cs-up-card"><div class="cs-up-ring"></div>'
'<button class="cs-up-x" data-cs-close="1" aria-label="Close">×</button>'
f'<div class="cs-up-icon">✅</div><div class="cs-up-title">Your {what} already reads as human-written</div>'
f'<div class="cs-up-text">Our detector scores it <b>0% AI</b>, so nothing was changed. '
f'Do you still want it rephrased?</div>'
'<ul class="cs-up-perks"><li><span>✓</span>Every sentence is reworded; your numbers, citations and '
'technical terms stay</li><li><span>✓</span>Your original stays in the box, so you can compare</li></ul>'
f'<div class="cs-up-actions"><button class="cs-up-go" data-cs-go="{go}" data-cs-stay="1">✍️ Rephrase anyway'
'</button><button class="cs-up-later" data-cs-close="1">Keep my text</button></div>'
'</div></div>')
return modal + (f'<div class="cs-nudge">✅ Already human-written (0% AI): nothing was changed. '
f'<a class="cs-up-link" data-cs-go="{go}" data-cs-stay="1">Rephrase anyway</a></div>')
# ---------------------------------------------------------------- pay per use (plans.usage_price)
# Set for a job of a signed-in customer without credits: the job runs, the scores are shown, and the result is
# locked behind a charge priced from what the job used.
PAYG_FOR = __import__("contextvars").ContextVar("pay_per_use_for", default=None)
LOCKED_PREVIEW = ('<div class="cs-scores cs-empty"><div>🔒 The text is shown after payment. Your scores are above.'
'</div></div>')
def pay_popup_html(charge, which="smart", modal=True):
"""Pop-up (and in-page line) for a locked result: the price, a Pay button that opens the payment page, and
"I have paid", which unlocks the download once the payment is confirmed."""
url = html.escape(P.pay_link(charge), quote=True)
amount = f"₹{charge['amount_inr']:,.0f}"
name, ref = html.escape(str(charge["name"])), html.escape(charge["ref"])
go = f"cs-go-unlock-{which}"
card = ('<div class="cs-upsell cs-up-pay" role="dialog" aria-modal="true" aria-label="Your document is ready">'
'<div class="cs-up-backdrop" data-cs-close="1"></div>'
'<div class="cs-up-card"><div class="cs-up-ring"></div>'
'<button class="cs-up-x" data-cs-close="1" aria-label="Close">×</button>'
'<div class="cs-up-icon">🔓</div><div class="cs-up-title">Your document is ready</div>'
f'<div class="cs-up-text"><b>{name}</b> · {int(charge["words"] or 0):,} words<br>Unlock the download for '
f'<b>{amount}</b> (includes {P.PRICE_GST:.0f}% GST). Reference <b>{ref}</b>.</div>'
'<ul class="cs-up-perks"><li><span>✓</span>The price is worked out from what this document actually '
'used</li><li><span>✓</span>Your scores are already shown on the page</li>'
'<li><span>✓</span>After paying, press “I have paid” to download</li></ul>'
f'<div class="cs-up-actions"><a class="cs-up-go" href="{url}" target="_blank" rel="noopener" '
f'data-cs-close="1">💳 Pay {amount}</a>'
'<button class="cs-up-later" data-cs-close="1">Later</button></div></div></div>') if modal else ""
return card + (f'<div class="cs-nudge">🔒 Locked until paid: <b>{amount}</b> ({ref}). '
f'<a class="cs-up-link" href="{url}" target="_blank" rel="noopener">Pay now</a> · '
f'<a class="cs-up-link" data-cs-go="{go}" data-cs-stay="1">I have paid: unlock</a></div>')
def not_lowered(before, after):
"""True when a run that had something to lower left the AI score where it was (or higher)."""
try:
return float(before) > 0 and float(after) >= float(before)
except (TypeError, ValueError):
return False
def _payg_lock(path, tool, name, words, seconds, rep):
"""If this job is pay-per-use: price it, record the charge, keep the file out of the run history. Returns
the charge (dict) or None when the job is covered by a plan, credits or the trial."""
email = PAYG_FOR.get()
if not email or not path:
return None
price = P.usage_price(seconds, S.G.spent())
ref = P.create_charge(email, tool, name, words, price, path)
if rep is not None and hasattr(rep, "meta"):
rep.meta.update(output="", locked=ref, price=price["inr"])
print(f"[charge] {ref} {email} {name}: {words} words, {price['seconds']}s -> GPU ${price['gpu_usd']:.4f} + "
f"Space share ${price['space_usd']:.4f} + language model ${price['llm_usd']:.4f} = cost "
f"${price['cost_usd']:.4f}; +{P.PRICE_MARGIN:.0f}% +{P.PRICE_GST:.0f}% GST = ₹{price['inr']}", flush=True)
return P.charge_of(ref)
def claim_paid(user):
""""I have paid": hand over the customer's latest locked result once its charge is marked paid."""
keep = (gr.update(), gr.update())
if not user:
return (H.status_error("Please sign in to the account the document was run on."),) + keep
c = P.latest_charge(user["email"])
if not c:
return (H.status_idle("No locked result was found for this account."),) + keep
if c["status"] != "paid":
return (H.status_done(f"🔒 The payment for **{c['name']}** ({c['ref']}) is not confirmed yet. It is "
"unlocked as soon as it is confirmed; press **I have paid** again in a moment.")
+ pay_popup_html(c, modal=False),) + keep
if not c["output"] or not os.path.exists(c["output"]):
return (H.status_error(f"The file for {c['ref']} is no longer on the server. Please contact us with this "
"reference and we will run it again at no charge."),) + keep
return (H.status_done(f"✅ **Payment received** — {c['name']} is unlocked. Download it below."), c["output"],
H.download_button(c["output"]))
def _upsell_frame(up, n, idx):
frame = [gr.update()] * n
frame[idx] = upsell_html(up)
return tuple(frame)
def _who(user, request):
"""The visitor: account email, else browser id, else IP ('' if unknown)."""
who = (user or {}).get("email") or PU.cid_from(request)
if not who:
try:
who = "ip:" + (request.client.host if request is not None and request.client else "")
except Exception:
who = ""
return "" if who == "ip:" else who
def stop_jobs(user, request: gr.Request = None):
"""Stop & reset button: stop this visitor's running jobs (the page then reloads itself)."""
n = H.cancel_jobs(_who(user, request))
print(f"[job] stop & reset: {n} job(s) stopped", flush=True)
def _job_key(user, request, tool, args):
"""Same visitor + same tool + same input/settings -> same job (used to resume after a dropped connection)."""
who = _who(user, request)
if not who:
return None
h = hashlib.sha1(f"{who}|{tool}".encode())
for a in args:
if isinstance(a, str) and len(a) < 1024 and os.path.isfile(a):
with open(a, "rb") as fh: # uploaded file: by content (temp path may change)
h.update(hashlib.sha1(fh.read()).digest())
else:
h.update(repr(a).encode())
return h.hexdigest()
def _finish_tip(job, user, need_login, tool):
tip = P.nudge(user, need_login)
paid = job.extra.get("wallet")
if paid:
tip = (f'<div class="cs-nudge">💳 {W.inr(paid[0])} was taken from your wallet for this run (balance '
f'{W.inr(paid[1])}).</div>' + tip)
sent_to = job.extra.get("mailed")
if sent_to:
tip = f'<div class="cs-nudge">📧 The result is also being emailed to <b>{html.escape(sent_to)}</b>.</div>' + tip
return tip + f'<span class="cs-job" data-job="{job.id}" hidden></span>'
_UPLOAD_EXT = (".docx", ".pdf", ".txt")
_IN_DIR = OUT_DIR / "inputs"
def _stable_uploads(args):
"""Uploaded files live in Gradio's temp folder, which is emptied when the Space restarts or updates (and may
be cleaned while a long job runs). Copy each upload into our own folder before the job starts, and give a
clear message when the browser still points at a file the server no longer has."""
import shutil
out = []
for a in args:
p = str(a) if isinstance(a, (str, Path)) else None
if p and os.path.isabs(p) and p.lower().endswith(_UPLOAD_EXT):
if not os.path.exists(p):
raise gr.Error("Your uploaded file is no longer on the server (the app was restarted or updated). "
"Please remove the file (×) and upload it again, then press the button.",
title="Please upload the file again")
if not p.startswith(str(_IN_DIR)):
with open(p, "rb") as fh:
tag = hashlib.sha1(fh.read()).hexdigest()[:16]
dst = _IN_DIR / tag / os.path.basename(p)
if not dst.exists():
dst.parent.mkdir(parents=True, exist_ok=True)
shutil.copyfile(p, dst)
a = str(dst)
out.append(a)
return tuple(out)
def _stream(core, args, n, idx, user=None, need_login=False, request=None, tool="", words=None, paste=None,
quota=True):
args = _stable_uploads(args)
# the uploaded file's content key (folder name under inputs/): the same file re-run = free re-run
fkey = next((Path(str(a)).parent.name for a in args if isinstance(a, str) and a.startswith(str(_IN_DIR))), None)
key = _job_key(user, request, tool, args)
upsell = None
# find-or-start in one step: a double click, or the page re-asking after a dropped connection while the first
# request is still being set up, must re-attach to that job, never start (and charge) a second one
with _START_LOCK:
job = H.find_job(key)
if job is not None and job.done.is_set() and (job.delivered or "error" in job.box):
job = None # already shown to the customer: this is a fresh run
if job is None: # new job: plan / limit checks first
PAYG_FOR.set(None)
wallet = None
try:
if paste is not None:
P.check_paste(user, paste) # words per paste for this plan
if need_login:
A._require(user)
if quota: # credits / plan / free trial / free re-run + word cap
if P.check_doc(user, words, tool, fkey) == "wallet":
wallet = _wallet_plan(user, tool, core, words) # money reserved before the job runs
if quota:
A.check_limit(user, request, need_login)
except A.Upsell as up: # plan / sign-in / limit -> friendly pop-up, not an error
upsell = up
if wallet is not None:
W.release(wallet["ref"])
if upsell is None:
job = _start_billed_job(key, core, args, user, request, tool, need_login, words, quota, fkey,
wallet)
if upsell is not None:
yield _upsell_frame(upsell, n, idx)
return
last = None
try:
for frame in H.follow(job, n, idx):
last = frame
yield frame
except gr.Error as err: # no engine / service names in customer-facing errors
raise gr.Error(PV.clean(getattr(err, "message", str(err))) or "Something went wrong; please try again.")
tip = _finish_tip(job, user, need_login, tool)
if last is not None and isinstance(last[idx], str):
frame = list(last)
frame[idx] = last[idx] + tip
yield tuple(frame)
job.delivered = True # the page received the result
_START_LOCK = threading.Lock()
def _wallet_kind(tool, core):
"""Which kind of work a job is, for its usual price (wallet.usual)."""
if core in (_fast_text_core, _fast_docx_core):
return "fast", F.BUDGET
if "Smart" in tool or "Humaniz" in tool:
return "smart", S.TIME_BUDGET
if "Detect" in tool or "AI Writing" in tool or "Report" in tool:
return "detector", 300
if "Proofread" in tool:
return "proofread", 600
return "other", 900
def _wallet_plan(user, tool, core, words):
"""A job paid from the wallet: reserve its money now, or ask the customer to add money (pop-up)."""
kind, budget = _wallet_kind(tool, core)
have = W.available(user["email"])
plan = W.plan_job(kind, words or 0, have, budget)
if "short" in plan:
raise A.Upsell(f"This {words or 0:,}-word document usually costs {W.inr(plan['usual'])}. To start, "
f"{W.inr(plan['short'])} is set aside from your wallet for the run (it has {W.inr(have)}); you "
"are charged only what the run actually uses and the rest stays in your wallet. Add money on "
"the Pricing tab with UPI, a card or net banking (PhonePe).",
kind="wallet", title="Add money to your wallet")
ref = W.reserve(user["email"], plan["hold"], tool)
if ref is None: # another run took the money a moment ago
raise A.Upsell("Your wallet balance is reserved by another run that is still going. Wait for it to finish, "
"or add money on the Pricing tab.", kind="wallet", title="Add money to your wallet")
return dict(plan, ref=ref, kind=kind)
def _start_billed_job(key, core, args, user, request, tool, need_login, words, quota, fkey, wallet=None):
"""Start a customer's job; logging, usage, charging, the result email and the push notification happen when it
ends, on the server, even if the page was closed. wallet: the job is paid from the prepaid wallet; it runs
inside its reservation (Groq cap + deadline) and is charged its actual price when it ends."""
cid, t0 = PU.cid_from(request), time.time()
def on_finish(j): # runs on the server even if the page was closed
meta = j.meta
llm = S.G.spent() # this job's language-model bill (its own account)
who = (user or {}).get("email") or ""
if "error" in j.box:
err = j.box["error"]
if wallet:
W.release(wallet["ref"]) # a run that fails or is stopped costs the customer nothing
W.record_cost(who, tool, words, time.time() - t0, llm, 0, "failed")
DB.log_run(user, request, tool, need_login, meta, time.time() - t0, "error",
str(err) or type(err).__name__)
return
charged = 0
if wallet:
if meta.get("no_charge"):
W.release(wallet["ref"])
else:
charged = W.settle(wallet["ref"], time.time() - t0, llm, words, meta.get("name") or tool)["charged"]
j.extra["wallet"] = (charged, W.available(who))
W.record_cost(who, tool, words, time.time() - t0, llm, charged,
"wallet" if wallet else ("free" if not need_login else "plan"))
DB.log_run(user, request, tool, need_login, meta, time.time() - t0, "ok")
A._count_run(user, document=need_login) # counted only after a successful run
if quota:
A.add_usage(user, request, need_login)
if need_login and quota and not meta.get("locked"): # (a locked result is paid for, not counted)
if not meta.get("no_charge") and not wallet: # (a wallet run is paid in money, not in credits)
P.consume_doc(user, words, tool, fkey) # it must not unlock "free re-runs" of that file
j.extra["mailed"] = M.send_result(user, tool, meta)
b_, a_ = meta.get("before"), meta.get("after")
score = f" — AI score {b_}% → {a_}%" if (b_ is not None and a_ is not None) else ""
PU.send_to(cid, user, {"title": "✅ Your result is ready",
"body": f"{tool}: {meta.get('name') or 'your text'}{score}. Tap to open your result.",
"tag": f"cs-ready-{j.id}", "icon": ICON_URL, "url": "/"})
toks = []
if wallet: # the job's limits travel with it into its thread
toks = [(S.G.WALLET_CAP, S.G.WALLET_CAP.set(wallet["groq_cap"])),
(H.JOB_DEADLINE, H.JOB_DEADLINE.set(t0 + wallet["deadline_s"]))]
try:
return H.start_job(key, core, args, on_finish, owner=_who(user, request))
finally:
for var, tok in toks:
var.reset(tok)
def count_words_for(text, user):
return H.count_words(text, P.paste_limit(user))
# ---------------- Companion tools: journal formatting and journal finder ----------------
import formatter_tab as FMT # Journal Formatter (FormatMatch engine in ./formatmatch)
import finder_tab as FND # Journal Finder (journal_traffic pipeline in ./finder)
TOOLS_EMBED = os.getenv("TOOLS_EMBED", "0") == "1"
FORMAT_URL = os.getenv("FORMAT_URL", "https://formatmatch.streamlit.app/")
FINDER_URL = os.getenv("FINDER_URL", "https://journal-finder-catalyst-scholar.streamlit.app/")
_NEXT_CSS = (".cs-next{display:flex;flex-wrap:wrap;gap:10px;margin:10px 0 4px}"
".cs-next a{flex:1 1 200px;text-align:center;padding:11px 14px;border-radius:10px;font-weight:700;"
"text-decoration:none!important;border:2px solid var(--cs-navy,#08238A);color:var(--cs-navy,#08238A)!important;"
"background:#fff}.cs-next a:hover{background:#eef1fb}.cs-next small{display:block;font-weight:500;"
"opacity:.75;margin-top:2px}")
def next_steps_html():
"""Buttons shown under every humanized result: format for a journal / find a journal."""
if not TOOLS_EMBED: # both tools are tabs of this app: open the tab
return (f"<style>{_NEXT_CSS}.cs-next a{{cursor:pointer}}</style><div class='cs-next'>"
"<a data-cs-go='cs-go-formatter'>📐 Format for a journal"
"<small>Word file + Overleaf-ready LaTeX in the journal's format</small></a>"
"<a data-cs-go='cs-go-finder'>🔎 Find a journal"
"<small>Journals that match your title, with indexing and acceptance odds</small></a></div>")
return (f"<style>{_NEXT_CSS}</style><div class='cs-next'>"
f"<a href='{FORMAT_URL}' target='_blank' rel='noopener'>📐 Format for a journal"
"<small>Arrange your humanized file to a journal's template</small></a>"
f"<a href='{FINDER_URL}' target='_blank' rel='noopener'>🔎 Find a journal"
"<small>Journals that match your title, with indexing and acceptance odds</small></a></div>")
def companion_tab_html(url, what, how):
embed = url.rstrip("/") + "/?embed=true"
return (f"<style>{_NEXT_CSS}.cs-embed{{width:100%;height:1100px;border:1px solid #e3e7ee;border-radius:12px}}</style>"
f"<div class='cs-intro'><b>{what}</b> {how}</div>"
f"<div class='cs-next'><a href='{url}' target='_blank' rel='noopener'>↗ Open in a new tab"
"<small>Recommended for large files</small></a></div>"
f"<iframe class='cs-embed' src='{embed}' loading='lazy' "
f"allow='clipboard-write' title='{what.rstrip('.')}'></iframe>"
"<p><small>If the window shows a wake-up message, press the button inside it and wait a few seconds.</small></p>")
# ---------------- Smart Humanize ----------------
def _smart_result(res, hi, mod, text_mode, rep=None, name=""):
b, a = res["before"], res["after"]
if rep is not None and hasattr(rep, "meta"):
rep.meta.update(name=name, words=b["all_words"], before=b["pct"], after=a["pct"],
output=res["path"], engine=D.LAST_ENGINE)
# customers see only the headline; the pass log and engine notes go to the Space logs
took = f", {time.time() - rep.start:.0f}s" if rep is not None and getattr(rep, "start", None) else ""
print(f"[smart] {name}: AI {b['pct']}% -> {a['pct']}%{took}, {b['all_words']} words | " + " | ".join(res["log"])
+ f" |{res['note']}", flush=True)
print(f"[cost] Smart Humanize, {b['all_words']} words: {S.G.usage_line(S.G.ACCT.get() or {})}", flush=True)
msg = f"✅ **Done** — AI score {b['pct']}% → {a['pct']}%."
log_ = res.get("log") or []
n_pass = len({m.group(1) for m in (re.match(r"Pass (\d+): AI score .*rewrote", x) for x in log_) if m})
why = next((x.split(" — ", 1)[1] for x in reversed(log_) if " — " in x), "")
locked = max([int(m.group(1)) for m in (re.search(r"\((\d+) skipped", x) for x in log_) if m] or [0])
if n_pass:
msg += (f" ({n_pass} pass{'es' if n_pass != 1 else ''}"
+ (f"; {why.replace('stopped: ', 'stopped after ')}" if why else "") + ")")
# rewritten, yet our detector's score is where it was: say so plainly (and such a run is never charged)
stuck = not_lowered(b["pct"], a["pct"]) and not res.get("report")
if stuck:
msg = (f"⚠️ **Rewritten, but the AI score did not come down** ({b['pct']}% → {a['pct']}%). The sentences "
"were reworded and checked for meaning, yet our detector still reads the passage as AI-written. "
"This happens mostly with short or very uniform passages, which are scored as one block. Press "
"the button again for a different rewrite, or add more of the surrounding text.")
if locked:
msg += (f"\n\n🔒 {locked} flagged sentence(s) could not be edited (citation fields, links, equations or "
"sub/superscripts inside the sentence) and keep their wording.")
# 0% is the promise: a document that did not reach it is delivered with the sentences still flagged highlighted,
# and it is not charged (no pay-per-use payment, no credit used)
short = bool(res.get("zero_goal")) and a["pct"] > 0 and not res.get("report") and not stuck
if short:
msg = (f"⚠️ **AI score {b['pct']}% → {a['pct']}%.** {res.get('left') or 'A few'} sentence(s) still read as "
"AI-written and could not be rewritten further without changing what they say; they are "
"highlighted in the AI view below so you can adjust them yourself.")
labels, g_before, g_after = None, b["pct"], a["pct"]
gline = _grammar_line(res.get("note"))
rs = res.get("report")
table = ""
extra = None
if rs: # report-guided run
what = "AI" if rs.get("kind") != "Similarity" else "Similarity"
if rs.get("score") is not None:
g_before, g_after = rs["score"], rs["est"]
labels = (f"{what} · your report", "Read from the report you uploaded",
f"{what} · report, estimated now", "Our estimate for the same checker; re-check there to confirm")
if what == "AI": # our own detector, clearly labelled as a different scale
extra = [("AI · our detector, now", a["pct"],
f"Our AI detector (was {b['pct']}%); a different scale from your report")]
msg = (f"✅ **Done** — report {what} score {rs['score']:g}% → estimated **{rs['est']:g}%** "
f"after rewriting. Re-check with the same checker for the actual score.")
else:
msg += "\n\nThe score could not be read from your report, so our own detector's score is shown."
kept = rs["flagged"] - rs["rephrased"]
msg += (f"\n\n📑 **Report:** {rs['flagged']} flagged sentence{'s' if rs['flagged'] != 1 else ''} found · "
f"**{rs['rephrased']} rephrased**" + (f" · {kept} kept as written" if kept else "")
+ (f" · ⚠ {rs['likely']} likely still flagged (marked in the table below)" if rs.get("likely") else "")
+ "."
+ (" (Scanned report read with text recognition.)" if rs.get("ocr") else ""))
table = _report_table(rs)
if res.get("proof") is False or "proof" not in res:
msg += gline # (no proofread ran: the humanizer's own final-polish line)
else:
msg += _proof_line(res["proof"])
untouched = b["pct"] == 0 and not rs and not any("rewrote" in x for x in log_)
if untouched and not FORCED.get(): # nothing to humanize: say so, and ask before rewording
msg = ("✅ **Already human-written** — AI score 0%, so nothing was changed. "
"If you still want it reworded, choose **Rephrase anyway**.")
elif FORCED.get():
msg += "\n\n✍️ Rephrased at your request (the text scored 0% AI before)."
preview = D.build_html(res["blocks"], a, hi, mod)
panel = suite_panel(g_before, g_after, res["orig"], res["new"], labels, extra) + table
_remember(res["path"], ours=a["pct"], ours_before=b["pct"],
tool=(rs or {}).get("tool"), kind=(rs or {}).get("kind"),
rep_score=(rs or {}).get("score"), rep_est=(rs or {}).get("est"))
status = H.status_done(msg) + (already_human_html(text_mode) if untouched and not FORCED.get() else "")
out = [status, preview, panel, res["path"], H.download_button(res["path"])]
charge = None if text_mode or untouched or stuck or short else _payg_lock(
res["path"], "Smart Humanize", name, b["all_words"], res.get("seconds", 0), rep)
if stuck and PAYG_FOR.get() and not text_mode:
out[0] = H.status_done(msg + "\n\n🎁 **No charge for this run**, because the score was not lowered.")
if rep is not None and hasattr(rep, "meta"): # given away, not bought: it earns no free re-runs
rep.meta.update(no_charge=True)
if short and not text_mode:
out[0] = H.status_done(msg + "\n\n🎁 **No charge for this document**, because it did not reach 0%.")
if rep is not None and hasattr(rep, "meta"): # no pay-per-use payment and no credit from the plan
rep.meta.update(no_charge=True)
if charge: # pay per use: scores now, the document after payment
out = [H.status_done(msg) + pay_popup_html(charge, "smart"), LOCKED_PREVIEW, panel, None, ""]
if text_mode:
out.insert(1, res["text"])
return tuple(out)
REPORT_KINDS = ["Recognise automatically", "AI-writing report", "Similarity / plagiarism report"]
RESULT_CTX = {} # sha1 of an output file -> its scores (so a later proofread can show matching indicators)
def _file_key(path):
try:
with open(path, "rb") as fh:
return hashlib.sha1(fh.read()).hexdigest()
except Exception:
return None
def _remember(path, **ctx):
k = _file_key(path)
if k:
if len(RESULT_CTX) > 2000:
RESULT_CTX.clear()
RESULT_CTX[k] = ctx
def _use_report(report, kind=None, rep=None):
"""Uploaded AI / similarity report (PDF) -> only the sentences it highlights are rewritten."""
if not report:
RF.ACTIVE.set(None)
return
try:
k = str(kind or "")
RF.ACTIVE.set(RF.load(report, rep, "Similarity" if k.startswith("Sim") else "AI" if k.startswith("AI") else "auto"))
except TypeError as err:
if "positional argument" not in str(err):
raise
raise gr.Error("report_flags.py on the Space is an older version than app.py. "
"Upload the latest report_flags.py (or all files from the zip) and restart the Space.")
except RF.ReportError as err:
raise gr.Error(str(err))
def _grammar_line(note):
"""The final grammar check's result, for the visible result message."""
if not H.GRAMMAR:
return ""
m = re.search(r"📝 Final (?:polish|grammar check)[^:]*: (.*?)(?:\s*\(Groq|\s*🔎|\s*📝|$)", note or "")
extra = " (Advanced check busy: standard corrections applied.)" if "unavailable" in (note or "") else ""
if m:
return f"\n\n📝 **Final polish (grammar, academic wording, flow):** {m.group(1).strip()}{extra}"
return "\n\n📝 **Final polish (grammar, academic wording, flow):** done."
_LEVEL = {"cyan": "High", "blue": "High", "purple": "Moderate", "pink": "Moderate"}
_SWATCH = {"cyan": "#bfe9ee", "blue": "#c9dcfb", "purple": "#ece6fb", "pink": "#fbe0ee", "yellow": "#fff3b0",
"green": "#d8f3d4", "orange": "#ffe1c2", "red": "#ffd6d6"}
def _report_table(rs):
"""Flagged sentences from the report, each with its rephrased version (HTML, no line breaks)."""
rows = []
sim = rs.get("kind") == "Similarity"
for i, r in enumerate(rs["rows"], 1):
col = r.get("colour") or "flagged"
lvl = "Source match" if sim else _LEVEL.get(col, col.capitalize())
sw = _SWATCH.get(col, "#eef1fb")
st = {"rephrased": '<span class="cs-rt-ok">Rephrased</span>',
"kept": '<span class="cs-rt-kept">Kept: no safe rewrite</span>',
"locked": f'<span class="cs-rt-kept">Kept: contains a {html.escape(r.get("reason") or "citation field or link")}'
f' (not edited, to keep it intact)</span>'}[r["status"]]
if r.get("likely"):
st += ('<span class="cs-rt-likely">⚠ Likely still flagged: revise this sentence in your own words '
'(change its structure, add your own reasoning or an example)</span>')
rows.append(f'<tr><td class="n">{i}</td><td><span class="lv" style="background:{sw}">{html.escape(lvl)}</span>'
f'<div class="o">{html.escape(r["orig"])}</div></td>'
f'<td><div class="w">{html.escape(r["new"]) if r["status"] == "rephrased" else "—"}</div>'
f'{st}</td></tr>')
head = "Matched in the report" if sim else "Flagged in the report"
title = ("Matched sentences from your similarity report" if sim else "Flagged sentences from your AI report")
return (f'<details class="cs-rtab" open><summary>{title} and how each was rephrased</summary>'
f'<div class="cs-rt-wrap"><table><thead><tr><th>#</th><th>{head}</th><th>Rephrased</th></tr></thead>'
'<tbody>' + "".join(rows) + '</tbody></table></div></details>')
# Every humanized text / document goes through the final check & proofread by itself (AUTO_PROOFREAD=0 = only on
# request, with the button). If the proofread cannot run, the humanized result is still delivered and says so.
AUTO_PROOFREAD = os.getenv("AUTO_PROOFREAD", "1") != "0"
def _text_docx(text):
"""A .docx with one paragraph per pasted paragraph (the proofread works on Word files)."""
from docx import Document
d = Document()
for p, _ in TP.parse_paste(text):
d.add_paragraph(p)
tmp = OUT_DIR / f"paste_{secrets.token_hex(6)}.docx"
d.save(tmp)
return str(tmp)
def _docx_text(path, like):
"""Text of a .docx made by _text_docx, with the paragraph breaks of the pasted text `like`."""
from docx import Document
paras = [p.text for p in Document(path).paragraphs]
parts = TP.parse_paste(like)
if len(parts) == len(paras):
return TP.join_paste([(p, sep) for p, (_, sep) in zip(paras, parts)])
return "\n\n".join(p for p in paras if p.strip())
def _auto_proofread(path, original, hi, mod, rep):
"""Final check & proofread of a humanized .docx. Returns the proofread result (dict) or None if it could not
run (the humanized file is then delivered as it is)."""
if not AUTO_PROOFREAD or not path or not str(path).lower().endswith(".docx"):
return None
import proofread as PR
job = S.G.ACCT.get() # the proofread keeps its own account: add it to the job's
try:
rep(0.9, "Final check & proofread of the result")
pr = PR.proofread_docx(path, original if original and str(original).lower().endswith(".docx") else None,
rep, hi, mod, protect_ai=True)
if pr["after"]["pct"] > pr["before"]["pct"] + PR.MAX_DOC_RISE:
# safety net: the proofread must never hand back a result that scores worse than the humanized one
print(f"[proofread] automatic proofread not used: AI {pr['before']['pct']}% -> {pr['after']['pct']}%",
flush=True)
return dict(pr, rejected=True)
return pr
except Exception as err: # the humanized result is still good: deliver it
print(f"[proofread] automatic proofread not done: {type(err).__name__}: {err}", flush=True)
return None
finally:
S.G.join_account(job)
def _proof_line(pr):
"""The automatic proofread's outcome for the result message."""
if not AUTO_PROOFREAD:
return ""
if pr is None:
return ("\n\n🔍 **Proofread:** could not run this time; press **Final check & proofread** to run it "
"(it is free).")
if pr.get("rejected"):
return ("\n\n🔍 **Proofread:** its corrections would have raised the AI score, so the humanized version "
"is given as it is; press **Final check & proofread** to apply them anyway (it is free).")
if pr.get("limited"):
return (f"\n\n🔍 **Proofread (basic):** {pr['n']} of {pr['total']} sentences corrected. The full check "
"could not run right now; press **Final check & proofread** later to finish it (it is free).")
return (f"\n\n🔍 **Proofread:** done automatically, {pr['n']} of {pr['total']} sentences corrected."
+ (f" {pr['held']} more correction(s) were left out because they would have raised the AI score; press "
"**Final check & proofread** to apply them anyway (it is free)." if pr.get("held") else ""))
SMART_FALLBACK = os.getenv("SMART_FALLBACK", "1") != "0"
def _full_rewrite_fallback(res, src, temp, mode, hi, mod, rep, text_mode):
"""Smart Humanize rewrote the flagged sentences and the score stayed where it was: run the full-paraphrase
engine (the Humanizer tab's) on the original and keep its result when our detector scores it lower. The
customer gets the better of the two without having to try the other tab himself."""
b, a = res["before"], res["after"]
if not SMART_FALLBACK or res.get("report") or FORCED.get() or not not_lowered(b["pct"], a["pct"]):
return res
try:
rep(0.86, "The score did not come down: trying a full rewrite")
if text_mode:
new, path = H._text_core(src, temp, 6, mode, rep, 10 ** 9)[:2]
blocks = S._score_paras([p for p, _ in TP.parse_paste(new)])
else:
path = H._docx_core(src, temp, 8, mode, rep)[0]
blocks = S._score_doc(S.DX.open_clean(path))
new = "\n".join(x.text for x in blocks if x.kind == "para")
after = D.summarise(blocks, hi, mod)
except Exception as err: # the first result stands
print(f"[smart] full-rewrite fallback not done: {type(err).__name__}: {err}", flush=True)
return res
print(f"[smart] full-rewrite fallback: AI {a['pct']}% -> {after['pct']}%", flush=True)
if after["pct"] >= a["pct"]:
return res
res = dict(res, after=after, blocks=blocks, path=path, new=new,
log=list(res.get("log") or []) + [f"Full rewrite: AI score {a['pct']}% → {after['pct']}%"])
if text_mode:
res["text"] = new
return res
def _smart_text_core(text, target, passes, temp, mode, hi, mod, full_ai, vocab, report, rkind, rep):
H.FULL_AI.set(bool(full_ai)); H.VOCAB_TARGET.set(float(vocab or 0)); _use_report(report, rkind, rep)
text = (text or "").strip()
if not text:
raise gr.Error("Please paste some text.")
H.PROOFREAD_FOLLOWS.set(AUTO_PROOFREAD)
res = S.smart_text(text, target, passes, temp, mode, hi, mod, rep)
res = _full_rewrite_fallback(res, text, temp, mode, hi, mod, rep, True)
changed = res["new"].strip() != res["orig"].strip()
pr = _auto_proofread(res["path"], _text_docx(text), hi, mod, rep) if changed else None
if pr and not pr.get("rejected"):
out = _docx_text(pr["path"], res["text"])
res.update(path=pr["path"], after=pr["after"], text=out, new=out,
blocks=S._score_paras([p for p, _ in TP.parse_paste(out)]))
res["proof"] = pr if changed else False # False: nothing was humanized, so nothing to proofread
return _smart_result(res, hi, mod, True, rep, "Pasted text")
def _smart_docx_core(path, target, passes, temp, mode, hi, mod, full_ai, vocab, report, rkind, rep):
H.FULL_AI.set(bool(full_ai)); H.VOCAB_TARGET.set(float(vocab or 0)); _use_report(report, rkind, rep)
if not path or not str(path).lower().endswith(".docx"):
raise gr.Error("Please upload a Word (.docx) file.")
H.PROOFREAD_FOLLOWS.set(AUTO_PROOFREAD)
t0 = time.time()
res = S.smart_docx(path, target, passes, temp, mode, hi, mod, rep)
res = _full_rewrite_fallback(res, path, temp, mode, hi, mod, rep, False)
changed = res["new"].strip() != res["orig"].strip()
pr = _auto_proofread(res["path"], path, hi, mod, rep) if changed else None
if pr and not pr.get("rejected"):
blocks = S._score_doc(S.DX.open_clean(pr["path"]))
res.update(path=pr["path"], after=pr["after"], blocks=blocks,
new="\n".join(b.text for b in blocks if b.kind == "para"))
res["proof"] = pr if changed else False
res["seconds"] = time.time() - t0
return _smart_result(res, hi, mod, False, rep, os.path.basename(str(path)))
# ---------------- fast engine (fast.py): the whole document at once, about a minute for any length ----------------
import fast as F
# SMART_ENGINE=fast makes it the default; a page opened with ?engine=fast / ?engine=smart picks one (testing)
FAST_DEFAULT = os.getenv("SMART_ENGINE", "smart").lower() == "fast"
# the fast engine reaches 0% in about a minute up to a few thousand words; longer documents (Groq's per-minute token
# limits) are handled by the sentence-by-sentence engine. FAST_MAX_WORDS=0 = no limit.
FAST_MAX_WORDS = int(os.getenv("FAST_MAX_WORDS", "2500"))
def _use_fast(request, words=None):
try:
e = str(dict(request.query_params).get("engine", "")).lower() if request is not None else ""
except Exception:
e = ""
if e == "fast":
return True
if FAST_MAX_WORDS and words and words > FAST_MAX_WORDS:
return False
return FAST_DEFAULT and e != "smart"
def _fast_text_core(text, target, passes, temp, mode, hi, mod, full_ai, vocab, report, rkind, rep):
if report: # a checker report: the sentence-by-sentence engine
return _smart_text_core(text, target, passes, temp, mode, hi, mod, full_ai, vocab, report, rkind, rep)
text = (text or "").strip()
if not text:
raise gr.Error("Please paste some text.")
return _smart_result(F.humanize_text(text, hi, mod, rep), hi, mod, True, rep, "Pasted text")
def _fast_docx_core(path, target, passes, temp, mode, hi, mod, full_ai, vocab, report, rkind, rep):
if report:
return _smart_docx_core(path, target, passes, temp, mode, hi, mod, full_ai, vocab, report, rkind, rep)
if not path or not str(path).lower().endswith(".docx"):
raise gr.Error("Please upload a Word (.docx) file.")
return _smart_result(F.humanize_docx(path, hi, mod, rep), hi, mod, False, rep, os.path.basename(str(path)))
def smart_text(text, target, passes, temp, mode, hi, mod, full_ai, vocab, report, rkind, user, request: gr.Request = None):
passes = P.max_passes(user, passes)
yield from _stream(_fast_text_core if _use_fast(request, len((text or "").split())) else _smart_text_core, (text, target, passes, temp, mode, hi, mod, bool(full_ai), vocab, report, rkind), 6, 0, user, request=request,
tool="Smart Humanize", paste=text)
def _smart_text_force_core(*args):
FORCED.set(True)
return _smart_text_core(*args)
def _smart_docx_force_core(*args):
FORCED.set(True)
return _smart_docx_core(*args)
def _force_args(first, target, passes, temp, mode, hi, mod, vocab, rkind):
"""Settings of a "Rephrase anyway" run: every sentence, a moderate share of new wording, no report."""
return (first, target, min(int(passes), FORCE_PASSES), temp, mode, hi, mod, False,
max(float(vocab or 0), FORCE_VOCAB), None, rkind)
def smart_text_force(text, target, passes, temp, mode, hi, mod, full_ai, vocab, report, rkind, user, request: gr.Request = None):
"""The customer chose "Rephrase anyway" for a text that scored 0% AI."""
passes = P.max_passes(user, passes)
yield from _stream(_smart_text_force_core, _force_args(text, target, passes, temp, mode, hi, mod, vocab, rkind),
6, 0, user, request=request, tool="Smart Humanize", paste=text)
def smart_docx_force(path, target, passes, temp, mode, hi, mod, full_ai, vocab, report, rkind, user, request: gr.Request = None):
passes = P.max_passes(user, passes, document=True)
yield from _stream(_smart_docx_force_core, _force_args(path, target, passes, temp, mode, hi, mod, vocab, rkind),
5, 0, user, True, request, tool="Smart Humanize", words=P.doc_words(path) if path else None)
def smart_docx(path, target, passes, temp, mode, hi, mod, full_ai, vocab, report, rkind, user, request: gr.Request = None):
passes = P.max_passes(user, passes, document=True)
words = P.doc_words(path) if path else None
yield from _stream(_fast_docx_core if _use_fast(request, words) else _smart_docx_core, (path, target, passes, temp, mode, hi, mod, bool(full_ai), vocab, report, rkind), 5, 0, user, True, request,
tool="Smart Humanize", words=words)
# ---------------- Rigorous proofread (of a finished humanized document) ----------------
def _proof_panel(res, ctx):
"""Indicators after a proofread, on the same scales as the result it was run on."""
b, a = res["before"]["pct"], res["after"]["pct"]
labels = ("AI · our detector, before proofread", "Our AI detector on the file you proofread",
"AI · our detector, after proofread", "Same detector on the proofread file")
extra = [("Sentences corrected", round(100 * res["n"] / max(1, res["total"])),
f"{res['n']} of {res['total']} sentences", "Grammar & wording fixes")]
if ctx and ctx.get("rep_est") is not None and ctx.get("kind") != "Similarity":
est = max(0, min(100, round(ctx["rep_est"] + (a - b))))
extra.insert(0, ("AI · report, estimated after proofread", est,
f"Was {ctx['rep_est']:g}% estimated after humanizing; re-check with the same checker"))
return suite_panel(b, a, "", "", labels, extra, sim_vocab=False)
def _proof_core(result, original, hi, mod, rep, standalone=False):
import proofread as PR
path = result if isinstance(result, str) else getattr(result, "name", None)
if not path or not str(path).lower().endswith(".docx"):
raise gr.Error("Upload a Word (.docx) file to proofread." if standalone else
"Humanize a Word document first; the proofread works on the humanized file.")
ctx = RESULT_CTX.get(_file_key(path))
try:
res = PR.proofread_docx(path, original if original and str(original).lower().endswith(".docx") else None,
rep, hi, mod)
except PR.G.Unavailable as err:
raise gr.Error(str(err))
except ValueError as err:
raise gr.Error(str(err))
rep.meta.update(name=os.path.basename(str(path)), words=res["before"]["all_words"],
before=res["before"]["pct"], after=res["after"]["pct"], output=res["path"])
_remember(res["path"], ours=res["after"]["pct"], ours_before=res["before"]["pct"],
**{k: (ctx or {}).get(k) for k in ("tool", "kind", "rep_score")},
rep_est=(None if not ctx or ctx.get("rep_est") is None else
max(0, min(100, round(ctx["rep_est"] + res["after"]["pct"] - res["before"]["pct"])))))
head = (f"⚠️ **Basic proofread done** — {res['n']} of {res['total']} sentences corrected. The full check "
"could not run right now; run it again later." if res.get("limited") else
f"✅ **Proofread done** — {res['n']} of {res['total']} sentences corrected.")
return (H.status_done(head + "\n\n" + res["note"]), res["path"], H.download_button(res["path"]),
_proof_panel(res, ctx))
def proofread_doc(result, original, hi, mod, user, request: gr.Request = None):
"""Free follow-up step on a document the user just humanized (does not use a document from the plan)."""
yield from _stream(_proof_core, (result, original, hi, mod), 4, 0, user, True, request,
tool="Proofread", quota=False)
PROOF_STANDALONE_FREE = os.getenv("PROOF_STANDALONE_FREE", "0") == "1"
def _proof_new_core(file, original, hi, mod, rep):
return _proof_core(file, original, hi, mod, rep, standalone=True)
def proofread_new(file, original, hi, mod, user, request: gr.Request = None):
"""Proofread any Word document (a new article): uses one document from the plan unless PROOF_STANDALONE_FREE=1."""
yield from _stream(_proof_new_core, (file, original, hi, mod), 4, 0, user, True, request,
tool="Proofread", words=P.doc_words(file) if file else None, quota=not PROOF_STANDALONE_FREE)
def _proof_text_core(text, hi, mod, rep):
from docx import Document
d = Document()
for p in [p for p, _ in TP.parse_paste(text)]:
d.add_paragraph(p)
tmp = OUT_DIR / f"paste_{secrets.token_hex(6)}.docx"
d.save(tmp)
st, path, dl, panel = _proof_core(str(tmp), None, hi, mod, rep, standalone=True)
out_text = "\n\n".join(p.text for p in Document(path).paragraphs if p.text.strip())
return st, out_text, path, dl, panel
def proofread_text(text, hi, mod, user, request: gr.Request = None):
if not (text or "").strip():
raise gr.Error("Please paste some text to proofread.")
yield from _stream(_proof_text_core, (text, hi, mod), 5, 0, user, False, request, tool="Proofread",
paste=text)
# ---------------- Detector ----------------
def _det_core(file, text, hi, mod, rep):
if mod >= hi:
raise gr.Error("Moderate threshold must be below the high threshold.")
page, pdf = D.run(file, text, hi, mod, rep)
eng = ""
return (H.status_done(f"✅ **Report ready**{eng} — download the PDF below."), page, pdf,
H.download_button(pdf))
def detect_text(text, hi, mod, user, request: gr.Request = None):
if not (text or "").strip():
raise gr.Error("Please paste some text.")
yield from _stream(_det_core, (None, text, hi, mod), 4, 0, user, request=request, tool="AI Detector", paste=text)
def detect_file(file, hi, mod, user, request: gr.Request = None):
if not file:
try:
A._require(user)
except A.Upsell as up:
yield _upsell_frame(up, 4, 0)
return
raise gr.Error("Please upload a .docx, .pdf or .txt file.")
yield from _stream(_det_core, (file, None, hi, mod), 4, 0, user, True, request, tool="AI Detector",
words=P.doc_words(file))
# ---------------- Humanizer ----------------
def _hum_text_core(text, temp, min_words, mode, max_words, full_ai, vocab, rep):
H.FULL_AI.set(bool(full_ai)); H.VOCAB_TARGET.set(float(vocab or 0))
H.PROOFREAD_FOLLOWS.set(AUTO_PROOFREAD)
out_text, path, dl, summary, _ = H._text_core(text, temp, min_words, mode, rep, max_words)
pr = _auto_proofread(path, _text_docx(text), 0.80, 0.50, rep) if out_text.strip() != (text or "").strip() else None
if pr and not pr.get("rejected"):
out_text, path = _docx_text(pr["path"], out_text), pr["path"]
dl = H.download_button(path)
summary = summary.split("\n\n")[0] + _proof_line(pr)
rep(0.97, "Checking AI score before/after")
b, a = S.detector_pct_text(text), S.detector_pct_text(out_text)
rep.meta.update(name="Pasted text", words=len(text.split()), before=b, after=a, output=path,
engine="CPU" if "CPU" in summary else "GPU")
panel = suite_panel(b, a, text, out_text)
print(f"[humanizer] {summary}", flush=True)
return out_text, path, dl, H.status_done(summary), panel
def _hum_docx_core(path, temp, min_words, mode, full_ai, vocab, rep):
H.FULL_AI.set(bool(full_ai)); H.VOCAB_TARGET.set(float(vocab or 0))
H.PROOFREAD_FOLLOWS.set(AUTO_PROOFREAD)
t0 = time.time()
S.G.start_account()
out, dl, summary, _, preview = H._docx_core(path, temp, min_words, mode, rep)
pr = _auto_proofread(out, path, 0.80, 0.50, rep)
if pr and not pr.get("rejected"):
out = pr["path"]
dl = H.download_button(out)
summary = summary.split("\n\n")[0] + _proof_line(pr)
rep(0.97, "Checking AI score before/after")
from docx import Document
orig = "\n".join(p.text for p in Document(path).paragraphs)
new = "\n".join(p.text for p in Document(out).paragraphs)
b, a = S.detector_pct_docx(path), S.detector_pct_docx(out)
rep.meta.update(name=os.path.basename(str(path)), words=len(orig.split()), before=b, after=a, output=out,
engine="CPU" if "CPU" in summary else "GPU")
panel = suite_panel(b, a, orig, new)
_remember(out, ours=a, ours_before=b)
print(f"[humanizer] {summary}", flush=True)
charge = _payg_lock(out, "Humanizer", os.path.basename(str(path)), len(orig.split()), time.time() - t0, rep)
if charge:
return None, "", H.status_done(summary) + pay_popup_html(charge, "hum"), panel, ""
return out, dl, H.status_done(summary), panel, preview
def humanize_text(text, temp, min_words, mode, full_ai, vocab, user, request: gr.Request = None):
if not (text or "").strip():
raise gr.Error("Please paste some text to paraphrase.")
yield from _stream(_hum_text_core, (text, temp, min_words, mode, P.paste_limit(user), bool(full_ai), vocab), 5, 3, user,
request=request, tool="Humanizer", paste=text)
def humanize_docx(path, temp, min_words, mode, full_ai, vocab, user, request: gr.Request = None):
yield from _stream(_hum_docx_core, (path, temp, min_words, mode, bool(full_ai), vocab), 5, 2, user, True, request, tool="Humanizer",
words=P.doc_words(path) if path else None)
# =====================================================================
# Auth events
# =====================================================================
LOCKS, OPENS = [], [] # filled while building the UI
def _auth_updates(user, msg=""):
admin = A.is_admin(user)
signed = bool(user)
base = [
user,
gr.update(visible=not signed), # auth_view
gr.update(visible=signed), # profile_view
_profile(user), # profile_html
_userbar(user), # user_bar
msg, # auth_msg
gr.update(visible=admin), # admin_tab
A.admin_stats(user) if admin else "",
A.admin_table(user) if admin else None,
gr.update(visible=not signed), # top sign-in button
gr.update(visible=signed), # top log-out button
gr.update(visible=not P.state(user)["paid"]), # top upgrade button (guests + free users)
gr.update(visible=admin), # top admin-panel button
]
base += [gr.update(visible=not signed) for _ in LOCKS]
base += [gr.update(visible=signed) for _ in OPENS]
return base
SIGNED_OUT = "signed-out" # BrowserState ignores empty values, so logout stores a marker
def _fail(err):
return ([gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), A._msg(err)]
+ [gr.update()] * (6 + 2 * len(LOCKS)) + [gr.update()])
def do_login(email, password):
user, err = A.check_user(email, password)
if err:
return _fail(err)
msg = A._msg(f"Welcome back, {html.escape(user['name'])}! Upload is now unlocked.", ok=True)
return _auth_updates(user, msg) + [A.make_token(user)]
def do_signup(name, email, password, confirm):
if A.verify_signups(): # email confirmed first: see do_signup_start / do_signup_verify
return _fail("Please press Create account to get your email code first.")
user, err = A.create_user(name, email, password, confirm)
if err:
return _fail(err)
msg = (A._msg("Account created — document upload is now unlocked.", ok=True)
+ A.key_html(A.new_recovery_key(user["email"])))
M.send_welcome(user["email"], user["name"], P.FREE_TRIAL_DOCS)
return _auth_updates(user, msg) + [A.make_token(user)]
def do_signup_start(name, email, password, confirm):
"""Sign-up step 1 (email set up): the code goes to the address; the code box appears."""
ok, msg = A.start_signup(name, email, password, confirm)
return A._msg(msg, ok=ok), gr.update(visible=ok)
def do_signup_verify(email, code):
"""Sign-up step 2: the right code creates the account and signs the customer in."""
user, err = A.confirm_signup(email, code)
if err:
return _fail(err)
msg = (A._msg("Email confirmed and account created: document upload is now unlocked.", ok=True)
+ A.key_html(A.new_recovery_key(user["email"])))
M.send_welcome(user["email"], user["name"], P.FREE_TRIAL_DOCS)
return _auth_updates(user, msg) + [A.make_token(user)]
def do_send_code(email):
ok, msg = A.request_reset(email)
return A._msg(msg, ok=ok)
def do_reset_key(email, key, pw, pw2):
user, new_key, err = A.reset_any(email, key, pw, pw2)
if err:
return _fail(err)
msg = A._msg("Password changed — you are now signed in.", ok=True)
if new_key:
msg += A.key_html(new_key, "Your old recovery key is used up. Your NEW recovery key")
return _auth_updates(user, msg) + [A.make_token(user)]
def do_admin_request(email, contact):
ok, msg = A.request_admin_reset(email, contact)
return A._msg(msg, ok=ok)
def do_change_pw(user, old, new, new2):
ok, msg = A.change_password(user, old, new, new2)
return A._msg(msg, ok=ok), gr.update(value=""), gr.update(value=""), gr.update(value="")
def do_new_key(user):
if not user:
return A._msg("Please sign in first.")
return A.key_html(A.new_recovery_key(user["email"]), "Your new recovery key (the old one no longer works)")
def do_logout():
return _auth_updates(None, A._msg("You have been logged out.", ok=True)) + [SIGNED_OUT]
def restore_session(token):
"""On page load: sign the visitor back in from the token kept in their browser."""
user = A.user_from_token(token)
if not user:
return _auth_updates(None, "") + [gr.update()]
return _auth_updates(user, "") + [token]
# =====================================================================
# UI
# =====================================================================
CSS = f"""
:root {{ --cs-navy:{BRAND['navy']}; --cs-purple:{BRAND['purple']}; --cs-magenta:{BRAND['magenta']};
--cs-sky:{BRAND['sky']}; --cs-rainbow:{RAINBOW}; }}
.gradio-container {{ max-width:1200px !important; margin:0 auto !important;
font-family:'Poppins', ui-sans-serif, system-ui, sans-serif !important; }}
body, .gradio-container {{ background:#f6f7fb !important; }}
footer, #huggingface-space-header, .show-api {{ display:none !important; }}
/* header */
.cs-header {{ display:flex; align-items:center; gap:20px; padding:18px 24px; border-radius:20px 20px 0 0;
background:#fff; box-shadow:0 8px 30px rgba(7,34,139,.08); }}
.cs-header img {{ width:74px; height:74px; border-radius:50%; flex:none; box-shadow:0 4px 14px rgba(0,0,0,.12); }}
.cs-header h1 {{ margin:0; font-size:27px; font-weight:800; color:var(--cs-navy); letter-spacing:-.3px; line-height:1.15; }}
.cs-header h1 span {{ color:var(--cs-magenta) !important; }}
.cs-header p {{ margin:4px 0 0; color:#555; font-size:14px; }}
@media (max-width:640px) {{ .cs-header {{ flex-direction:column; text-align:center; }} .cs-header h1 {{ font-size:22px; }} }}
/* account strip under header */
#cs-strip {{ background:#fff; border-radius:0 0 20px 20px; padding:4px 18px 8px; margin-top:-16px !important;
box-shadow:0 8px 30px rgba(7,34,139,.08); position:relative; align-items:center; }}
#cs-strip::before {{ content:""; position:absolute; left:0; right:0; top:0; height:4px; background:var(--cs-rainbow); }}
.cs-userbar {{ display:flex; align-items:center; padding:8px 2px; color:#444; font-size:14px; }}
.cs-userbar small {{ color:#888; }}
.cs-topbtn {{ max-width:150px !important; min-width:110px !important; }}
/* navigation bar (top-level tabs) */
#cs-nav > .tab-wrapper, #cs-nav > div:first-child {{ background:linear-gradient(90deg,var(--cs-navy),var(--cs-purple) 65%,var(--cs-magenta));
border-radius:14px; padding:6px; margin:14px 0 10px; border:none !important;
height:auto !important; min-height:0 !important; overflow:visible !important; }}
#cs-nav > .tab-wrapper button[role="tab"], #cs-nav > div:first-child > button[role="tab"],
#cs-nav > .tab-wrapper .tab-container > button {{ color:#e9e9ff !important; font-weight:700 !important; font-size:14.5px !important;
border:none !important; border-radius:10px !important; padding:9px 11px !important; background:transparent !important;
white-space:nowrap !important; flex:0 0 auto !important; }}
#cs-nav > .tab-wrapper button[role="tab"].selected, #cs-nav > div:first-child > button[role="tab"].selected,
#cs-nav > .tab-wrapper .tab-container > button.selected {{ background:#fff !important; color:var(--cs-navy) !important;
box-shadow:0 3px 10px rgba(0,0,0,.18); }}
@media (hover:hover) {{ #cs-nav > .tab-wrapper button[role="tab"]:not(.selected):hover {{ background:rgba(255,255,255,.14) !important; }} }}
#cs-nav > .tab-wrapper .tab-container::after {{ display:none !important; }}
/* tabs that don't fit go into Gradio's "…" menu: keep it visible and readable */
#cs-nav > .tab-wrapper {{ display:flex; align-items:center; }}
#cs-nav > .tab-wrapper .overflow-menu {{ display:inline-flex !important; position:relative; flex:none; margin-left:4px; }}
#cs-nav > .tab-wrapper .overflow-menu > button {{ background:rgba(255,255,255,.18) !important; color:#fff !important;
border-radius:12px !important; padding:9px 12px !important; }}
#cs-nav > .tab-wrapper .overflow-menu > button svg {{ color:#fff; }}
#cs-nav > .tab-wrapper .overflow-dropdown {{ background:#fff !important; border-radius:12px !important; z-index:50;
box-shadow:0 10px 30px rgba(7,34,139,.25) !important; padding:6px !important; right:0; }}
#cs-nav > .tab-wrapper .overflow-dropdown button {{ color:var(--cs-navy) !important; font-weight:700 !important;
white-space:nowrap; padding:8px 14px !important; border-radius:8px !important; }}
#cs-nav > .tab-wrapper .overflow-dropdown button:hover {{ background:#eef1fb !important; }}
#cs-nav > .tab-wrapper .tab-container {{ height:auto !important; min-height:0 !important; flex-wrap:wrap !important;
gap:4px; overflow:visible !important; justify-content:center; align-items:center; width:100%; }}
#cs-nav > .tab-wrapper, #cs-nav > .tab-wrapper .tab-container {{ border-bottom:none !important; box-shadow:none; }}
#cs-nav > .tab-wrapper .tab-container > button::after, #cs-nav > .tab-wrapper button[role="tab"]::after {{ display:none !important; }}
/* Gradio hides tabs that don't fit into a "…" menu; keep every tab visible inside the bar instead */
#cs-nav > .tab-wrapper .tab-container > button[role="tab"] {{ visibility:visible !important; position:static !important; }}
.sub-tabs button[role="tab"] {{ font-weight:600 !important; }}
.sub-tabs > .tab-wrapper, .cs-auth .tab-wrapper {{ height:auto !important; }}
.sub-tabs > .tab-wrapper .tab-container, .cs-auth .tab-wrapper .tab-container {{ flex-wrap:wrap !important; height:auto !important; }}
.sub-tabs > .tab-wrapper .overflow-menu, .cs-auth .tab-wrapper .overflow-menu {{ display:none !important; }}
.sub-tabs button[role="tab"].selected {{ color:var(--cs-navy) !important; border-color:var(--cs-magenta) !important; }}
.cs-intro {{ background:#fff; border:1px solid #eceef6; border-radius:14px; padding:12px 16px; color:#444;
font-size:14px; margin-bottom:8px; }}
.cs-intro b {{ color:var(--cs-navy); }}
.cs-flow {{ display:flex; gap:8px; flex-wrap:wrap; margin-top:8px; }}
.cs-flow span {{ background:#f1f3fb; border-radius:99px; padding:4px 12px; font-size:12.5px; font-weight:600; color:var(--cs-navy); }}
.cs-card {{ background:#fff !important; border-radius:18px !important; padding:18px !important;
box-shadow:0 6px 24px rgba(7,34,139,.06) !important; border:1px solid #eceef6 !important; }}
button.primary {{ background:linear-gradient(90deg,var(--cs-navy),var(--cs-purple) 60%,var(--cs-magenta)) !important;
color:#fff !important; border:none !important; font-weight:700 !important; font-size:16px !important;
border-radius:12px !important; box-shadow:0 6px 18px rgba(71,20,139,.28) !important; }}
button.primary:hover {{ filter:brightness(1.08); }}
.cs-lock {{ text-align:center; padding:34px 20px 18px; }}
.cs-lock-ico {{ font-size:44px; }}
.cs-lock-t {{ font-weight:800; font-size:20px; color:var(--cs-navy); margin-top:6px; }}
.cs-lock-s {{ color:#666; font-size:14px; margin:6px auto 0; max-width:520px; }}
.cs-lockbtn {{ max-width:260px !important; margin:0 auto 22px !important; }}
.cs-scores {{ display:grid; grid-template-columns:repeat(4,1fr); gap:14px; margin-top:6px; }}
.cs-scores.cs-cols-2 {{ grid-template-columns:repeat(2,1fr); }}
.cs-scores.cs-cols-3 {{ grid-template-columns:repeat(3,1fr); }}
.cs-scores.cs-cols-5 {{ grid-template-columns:repeat(5,1fr); }}
@media (max-width:1100px) {{ .cs-scores.cs-cols-5 {{ grid-template-columns:repeat(3,1fr); }} }}
@media (max-width:820px) {{ .cs-scores.cs-cols-3, .cs-scores.cs-cols-5 {{ grid-template-columns:repeat(2,1fr); }} }}
@media (max-width:820px) {{ .cs-scores {{ grid-template-columns:repeat(2,1fr); }} }}
.cs-scores.cs-empty {{ display:block; text-align:center; color:#888; padding:22px; border:2px dashed #e3e6f2; border-radius:16px; }}
.cs-gauge {{ background:#fff; border:1px solid #eceef6; border-radius:16px; padding:16px 10px; text-align:center;
box-shadow:0 4px 16px rgba(7,34,139,.05); }}
.cs-ring {{ width:104px; height:104px; margin:0 auto 10px; border-radius:50%;
background:conic-gradient(var(--c) calc(var(--p)*1%), #edf0f7 0); display:grid; place-items:center; }}
.cs-ring-inner {{ width:80px; height:80px; border-radius:50%; background:#fff; display:grid; place-items:center; }}
.cs-ring-inner span {{ font-size:22px; font-weight:800; color:#222; }}
.cs-gauge-title {{ font-weight:700; color:var(--cs-navy); font-size:14px; }}
.cs-chip {{ display:inline-block; margin:6px 0; padding:2px 12px; border-radius:99px; font-size:12px; font-weight:700; }}
.cs-gauge-cap {{ font-size:11.5px; color:#777; line-height:1.35; }}
.cs-disclaimer {{ margin-top:12px; font-size:12px; color:#666; background:#fff8ec; border-left:4px solid {BRAND['orange']};
padding:10px 14px; border-radius:10px; }}
.cs-count {{ text-align:right; font-size:12px; margin-top:-6px; }}
.cs-status {{ padding:16px 18px; border-radius:14px; font-size:14.5px; line-height:1.5; }}
.cs-idle {{ color:#666; }}
.cs-wait {{ color:#999; font-size:13px; min-height:64px; padding-top:40px; text-align:center; }}
.cs-done {{ background:#f1fbf1; border-left:4px solid {BRAND['green']}; color:#234; }}
.cs-err {{ background:#fff3f3; border-left:4px solid {BRAND['red']}; color:#8a1c1c; }}
.cs-run {{ text-align:center; background:#fff; border:1px solid #eceef6; }}
.cs-spin-logo {{ width:64px; height:64px; margin:4px auto 12px;
background:url("{FAVICON_SRC}") center/contain no-repeat; animation:cs-spin 1.4s linear infinite; }}
.cs-st-text {{ font-weight:600; color:var(--cs-navy); margin-bottom:10px; }}
.cs-bar {{ height:10px; border-radius:99px; background:#edf0f7; overflow:hidden; }}
.cs-bar > div {{ height:100%; border-radius:99px; background:var(--cs-rainbow); transition:width .5s ease; }}
.cs-st-meta {{ margin-top:8px; font-size:12px; color:#777; }}
@keyframes cs-spin {{ to {{ transform:rotate(360deg); }} }}
.cs-dl {{ display:block; overflow-wrap:anywhere; word-break:break-word; text-align:center; margin-top:10px; padding:12px 16px; border-radius:12px; font-weight:700;
color:#fff !important; text-decoration:none !important;
background:linear-gradient(90deg,var(--cs-navy),var(--cs-purple) 60%,var(--cs-magenta)); }}
.cs-settings {{ background:#f8f9fd !important; border:1px solid #eceef6 !important; border-radius:14px !important;
padding:12px 14px !important; margin:10px 0 !important; }}
.cs-settings-title {{ font-weight:700; color:var(--cs-navy); font-size:15px; margin:0 0 4px; }}
.cs-stats {{ display:grid; grid-template-columns:repeat(auto-fill,minmax(165px,1fr)); gap:12px; margin-bottom:10px; }}
@media (max-width:820px) {{ .cs-stats {{ grid-template-columns:repeat(2,1fr); }} }}
.cs-stat {{ background:#fff; border:1px solid #eceef6; border-radius:14px; padding:14px; text-align:center; }}
.cs-stat .n {{ font-size:22px; font-weight:800; color:var(--cs-navy); line-height:1.25; overflow-wrap:anywhere; }}
.cs-stat .l {{ font-size:12px; color:#777; margin-top:2px; }}
.cs-auth {{ max-width:480px; margin:10px auto 0 !important; }}
.cs-auth-title {{ text-align:center; font-weight:800; font-size:22px; color:var(--cs-navy); margin:4px 0 2px; }}
.cs-auth-sub {{ text-align:center; color:#666; font-size:13.5px; margin-bottom:10px; }}
.cs-authmsg {{ padding:10px 14px; border-radius:10px; font-size:13.5px; margin-top:6px; }}
.cs-ok {{ background:#f1fbf1; color:#1e5b1e; border-left:4px solid {BRAND['green']}; }}
.cs-bad {{ background:#fff3f3; color:#8a1c1c; border-left:4px solid {BRAND['red']}; }}
.cs-prof {{ display:grid; gap:8px; margin-top:12px; }}
.cs-prof div {{ display:flex; justify-content:space-between; border-bottom:1px solid #f0f1f7; padding:6px 2px; }}
.cs-prof span {{ color:#777; }}
.cs-prices {{ display:grid; grid-template-columns:repeat(auto-fit,minmax(200px,1fr)); gap:14px; margin:12px 0; }}
.cs-price {{ position:relative; background:#fff; border:1px solid #e3e6f2; border-radius:18px; padding:20px 18px 14px;
box-shadow:0 6px 24px rgba(7,34,139,.06); }}
.cs-price ul {{ margin:10px 0 0; padding-left:18px; color:#444; font-size:13.5px; line-height:1.7; }}
.cs-featured {{ border:2px solid var(--cs-magenta); box-shadow:0 10px 30px rgba(233,46,127,.18); }}
.cs-pop {{ position:absolute; top:-12px; right:16px; background:var(--cs-magenta); color:#fff; font-size:11.5px;
font-weight:700; padding:3px 10px; border-radius:99px; }}
.cs-pname {{ font-weight:800; color:var(--cs-navy); font-size:17px; }}
.cs-pval {{ font-size:30px; font-weight:800; color:#222; margin-top:6px; }}
.cs-pval small {{ font-size:13px; font-weight:600; color:#777; }}
.cs-inr {{ font-size:13px; color:#555; }}
.cs-single {{ background:#f8f9fd; border:1px dashed #cfd5ea; border-radius:14px; padding:12px 16px; color:#333; font-size:14px; }}
.cs-promo {{ background:linear-gradient(90deg,#fff4fa,#f3f0ff); border:1px solid #f3c4dc; border-radius:14px; padding:10px 16px;
color:#6b1a45; font-weight:600; font-size:14px; margin-top:6px; }}
.cs-nudge {{ margin-top:10px; background:#fbf7ff; border-left:4px solid var(--cs-purple); border-radius:10px; padding:10px 14px;
color:#3a2360; font-size:13.5px; }}
.cs-plan {{ display:inline-block; margin-left:6px; padding:1px 9px; border-radius:99px; font-size:11.5px; font-weight:700;
background:#eef0f7; color:#555; }}
.cs-plan-paid {{ background:var(--cs-magenta); color:#fff; }}
.cs-plan-admin {{ background:var(--cs-navy); color:#fff; }}
.cs-upbtn {{ background:linear-gradient(90deg,var(--cs-purple),var(--cs-magenta)) !important; color:#fff !important; border:none !important; font-weight:700 !important; }}
/* ---------------- phones & small tablets ---------------- */
@media (max-width: 700px) {{
body {{ overflow-x:hidden !important; }}
.gradio-container {{ min-width:0 !important; width:100% !important; max-width:100% !important;
padding-left:10px !important; padding-right:10px !important; }}
.cs-header {{ flex-direction:row !important; text-align:left !important; padding:12px 14px; gap:12px; border-radius:16px 16px 0 0; }}
.cs-header img {{ width:48px; height:48px; }}
.cs-header h1 {{ font-size:20px !important; }}
.cs-header p {{ font-size:12.5px; margin-top:2px; }}
#cs-strip {{ flex-wrap:wrap !important; gap:6px !important; padding:4px 12px 10px !important; border-radius:0 0 16px 16px; }}
#cs-strip > div:first-child {{ flex:1 1 100% !important; min-width:100% !important; }}
.cs-userbar {{ font-size:13px; padding:6px 0 2px; line-height:1.45; }}
.cs-topbtn {{ flex:1 1 0 !important; min-width:0 !important; max-width:none !important; }}
/* navigation = compact rows inside the coloured bar (all tabs visible) */
#cs-nav {{ max-width:100% !important; min-width:0 !important; }}
main, .wrap {{ max-width:100% !important; min-width:0 !important; }}
#cs-nav > .tab-wrapper {{ height:auto !important; max-width:100% !important; margin:10px 0 8px !important;
padding:5px !important; border-radius:12px; }}
#cs-nav > .tab-wrapper .tab-container {{ flex-wrap:wrap !important; height:auto !important; gap:3px !important;
justify-content:center; }}
#cs-nav > .tab-wrapper button[role="tab"], #cs-nav > .tab-wrapper .tab-container > button {{
padding:7px 9px !important; font-size:12.5px !important; white-space:nowrap !important; flex:0 0 auto; }}
/* every multi-column row stacks */
.row:not(#cs-strip) {{ flex-direction:column !important; gap:10px !important; }}
.row:not(#cs-strip) > * {{ min-width:100% !important; width:100% !important; }}
.cs-card {{ padding:12px !important; border-radius:14px !important; }}
.block, .form, .column, .gap, .tabitem, .tabs {{ min-width:0 !important; max-width:100% !important; }}
.table-wrap, .table-container {{
max-width:100% !important; overflow-x:auto !important; -webkit-overflow-scrolling:touch; }}
.table-wrap table {{ font-size:12px !important; }}
.vega-embed, .vega-embed canvas, .vega-embed svg {{ max-width:100% !important; }}
.cs-intro {{ font-size:13px; padding:10px 12px; }}
.cs-flow span {{ font-size:11.5px; padding:3px 9px; }}
.sub-tabs button[role="tab"] {{ font-size:13px !important; padding:6px 8px !important; }}
.cs-scores {{ grid-template-columns:repeat(2,1fr) !important; gap:8px; }}
.cs-gauge {{ padding:10px 6px; }}
.cs-ring {{ width:78px; height:78px; }}
.cs-ring-inner {{ width:60px; height:60px; }}
.cs-ring-inner span {{ font-size:17px; }}
.cs-stats {{ grid-template-columns:repeat(2,1fr) !important; gap:8px; }}
.cs-stat .n {{ font-size:18px; }}
.cs-auth {{ max-width:100% !important; margin-top:6px !important; }}
.cs-prices {{ grid-template-columns:1fr !important; }}
.cs-pval {{ font-size:26px; }}
.cs-dl {{ font-size:13.5px; padding:11px 12px; }}
.cs-status {{ font-size:13.5px; padding:12px 14px; }}
button.primary {{ font-size:15px !important; }}
.cs-footer {{ font-size:12px; padding:12px 4px; }}
}}
.g-diff {{ background:#fff; border:1px solid #eceef6; border-radius:12px; padding:12px 16px; line-height:1.8;
color:#222; font-size:14.5px; white-space:pre-wrap; }}
.g-diff ol {{ margin:0; padding-left:20px; white-space:normal; }} .g-diff li {{ margin-bottom:8px; }}
.g-del {{ color:#b42318; background:#fde8e8; text-decoration:line-through; border-radius:3px; padding:0 2px; }}
.g-ins {{ color:#146c2e; background:#e3f6e5; text-decoration:none; border-radius:3px; padding:0 2px; font-weight:600; }}
.cs-footer {{ text-align:center; margin-top:18px; padding:16px; color:#666; font-size:13px; }}
.cs-footer .bar {{ height:3px; width:220px; margin:0 auto 12px; border-radius:3px; background:var(--cs-rainbow); }}
.cs-footer a {{ color:var(--cs-navy); font-weight:700; text-decoration:none; }}
"""
FORCE_LIGHT = """
() => {
const url = new URL(window.location);
if (url.searchParams.get('__theme') !== 'light') {
url.searchParams.set('__theme', 'light'); window.location.href = url.href; return;
}
const hide = () => { const h = document.getElementById('huggingface-space-header');
if (h) h.style.setProperty('display', 'none', 'important'); };
hide(); new MutationObserver(hide).observe(document.documentElement, { childList: true, subtree: true });
const ICON = "__FAVICON__";
if (ICON) { document.querySelectorAll("link[rel*='icon']").forEach(l => l.remove());
const link = document.createElement('link'); link.rel = 'icon'; link.type = 'image/png'; link.href = ICON;
document.head.appendChild(link); }
}
""".replace("__FAVICON__", FAVICON_SRC)
_logo = f'<img src="{LOGO_SRC}" alt="logo">' if LOGO_SRC else ""
_title = html.escape(APP_NAME)
_title = _title.rsplit(" ", 1)[0] + f" <span>{_title.rsplit(' ', 1)[-1]}</span>" if " " in _title else _title
HEADER = f"""
<div class="cs-header">{_logo}
<div><h1>{_title}</h1>
<p>Detect AI-written passages and humanize them — sentence by sentence, with your Word layout kept intact.</p></div>
</div>"""
BRAND_HTML = (f'<div class="cs-brand">{_logo}<div><div class="t">{_title}</div>'
'<div class="s">AI detection and humanizing for academic writing</div></div></div>')
FOOTER = f"""
<div class="cs-footer"><div class="bar"></div>
© CS Software Solutions · Tri-Crest Press &nbsp;|&nbsp; <a href="{SITE_URL}" target="_blank">cssoftwaresolutions.in</a><br>
{SP.footer_contact_html()}<br>
Rewritten output should be checked for accuracy and properly cited before submission.
</div>"""
# Unscoped CSS (Gradio prefixes the css= string, so the outer containers are pinned here)
HEAD = """<style>
""" + SP.CSS + """
#cs-go-pricing, #cs-go-account, #cs-go-formatter, #cs-go-finder, #cs-stop-srv, #cs-go-rephrase-text, #cs-go-rephrase-doc, #cs-go-unlock-smart, #cs-go-unlock-hum, .cs-hidden-nav { display: none !important; }
a.cs-up-go { text-decoration: none !important; display: inline-block; }
.cs-rtab { margin-top: 14px; background: #fff; border: 1px solid #e3e6f2; border-radius: 14px; padding: 10px 14px; }
.cs-rtab summary { cursor: pointer; font-weight: 700; color: #07228B; padding: 4px 0; }
.cs-rt-wrap { overflow-x: auto; margin-top: 8px; }
.cs-rtab table { width: 100%; border-collapse: collapse; font-size: 13.5px; line-height: 1.55; }
.cs-rtab th { text-align: left; font-size: 12px; text-transform: uppercase; letter-spacing: .06em; color: #6b7090;
border-bottom: 1px solid #e3e6f2; padding: 6px 8px; }
.cs-rtab td { vertical-align: top; border-bottom: 1px solid #f0f2f8; padding: 8px; color: #2a2f45; min-width: 220px; }
.cs-rtab td.n { min-width: 0; width: 28px; color: #9aa0b8; font-variant-numeric: tabular-nums; }
.cs-rtab .lv { display: inline-block; font-size: 11.5px; font-weight: 700; border-radius: 99px; padding: 1px 9px;
margin-bottom: 4px; color: #1d2559; }
.cs-rt-ok { display: inline-block; margin-top: 4px; font-size: 11.5px; font-weight: 700; color: #2f8a3b; }
.cs-rt-kept { display: inline-block; margin-top: 4px; font-size: 11.5px; font-weight: 700; color: #b36b00; }
.cs-rt-likely { display: block; margin-top: 5px; font-size: 12px; font-weight: 600; color: #b3261e; background: #fdecec;
border-radius: 8px; padding: 4px 8px; }
/* Stop & reset: floating button shown only while a job is running */
#cs-stop-float { position: fixed; left: 50%; transform: translateX(-50%);
bottom: calc(18px + env(safe-area-inset-bottom, 0px)); z-index: 10001; display: none; align-items: center; gap: 6px;
padding: 11px 20px; border: none; border-radius: 99px; cursor: pointer; font: 700 15px/1 Poppins, system-ui, sans-serif;
color: #fff; background: linear-gradient(90deg, #E12B41, #E92E7F); box-shadow: 0 8px 24px rgba(225,43,65,.35); }
#cs-stop-float:hover { filter: brightness(1.06); }
#cs-stop-float:disabled { opacity: .7; cursor: default; }
.cs-stop-note { position: fixed; left: 50%; transform: translateX(-50%); bottom: calc(18px + env(safe-area-inset-bottom, 0px));
z-index: 10001; background: #fff; color: #1d2559; border-left: 5px solid #07228B; border-radius: 14px; padding: 12px 18px;
box-shadow: 0 10px 30px rgba(7,34,139,.18); font: 600 14px/1.4 Poppins, system-ui, sans-serif; max-width: 92vw; }
/* a dropped connection while the job keeps running on the server: don't show Gradio's stuck red "Error" badge */
.block:has(.cs-run) > .wrap .error, .block:has(.cs-q) > .wrap .error,
.block:has(.cs-run) > .wrap.error, .block:has(.cs-q) > .wrap.error { display: none !important; }
.cs-upsell { position: fixed; inset: 0; z-index: 10000; display: flex; align-items: center; justify-content: center;
padding: 16px; font-family: Poppins, ui-sans-serif, sans-serif; }
.cs-up-backdrop { position: absolute; inset: 0; background: rgba(12, 16, 45, .55); backdrop-filter: blur(4px);
-webkit-backdrop-filter: blur(4px); animation: csFade .3s ease both; }
.cs-up-card { position: relative; width: min(440px, 100%); background: #fff; border-radius: 22px;
padding: 30px 26px 22px; text-align: center; color: #1d2040; overflow: hidden;
box-shadow: 0 30px 80px rgba(7, 34, 139, .35); animation: csPop .55s cubic-bezier(.2, 1.3, .35, 1) both; }
.cs-up-ring { position: absolute; inset: 0; border-radius: 22px; padding: 2px; pointer-events: none;
background: conic-gradient(from var(--a, 0deg), #07228B, #47148B, #E92E7F, #50B4EF, #07228B);
-webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
-webkit-mask-composite: xor; mask-composite: exclude; animation: csSpin 4s linear infinite; }
@property --a { syntax: '<angle>'; initial-value: 0deg; inherits: false; }
.cs-up-icon { font-size: 46px; line-height: 1; display: inline-block; margin-bottom: 10px;
animation: csFloat 2.4s ease-in-out infinite; filter: drop-shadow(0 8px 14px rgba(233, 46, 127, .35)); }
.cs-up-title { font-size: 21px; font-weight: 700; margin-bottom: 8px;
background: linear-gradient(90deg, #07228B, #47148B 55%, #E92E7F); -webkit-background-clip: text;
background-clip: text; color: transparent; }
.cs-up-text { font-size: 14.5px; line-height: 1.6; color: #444a6b; margin-bottom: 14px; }
.cs-up-perks { list-style: none; padding: 0; margin: 0 0 18px; display: grid; grid-template-columns: 1fr 1fr;
gap: 8px; text-align: left; }
.cs-up-perks li { font-size: 13px; background: #f5f3ff; border-radius: 10px; padding: 8px 10px; color: #2b2f55;
opacity: 0; animation: csRise .45s ease forwards; }
.cs-up-perks li:nth-child(1) { animation-delay: .25s } .cs-up-perks li:nth-child(2) { animation-delay: .35s }
.cs-up-perks li:nth-child(3) { animation-delay: .45s } .cs-up-perks li:nth-child(4) { animation-delay: .55s }
.cs-up-perks span { color: #E92E7F; font-weight: 800; margin-right: 6px; }
.cs-up-actions { display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; }
.cs-up-go { border: 0; cursor: pointer; color: #fff; font-weight: 700; font-size: 15px; padding: 12px 22px;
border-radius: 12px; background: linear-gradient(90deg, #07228B, #47148B 60%, #E92E7F); background-size: 200% 100%;
box-shadow: 0 10px 24px rgba(233, 46, 127, .35); animation: csShine 3s ease-in-out infinite; }
.cs-up-go:hover { transform: translateY(-1px); }
.cs-up-later { border: 1px solid #e3e5f2; background: #fff; color: #555b7c; cursor: pointer; font-size: 14px;
padding: 12px 18px; border-radius: 12px; }
.cs-up-x { position: absolute; top: 10px; right: 12px; border: 0; background: transparent; font-size: 24px;
color: #9aa0bd; cursor: pointer; line-height: 1; }
.cs-sp { position: absolute; font-style: normal; color: #E92E7F; opacity: 0; animation: csTwinkle 2.6s ease-in-out infinite; }
.cs-sp.s1 { top: 18px; left: 26px; } .cs-sp.s2 { top: 60px; right: 40px; color: #50B4EF; animation-delay: .7s; }
.cs-sp.s3 { bottom: 70px; left: 18px; color: #47148B; animation-delay: 1.3s; }
.cs-sp.s4 { bottom: 26px; right: 22px; animation-delay: 1.9s; }
.cs-upsell.cs-out .cs-up-card { animation: csPopOut .22s ease forwards; }
.cs-upsell.cs-out .cs-up-backdrop { animation: csFadeOut .22s ease forwards; }
.cs-up-link { cursor: pointer; font-weight: 600; color: #47148B; text-decoration: underline; margin-left: 4px; }
@keyframes csFade { from { opacity: 0 } to { opacity: 1 } }
@keyframes csFadeOut { to { opacity: 0 } }
@keyframes csPop { from { opacity: 0; transform: translateY(24px) scale(.85) } to { opacity: 1; transform: none } }
@keyframes csPopOut { to { opacity: 0; transform: translateY(12px) scale(.92) } }
@keyframes csSpin { to { --a: 360deg } }
@keyframes csFloat { 0%, 100% { transform: translateY(0) rotate(-4deg) } 50% { transform: translateY(-8px) rotate(4deg) } }
@keyframes csRise { from { opacity: 0; transform: translateY(8px) } to { opacity: 1; transform: none } }
@keyframes csShine { 0%, 100% { background-position: 0% 0 } 50% { background-position: 100% 0 } }
@keyframes csTwinkle { 0%, 100% { opacity: 0; transform: scale(.6) } 50% { opacity: .9; transform: scale(1.1) } }
@media (max-width: 480px) { .cs-up-perks { grid-template-columns: 1fr; } .cs-up-card { padding: 26px 18px 18px; } }
@media (prefers-reduced-motion: reduce) { .cs-upsell * { animation: none !important; opacity: 1 !important; } }
.cs-ready-toast { position: fixed; left: 50%; bottom: 26px; transform: translateX(-50%); z-index: 10001;
display: flex; gap: 12px; align-items: center; background: #fff; border-radius: 16px; padding: 14px 18px;
min-width: 280px; max-width: min(92vw, 460px); box-shadow: 0 18px 50px rgba(7, 34, 139, .28);
border: 1px solid #e7e9f5; font-family: Poppins, ui-sans-serif, sans-serif; cursor: pointer;
animation: csRtIn .55s cubic-bezier(.2, 1.3, .35, 1) both; }
.cs-ready-toast::before { content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 5px; border-radius: 16px 0 0 16px;
background: linear-gradient(#07228B, #47148B, #E92E7F); }
.cs-rt-icon { font-size: 26px; animation: csFloat 2.4s ease-in-out infinite; }
.cs-rt-title { font-weight: 700; color: #07228B; font-size: 15px; }
.cs-rt-body { font-size: 13px; color: #555b7c; margin-top: 2px; white-space: pre-line; }
.cs-ready-toast.cs-rt-out { animation: csRtOut .35s ease forwards; }
@keyframes csRtIn { from { opacity: 0; transform: translate(-50%, 40px) scale(.9) } to { opacity: 1; transform: translate(-50%, 0) } }
@keyframes csRtOut { to { opacity: 0; transform: translate(-50%, 20px) } }
#cs-push-sub, #cs-push-save, #cs-push-test { display: none !important; }
.cs-fullai { border: 1.5px dashed #E92E7F !important; border-radius: 12px !important; background: #fff5fa !important; }
.cs-fullai label span { font-weight: 700 !important; color: #47148B !important; }
.cs-fullai input[type=checkbox] { width: 22px !important; height: 22px !important; accent-color: #E92E7F;
border: 2px solid #E92E7F !important; border-radius: 6px !important; cursor: pointer; }
.cs-fullai input[type=checkbox]:checked { background-color: #E92E7F !important; }
/* ---- queue card (replaces Gradio's plain "queue: 2/5" text) ---- */
.block:has(.cs-q) > .wrap { opacity: 0 !important; pointer-events: none !important; }
.html-container.pending:has(.cs-q), .pending:has(.cs-q) { opacity: 1 !important; }
.cs-q { text-align: center; background: #fff; border: 1px solid #eceef6; border-radius: 16px; padding: 22px 18px 18px;
animation: csQIn .45s ease both; font-family: Poppins, ui-sans-serif, sans-serif; }
.cs-q-orbit { position: relative; width: 58px; height: 58px; margin: 0 auto 12px; animation: csQSpin 1.6s linear infinite; }
.cs-q-orbit i { position: absolute; width: 14px; height: 14px; border-radius: 50%; top: 22px; left: 22px;
background: #07228B; transform: rotate(0deg) translateY(-20px); box-shadow: 0 4px 10px rgba(7,34,139,.25); }
.cs-q-orbit i:nth-child(2) { background: #E92E7F; transform: rotate(120deg) translateY(-20px); }
.cs-q-orbit i:nth-child(3) { background: #50B4EF; transform: rotate(240deg) translateY(-20px); }
.cs-q-title { font-weight: 700; font-size: 16px; color: #07228B !important; }
.cs-q-pos { font-size: 14px; color: #47148B; margin: 4px 0 12px; font-weight: 600; }
.cs-q-pos b { display: inline-block; min-width: 26px; padding: 1px 8px; border-radius: 99px; color: #fff !important;
background: linear-gradient(90deg, #47148B, #E92E7F); animation: csQPulse 1.4s ease-in-out infinite; }
.cs-q-bar { height: 8px; border-radius: 99px; background: #edf0f7; overflow: hidden; }
.cs-q-bar > div { height: 100%; width: 35%; border-radius: 99px;
background: linear-gradient(90deg, #07228B, #47148B, #E92E7F, #50B4EF); animation: csQSlide 1.4s ease-in-out infinite; }
.cs-q-hint { font-size: 12.5px; color: #6b7090; margin-top: 10px; }
.cs-run .cs-bar > div { background-size: 200% 100% !important; animation: csQShine 2s linear infinite; }
@keyframes csQIn { from { opacity: 0; transform: translateY(8px) } to { opacity: 1; transform: none } }
@keyframes csQSpin { to { transform: rotate(360deg) } }
@keyframes csQPulse { 0%, 100% { transform: scale(1) } 50% { transform: scale(1.12) } }
@keyframes csQSlide { 0% { transform: translateX(-110%) } 100% { transform: translateX(310%) } }
@keyframes csQShine { to { background-position: -200% 0 } }
@media (prefers-reduced-motion: reduce) { .cs-q *, .cs-q { animation: none !important; } }
@media (max-width: 700px) {
html, body { overflow-x: hidden !important; }
.gradio-container { min-width: 0 !important; width: 100% !important; max-width: 100vw !important;
padding-left: 10px !important; padding-right: 10px !important; box-sizing: border-box !important; }
.gradio-container main, .gradio-container .wrap, .gradio-container .contain,
.gradio-container .column, .gradio-container .tabs, .gradio-container .tabitem, .gradio-container .block {
min-width: 0 !important; max-width: 100% !important; }
.gradio-container .table-wrap { max-width: 100% !important; overflow-x: auto !important; }
}
</style>
<script>
(function () {
// The pop-up arrives inside a status panel; move it to <body> so it is centred on the whole page.
function mount() {
document.querySelectorAll('.cs-upsell').forEach(function (m) {
if (m.parentElement === document.body) return;
document.querySelectorAll('body > .cs-upsell').forEach(function (o) { o.remove(); });
document.body.appendChild(m);
});
}
function closeAll(el) {
var list = el ? [el] : Array.prototype.slice.call(document.querySelectorAll('body > .cs-upsell'));
list.forEach(function (m) { m.classList.add('cs-out'); setTimeout(function () { m.remove(); }, 230); });
}
function start() {
new MutationObserver(mount).observe(document.body, { childList: true, subtree: true });
document.addEventListener('click', function (e) {
var go = e.target.closest('[data-cs-go]');
var close = e.target.closest('[data-cs-close]');
if (go) {
var b = document.getElementById(go.getAttribute('data-cs-go'));
if (b) { (b.tagName === 'BUTTON' ? b : (b.querySelector('button') || b)).click(); }
if (!go.hasAttribute('data-cs-stay')) { window.scrollTo({ top: 0, behavior: 'smooth' }); }
}
if (go || close) { closeAll(e.target.closest('.cs-upsell')); }
});
document.addEventListener('keydown', function (e) { if (e.key === 'Escape') closeAll(); });
// ---- queue position inside our animated card (read from Gradio's hidden overlay) ----
setInterval(function () {
document.querySelectorAll('.cs-q').forEach(function (card) {
var block = card.closest('.block');
var pt = block && block.querySelector(':scope > .wrap .progress-text');
var pos = card.querySelector('.cs-q-pos');
if (!pt || !pos) return;
var t = pt.textContent || '';
var m = t.match(/queue:\\s*(\\d+)\\s*\\/\\s*(\\d+)/i);
var eta = t.match(/([\\d.]+)\\s*\\/?\\s*([\\d.]+)?s/);
if (m) {
var n = parseInt(m[1], 10);
pos.innerHTML = n <= 1 ? "You're <b>next</b> — starting in a moment"
: "You're <b>#" + n + "</b> in line" + (eta ? " · about " + Math.max(1, Math.round(parseFloat(eta[2] || eta[1]))) + "s" : "");
} else if (/\\d/.test(t)) {
pos.textContent = 'Starting…';
}
});
}, 500);
// ---- "your result is ready" notifications ----
// 1) server Web Push (push.py) -> service worker: arrives even if the tab is paused / phone locked
// 2) page notification + chime + flashing tab title as a fallback / on desktop
// 3) an animated in-page toast when the customer is looking at the page
var pending = false, audio = null, baseTitle = document.title, flash = null;
function cid() {
var v = null;
try { v = localStorage.getItem('cs_cid'); } catch (e) {}
if (!v) {
v = (Date.now().toString(36) + Math.random().toString(36).slice(2, 12));
try { localStorage.setItem('cs_cid', v); } catch (e) {}
}
document.cookie = 'cs_cid=' + v + '; path=/; max-age=31536000; SameSite=Lax' + (location.protocol === 'https:' ? '; Secure' : '');
return v;
}
cid();
function b64ToBytes(s) {
var p = '='.repeat((4 - s.length % 4) % 4), b = atob((s + p).replace(/-/g, '+').replace(/_/g, '/'));
var a = new Uint8Array(b.length); for (var i = 0; i < b.length; i++) a[i] = b.charCodeAt(i); return a;
}
function swReg() {
if (!('serviceWorker' in navigator) || !window.CS_SW) return Promise.resolve(null);
if (!window.__csSW) {
window.__csSW = navigator.serviceWorker.register(window.CS_SW).then(function (reg) {
if (reg.active) return reg;
return new Promise(function (res) {
var w = reg.installing || reg.waiting;
if (!w) return res(reg);
w.addEventListener('statechange', function () { if (w.state === 'activated') res(reg); });
setTimeout(function () { res(reg); }, 8000);
});
}).catch(function (e) { window.__csSWErr = String(e && e.message || e); console.warn('[cs] service worker:', e); return null; });
}
return window.__csSW;
}
function sendSub(sub) {
var box = document.querySelector('#cs-push-sub textarea, #cs-push-sub input');
var btn = document.getElementById('cs-push-save');
if (!box || !btn || !sub) return;
box.value = JSON.stringify(sub);
box.dispatchEvent(new Event('input', { bubbles: true }));
setTimeout(function () { btn.click(); }, 120);
}
function subscribe() {
if (!window.CS_VAPID || !('PushManager' in window) || Notification.permission !== 'granted') return Promise.resolve(null);
return swReg().then(function (reg) {
if (!reg || !reg.pushManager) return null;
return reg.pushManager.getSubscription().then(function (s) {
return s || reg.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: b64ToBytes(window.CS_VAPID) });
}).then(function (s) { sendSub(s); window.__csSubErr = null; return s; });
}).catch(function (e) { window.__csSubErr = String(e && e.message || e); console.warn('[cs] push subscribe:', e); return null; });
}
function unlockAudio() {
try {
if (!audio && (window.AudioContext || window.webkitAudioContext)) audio = new (window.AudioContext || window.webkitAudioContext)();
if (audio && audio.state === 'suspended') audio.resume();
} catch (e) {}
}
function askPermission() {
unlockAudio();
if (!('Notification' in window)) return Promise.resolve('unsupported');
var p = Notification.permission === 'default' ? Notification.requestPermission() : Promise.resolve(Notification.permission);
return Promise.resolve(p).then(function (perm) { if (perm === 'granted') subscribe(); return perm; });
}
if ('Notification' in window && Notification.permission === 'granted') subscribe(); // refresh on every visit
var lastRun = null, resumeAt = 0;
document.addEventListener('click', function (e) {
var b = e.target.closest('button.primary, button.lg');
if (b && !b.closest('.cs-upsell')) { pending = true; lastRun = b; askPermission(); }
}, true);
// Connection dropped while a job was running (phone locked, app switched, mobile data blip)?
// Ask the server again: it re-attaches to the job that kept running in the background (no restart, no second
// charge) and shows progress or the finished result. Triggers: Gradio's red "Error" badge on the running
// status panel, a reconnect / "connection errored" message, coming back online, or returning to the page.
var runBlock = null, hiddenAt = 0;
function resume(why) {
if (!pending || !lastRun || !document.body.contains(lastRun) || Date.now() < resumeAt) return;
resumeAt = Date.now() + 10000;
setTimeout(function () {
if (!pending) return;
console.info('[cs] resuming job after ' + why);
toast('Reconnected', 'Picking up your job where it left off…');
lastRun.click();
}, 1200);
}
function statusBroken() {
var b = runBlock && document.body.contains(runBlock) ? runBlock : null;
if (!b) {
var el = document.querySelector('.cs-run, .cs-q');
b = el && el.closest('.block');
}
if (!b) return false;
var err = b.querySelector(':scope > .wrap .error');
return !!(err && /error/i.test(err.textContent || ''));
}
var lastMeta = '', lastBeat = Date.now(), STALL_MS = 20000;
setInterval(function () {
var r = document.querySelector('.cs-run, .cs-q');
if (r) runBlock = r.closest('.block') || runBlock;
if (!pending) return;
// A running job's panel ticks every second ("42% · 31s"). A connection that dies without an error (mobile
// data switching, Wi-Fi roaming, a proxy dropping an idle stream) just stops the ticking: pick the job up.
var meta = document.querySelector('.cs-run .cs-st-meta');
if (meta && !document.hidden) {
var m = meta.textContent || '';
if (m !== lastMeta) { lastMeta = m; lastBeat = Date.now(); }
else if (Date.now() - lastBeat > STALL_MS) { lastBeat = Date.now(); resume('stalled progress'); return; }
} else { lastBeat = Date.now(); }
var t = Array.prototype.slice.call(document.querySelectorAll('[class*="toast"]'))
.map(function (x) { return x.textContent || ''; }).join(' ');
if (/re-?established|reconnected|errored out|connection (lost|error)/i.test(t)) resume('reconnect message');
else if (statusBroken()) resume('interrupted status');
}, 800);
window.addEventListener('online', function () { setTimeout(function () { resume('online'); }, 2000); });
document.addEventListener('visibilitychange', function () {
if (document.hidden) { hiddenAt = Date.now(); return; }
// back on the page after a while: phones usually cut the connection in the background
if (hiddenAt && Date.now() - hiddenAt > 4000) setTimeout(function () { if (statusBroken()) resume('return'); }, 1500);
});
window.addEventListener('pageshow', function (e) { if (e.persisted) setTimeout(function () { resume('pageshow'); }, 1500); });
function chime() {
try {
if (!audio) return;
[0, 0.16].forEach(function (d, i) {
var o = audio.createOscillator(), g = audio.createGain();
o.type = 'sine'; o.frequency.value = i ? 1046.5 : 784;
g.gain.setValueAtTime(0.0001, audio.currentTime + d);
g.gain.exponentialRampToValueAtTime(0.25, audio.currentTime + d + 0.02);
g.gain.exponentialRampToValueAtTime(0.0001, audio.currentTime + d + 0.45);
o.connect(g); g.connect(audio.destination); o.start(audio.currentTime + d); o.stop(audio.currentTime + d + 0.5);
});
} catch (e) {}
}
function flashTitle() {
if (flash) return;
var on = false; baseTitle = document.title.replace(/^✅ Ready! · /, '');
flash = setInterval(function () { document.title = (on = !on) ? '✅ Ready! · ' + baseTitle : baseTitle; }, 1000);
}
document.addEventListener('visibilitychange', function () {
if (!document.hidden && flash) { clearInterval(flash); flash = null; document.title = baseTitle; }
});
function notify(title, body, tag) {
if (!('Notification' in window) || Notification.permission !== 'granted') return;
var opts = { body: body, icon: window.CS_ICON || undefined, badge: window.CS_ICON || undefined,
tag: tag || 'cs-ready', renotify: false, data: { url: location.href } };
var viaSW = function () {
return swReg().then(function (reg) { if (reg) return reg.showNotification(title, opts); });
};
try {
var n = new Notification(title, opts);
n.onclick = function () { window.focus(); n.close(); };
} catch (e) { viaSW(); } // Android Chrome: only through the service worker
}
function toast(title, body) {
var t = document.createElement('div');
t.className = 'cs-ready-toast';
t.innerHTML = '<div class="cs-rt-icon">✅</div><div><div class="cs-rt-title"></div><div class="cs-rt-body"></div></div>';
t.querySelector('.cs-rt-title').textContent = title;
t.querySelector('.cs-rt-body').textContent = body;
t.addEventListener('click', function () { t.remove(); });
document.body.appendChild(t);
setTimeout(function () { t.classList.add('cs-rt-out'); setTimeout(function () { t.remove(); }, 400); }, 6000);
}
var readyTimer = null, mobileNoteShown = false;
// Gradio's own "On mobile, the connection can break … losing your position in queue" warning is not true
// here (jobs keep running on the server and resume by themselves), so hide it and show a friendly note.
function hideGradioMobileWarning() {
document.querySelectorAll('[class*="toast-body"]:not([data-cs-hidden])').forEach(function (t) {
if (/connection can break|losing your position/i.test(t.textContent || '')) {
t.setAttribute('data-cs-hidden', '1');
var item = t.closest('[class*="toast-wrap"] > *') || t;
item.style.display = 'none';
t.style.display = 'none';
if (!mobileNoteShown) {
mobileNoteShown = true;
toast('Your job keeps running', "You can switch apps or lock your phone — we'll pick it up and notify you when it's ready.");
}
}
});
}
new MutationObserver(function () {
hideGradioMobileWarning();
if (pending && document.querySelector('.cs-err:not([data-cs-seen]), .cs-upsell:not([data-cs-seen])')) {
document.querySelectorAll('.cs-err, .cs-upsell').forEach(function (x) { x.setAttribute('data-cs-seen', '1'); });
pending = false; // job failed or needs an upgrade: nothing to resume
}
document.querySelectorAll('.cs-done:not([data-cs-seen])').forEach(function (d) {
d.setAttribute('data-cs-seen', '1');
if (!pending) return;
var job = (d.parentElement || d).querySelector('.cs-job');
if (job) { // final update (carries the job id)
pending = false; clearTimeout(readyTimer); ready(d, job);
} else { // result shown; the job id follows a moment later
clearTimeout(readyTimer);
readyTimer = setTimeout(function () { if (pending) { pending = false; ready(d, null); } }, 1500);
}
});
}).observe(document.body, { childList: true, subtree: true });
function ready(d, job) {
var tag = job ? 'cs-ready-' + job.getAttribute('data-job') : 'cs-ready';
var txt = (d.textContent || '').replace(/\\s+/g, ' ').trim();
var first = (txt.match(/^.*?[.!?](?=\\s|[A-Z]|$)/) || [txt])[0].slice(0, 140);
var away = document.hidden || !document.hasFocus();
notify('✅ Your result is ready', first + ' Tap to open your result.', tag); // same tag as the server push -> shown once
if (away) flashTitle(); else toast('Your result is ready', first);
chime();
}
// 🔔 button: turn notifications on, check every step, and send a test notification (page + server push)
document.addEventListener('click', function (e) {
if (!e.target.closest('#cs-bell')) return;
if (!('Notification' in window)) {
toast('Not supported in this browser', 'Open www.catalysthumanizer.com in Chrome. (Inside the huggingface.co page or an in-app browser notifications are blocked.)');
return;
}
if (window.top !== window.self) {
toast('Open the site directly', 'Notifications are blocked inside the huggingface.co page — open www.catalysthumanizer.com.');
}
askPermission().then(function (perm) {
if (perm === 'denied') {
toast('Notifications are blocked', 'Tap the icon left of the web address → Permissions/Notifications → Allow, then reload.');
return;
}
if (perm !== 'granted') { toast('Please tap “Allow”', 'Press 🔔 Notify me again and choose Allow.'); return; }
subscribe().then(function (sub) {
var lines = ['✓ Notifications allowed'];
lines.push(window.__csSW ? (window.__csSWErr ? '✗ Background helper failed: ' + window.__csSWErr : '✓ Background helper ready') : '✗ Background helper not available');
lines.push(sub ? '✓ Phone/browser registered for alerts' : '✗ Registration failed' + (window.__csSubErr ? ': ' + window.__csSubErr : ''));
notify('🔔 Notifications are on', "You'll get a notification like this when your result is ready.", 'cs-test');
if (sub) setTimeout(function () { var b = document.getElementById('cs-push-test'); if (b) b.click(); }, 1500);
toast(sub ? 'Notifications are on' : 'Notifications partly on', lines.join('\\n'));
});
});
});
}
if (document.body) start(); else document.addEventListener('DOMContentLoaded', start);
})();
</script>"""
# Service worker + icon for "document ready" notifications, served from ./static only (never the app code).
STATIC_DIR = PU.web_dir(FAVICON_PATH) # written at start-up: nothing extra to upload
gr.set_static_paths(paths=[str(STATIC_DIR)])
ICON_URL = f"/gradio_api/file={(STATIC_DIR / 'cs-icon.png').as_posix()}"
HEAD += """<script>
(function () {
function btn() {
var b = document.getElementById('cs-stop-float');
if (!b && document.body) {
b = document.createElement('button');
b.id = 'cs-stop-float'; b.type = 'button'; b.textContent = '⏹ Stop & reset';
b.title = 'Cancel the running job and reload the page';
b.addEventListener('click', function () {
if (!window.confirm('Stop this job and reset the page?\\n\\nThe job is cancelled and nothing is charged.')) return;
var s = document.querySelector('#cs-stop-srv');
if (s) (s.tagName === 'BUTTON' ? s : (s.querySelector('button') || s)).click();
try { sessionStorage.setItem('cs_stopped', '1'); } catch (e) {}
b.disabled = true; b.textContent = 'Stopping…';
setTimeout(function () { window.location.reload(); }, 1500);
});
document.body.appendChild(b);
}
return b;
}
setInterval(function () {
var b = btn(); if (!b || b.disabled) return;
b.style.display = document.querySelector('.cs-run, .cs-q') ? 'flex' : 'none';
}, 700);
function stoppedNote() {
try {
if (!sessionStorage.getItem('cs_stopped')) return;
sessionStorage.removeItem('cs_stopped');
} catch (e) { return; }
var t = document.createElement('div');
t.className = 'cs-stop-note';
t.textContent = '⏹ Job stopped and the page was reset. You can start again.';
document.body.appendChild(t);
setTimeout(function () { t.remove(); }, 6000);
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', stoppedNote);
else stoppedNote();
})();
</script>"""
HEAD += (f'<script>window.CS_SW = "/gradio_api/file={(STATIC_DIR / "cs-notify-sw.js").as_posix()}";'
f'window.CS_ICON = "/gradio_api/file={(STATIC_DIR / "cs-icon.png").as_posix()}";'
f'window.CS_VAPID = "{PU.PUBLIC_KEY if PU.ENABLED else ""}";</script>')
HEAD += UI.head() # the design layer, last so it decides the look
theme = gr.themes.Soft(primary_hue="indigo", secondary_hue="pink", neutral_hue="slate",
font=[gr.themes.GoogleFont("Inter" if UI.ENABLED else "Poppins"), "ui-sans-serif", "sans-serif"],
radius_size="lg")
HID = dict(show_progress="hidden", api_name=False)
WAIT = ('<div class="cs-status cs-q"><div class="cs-q-orbit"><i></i><i></i><i></i></div>'
'<div class="cs-q-title">You\'re in the queue</div>'
'<div class="cs-q-pos">Getting a slot ready for you…</div>'
'<div class="cs-q-bar"><div></div></div>'
'<div class="cs-q-hint">🔔 You can switch tabs or apps — we\'ll notify you the moment it\'s ready.</div></div>')
def JOB(status):
"""Heavy jobs: Gradio's queue position / ETA shows on the status panel while waiting."""
# Jobs start at once (no waiting in Gradio's queue, where a phone going to sleep would drop them); the CPU is
# shared inside the job by runtime.cpu_job, and the job keeps running on the server if the page disconnects.
# trigger_mode "multiple": when a connection dies silently the page still thinks the old request is running;
# without this its automatic retry would be ignored (the retry re-attaches to the same job, never a second one)
return dict(show_progress="full", show_progress_on=[status], api_name=False, trigger_mode="multiple",
concurrency_limit=int(os.getenv("JOB_CONCURRENCY", "40")))
def _run_tool(core, args, n_outputs, status_index, user=None, request=None, tool=""):
"""The Journal Formatter / Finder jobs: like H._run_live, but keyed by visitor + tool + input, so a page that
reconnects after a dropped connection picks up the running (or finished) job instead of starting it over."""
key = _job_key(user, request, tool, args) if tool else None
with _START_LOCK:
job = H.find_job(key)
if job is not None and job.done.is_set() and (job.delivered or "error" in job.box):
job = None
if job is None:
job = H.start_job(key, core, args, owner=_who(user, request))
yield from H.follow(job, n_outputs, status_index)
job.delivered = True
# the shared look handed to the Journal Formatter / Finder tabs: animated waiting card, live status panel with the
# spinning logo and progress bar, and the job settings every heavy button uses
_FREE_NOTE = (f"Free with a free account: pasted text up to {P.PLANS['free']['paste_words']} words "
f"({P.PLANS['free']['pastes']} a day), {P.FREE_TRIAL_DOCS} trial document up to "
f"{P.PLANS['free']['doc_words']:,} words and {P.FREE_DETECT_DAY} AI detector reports a day. The Journal "
"Formatter and the Journal Finder are free. Customers who already have credits or a plan keep using "
"them until they run out; after that, runs are paid from the wallet.")
TOOLS_UI = dict(wait=WAIT, hid=HID, job=JOB, run_live=_run_tool, log=DB.log_run, user=None)
def thresholds():
with gr.Accordion("Detector thresholds (advanced)", open=False):
hi = gr.Slider(0.5, 0.99, 0.80, step=0.01, label="High (cyan) ≥")
mod = gr.Slider(0.3, 0.95, 0.50, step=0.01, label="Moderate (purple) ≥ — sentences above this are rewritten")
return hi, mod
def lock_gate(what):
"""Returns (lock column, open column). Caller builds the upload UI inside the open column."""
lock = gr.Column(visible=True, elem_classes="cs-card")
with lock:
gr.HTML(_locked(what))
btn = gr.Button("Sign in or create a free account", variant="primary", elem_classes="cs-lockbtn")
opened = gr.Column(visible=False)
LOCKS.append(lock)
OPENS.append(opened)
return lock, opened, btn
GOTO_ACCOUNT = []
with gr.Blocks(title=f"{APP_NAME} · CS Software Solutions", theme=theme, css=CSS + UI.css() + FND.CSS, js=FORCE_LIGHT,
head=HEAD) as demo:
user_state = gr.State(None)
TOOLS_UI["user"] = user_state # the formatter / finder log their runs under the signed-in user
session = gr.BrowserState(SIGNED_OUT, storage_key="cs_suite_session", secret=A.session_secret())
if not UI.ENABLED:
gr.HTML(HEADER)
with gr.Row(elem_id="cs-strip", equal_height=True):
if UI.ENABLED:
with gr.Column(scale=3, min_width=0):
gr.HTML(BRAND_HTML)
user_bar = gr.HTML(_userbar(None), elem_id="cs-ub")
top_signin = gr.Button("Sign in", size="sm", elem_classes="cs-topbtn", scale=0)
top_upgrade = gr.Button("💳 Wallet", size="sm", elem_classes="cs-topbtn cs-upbtn", scale=0)
top_admin = gr.Button("Admin", size="sm", elem_classes="cs-topbtn", visible=False, scale=0)
top_logout = gr.Button("Log out", size="sm", elem_classes="cs-topbtn", visible=False, scale=0)
bell = gr.Button("Notify me", size="sm", elem_id="cs-bell", elem_classes="cs-topbtn", scale=0)
push_sub = gr.Textbox(elem_id="cs-push-sub", show_label=False, container=False, scale=0)
push_save = gr.Button("save", elem_id="cs-push-save", scale=0)
push_test = gr.Button("test", elem_id="cs-push-test", scale=0)
go_pricing = gr.Button("pricing", elem_id="cs-go-pricing", elem_classes="cs-hidden-nav", scale=0)
go_account = gr.Button("account", elem_id="cs-go-account", elem_classes="cs-hidden-nav", scale=0)
go_formatter = gr.Button("formatter", elem_id="cs-go-formatter", elem_classes="cs-hidden-nav", scale=0)
go_finder = gr.Button("finder", elem_id="cs-go-finder", elem_classes="cs-hidden-nav", scale=0)
stop_srv = gr.Button("stop", elem_id="cs-stop-srv", elem_classes="cs-hidden-nav", scale=0)
unlock_smart = gr.Button("unlock", elem_id="cs-go-unlock-smart", elem_classes="cs-hidden-nav", scale=0)
unlock_hum = gr.Button("unlock", elem_id="cs-go-unlock-hum", elem_classes="cs-hidden-nav", scale=0)
sp_force = gr.Button("rephrase", elem_id="cs-go-rephrase-text", elem_classes="cs-hidden-nav", scale=0)
sd_force = gr.Button("rephrase", elem_id="cs-go-rephrase-doc", elem_classes="cs-hidden-nav", scale=0)
with gr.Tabs(elem_id="cs-nav", selected="smart") as nav:
# ============================ SMART HUMANIZE ============================
with gr.Tab("Smart Humanize", id="smart"):
gr.HTML('<div class="cs-intro cs-hero"><h2>Make AI-flagged writing read as <em>your own</em></h2>'
'<p>Drop your Word file and the rest is automatic: AI-sounding sentences are found, rewritten and '
'checked again until the score is as low as it can safely go. Numbers, citations, technical '
'terms and your layout stay exactly as they are.</p>'
'<div class="cs-flow"><span><i>1</i>Drop your .docx</span><span><i>2</i>We rewrite and '
're-check</span><span><i>3</i>Download your file</span></div>'
'<div class="cs-trust"><span>Meaning checked sentence by sentence</span>'
'<span>Formatting, tables and equations untouched</span><span>Your files are not kept for training'
'</span></div></div>')
with gr.Accordion("📑 Add a checker report (optional)", open=False):
s_report = gr.File(label="Report PDF, as downloaded from the checker", file_types=[".pdf"],
type="filepath", elem_classes="cs-report")
gr.HTML('<div class="cs-gauge-cap" style="text-align:left;margin:-4px 2px 4px">Any checker, scanned '
'reports too. The kind of report is recognised automatically, the highlighted sentences are '
'found on its pages and only those sentences are rewritten.</div>')
with gr.Tabs(elem_classes="sub-tabs", selected="smart_doc"):
with gr.Tab("Paste text (free)"):
with gr.Row(equal_height=True):
with gr.Column(elem_classes="cs-card"):
sp_in = gr.Textbox(label="Your text", lines=14, max_lines=30,
placeholder="Paste your text here — from Word, a web page or a PDF. Paragraphs and headings are kept; aim for 50+ words for a reliable AI score.")
sp_count = gr.HTML(H.count_words(""))
with gr.Column(elem_classes="cs-card"):
sp_out = gr.Textbox(label="Humanized text", lines=14, max_lines=30,
show_copy_button=True, interactive=False)
sp_btn = gr.Button("Detect & humanize", variant="primary", size="lg")
sp_status = gr.HTML(H.status_idle("Paste your text and press **Detect & humanize**. Nothing else to set."))
sp_scores = gr.HTML(EMPTY)
with gr.Row():
sp_file = gr.File(label="Download as .docx", interactive=False)
sp_dl = gr.HTML()
gr.HTML(next_steps_html())
with gr.Accordion("Final AI highlight view", open=False):
sp_prev = gr.HTML()
with gr.Tab("Word document", id="smart_doc"):
_, sd_open, sd_lockbtn = lock_gate("Word documents")
GOTO_ACCOUNT.append(sd_lockbtn)
with sd_open:
with gr.Row(equal_height=False):
with gr.Column(elem_classes="cs-card"):
sd_in = gr.File(label="Your Word document (.docx): drop it here and it starts by itself",
file_types=[".docx"], type="filepath", elem_id="cs-drop")
sd_btn = gr.Button("Run again", variant="secondary", size="sm")
gr.Markdown("<small>Only flagged sentences are replaced; formatting of everything else, "
"tables, figures, equations and headers stay untouched. Citation fields, hyperlinks "
"and super/subscripts inside a sentence are never altered.</small>")
if P.PAY_PER_USE:
gr.Markdown("<small>💳 <b>No credits or plan?</b> You can still run your document: "
"you see the AI scores first and pay only to download the result. "
"The price depends on the document (its length and the work it "
"needed) and is shown before you pay.</small>")
with gr.Column(elem_classes="cs-card"):
sd_status = gr.HTML(H.status_idle("Drop your .docx on the left. It starts by itself; nothing to press or set."))
sd_file = gr.File(label="Download humanized .docx", interactive=False)
sd_dl = gr.HTML()
sd_pr_btn = gr.Button("Final check & proofread (keeps the AI score)", variant="secondary")
gr.Markdown("<small>After humanizing: one last full check of the whole document in a "
"single pass: grammar, unclear sentences, academic wording, flow and "
"consistent terms, compared with your original. Grammar fixes are always "
"made; style edits only if the AI score holds. Problems only the author can "
"decide are listed. Free, does not use a document from your plan.</small>")
sd_pr_status = gr.HTML()
sd_pr_file = gr.File(label="Download proofread .docx", interactive=False)
gr.HTML(next_steps_html())
sd_pr_dl = gr.HTML()
sd_pr_panel = gr.HTML()
sd_scores = gr.HTML(EMPTY)
with gr.Accordion("Final AI highlight view", open=False):
sd_prev = gr.HTML()
with gr.Tab("Proofread only"):
gr.Markdown("Proofread **any** article or chapter, no humanizing needed: grammar first (every "
"error fixed and verified), then clarity, academic wording and flow, doubled terms "
"and AI leftovers removed, while keeping the AI score where it was.")
with gr.Tabs(elem_classes="sub-tabs"):
with gr.Tab("Word document"):
_, pn_open, pn_lockbtn = lock_gate("Word documents")
GOTO_ACCOUNT.append(pn_lockbtn)
with pn_open:
with gr.Row(equal_height=False):
with gr.Column(elem_classes="cs-card"):
pn_in = gr.File(label="Document to proofread (.docx)", file_types=[".docx"],
type="filepath")
pn_orig = gr.File(label="Optional: the author's original version (.docx), "
"used to repair unclear sentences",
file_types=[".docx"], type="filepath")
pn_btn = gr.Button("Proofread document", variant="primary", size="lg")
with gr.Column(elem_classes="cs-card"):
pn_status = gr.HTML(H.status_idle("Upload a .docx and press **Proofread "
"document**."))
pn_file = gr.File(label="Download proofread .docx", interactive=False)
pn_dl = gr.HTML()
pn_panel = gr.HTML(EMPTY)
with gr.Tab("Paste text"):
with gr.Row(equal_height=False):
with gr.Column(elem_classes="cs-card"):
pt_in = gr.Textbox(label="Text to proofread", lines=12,
placeholder="Paste a paragraph, abstract or section…")
pt_btn = gr.Button("Proofread text", variant="primary", size="lg")
with gr.Column(elem_classes="cs-card"):
pt_status = gr.HTML(H.status_idle("Paste text and press **Proofread text**."))
pt_out = gr.Textbox(label="Proofread text", lines=12, interactive=False)
pt_file = gr.File(label="Download as .docx", interactive=False)
pt_dl = gr.HTML()
pt_panel = gr.HTML(EMPTY)
# ============================ DETECTOR ============================
with gr.Accordion("⚙️ Advanced options (not needed)", open=False):
gr.HTML('<div class="cs-gauge-cap" style="text-align:left;margin:2px 2px 8px">Leave these as they are '
'unless you want to steer the rewrite yourself. The tool already aims for the lowest score, '
'runs as many passes as help, and switches to a deeper rewrite by itself when a text needs it.'
'</div>')
with gr.Row():
s_target = gr.Slider(0, 60, value=0, step=1, label="Target AI score (%)",
info="After the first rewrite pass, stop once the score is at or below this")
s_passes = gr.Slider(1, 10, value=10, step=1, label="Maximum passes",
info="Passes run automatically: each one rewrites what is still flagged, "
"starting from the previous result, and they stop as soon as the AI "
"score reaches the target or stops improving")
with gr.Row():
s_temp = gr.Slider(0.3, 1.5, value=0.9, step=0.1, label="Rewrite variation",
info="Increases a little on each extra pass")
s_mode = gr.Radio(MODES, value=MODES[0], label="Processing", visible=H.ON_GPU)
s_vocab = gr.Slider(0, 90, value=0, step=5, label="Vocabulary change (%)",
info="Share of the vocabulary (content words) to replace (0 = off, standard rewrite). Higher = more "
"rewriting and less of the original wording; terms, numbers and citations always stay. "
"Ticking the 100% AI box sets 70.")
s_full = gr.Checkbox(value=False, label="🤖 Rewrite every sentence (my text is 100% AI-written)",
info="Not needed for a low score: flagged sentences already get a deeper rewrite "
"automatically when the normal one is not enough. Tick this only to have the "
"sentences that read as human rewritten as well. Slower.",
elem_classes="cs-fullai")
s_rkind = gr.Radio(REPORT_KINDS, value=REPORT_KINDS[0], label="Report type",
info="Recognised from the report itself; choose one only if it was read as the "
"wrong kind")
s_hi, s_mod = thresholds()
with gr.Tab("AI Detector", id="detect"):
gr.HTML('<div class="cs-intro"><b>AI Writing Report.</b> Sentence-level highlighting '
'(cyan = high, purple = moderate) plus a downloadable PDF report. Results are probabilistic indicators.</div>')
d_hi, d_mod = thresholds()
with gr.Tabs(elem_classes="sub-tabs"):
with gr.Tab("Paste text (free)"):
with gr.Row(equal_height=False):
with gr.Column(scale=1, elem_classes="cs-card"):
dp_in = gr.Textbox(label="Your text", lines=14, max_lines=30,
placeholder="Paste your text here — from Word, a web page or a PDF. Paragraphs and headings are kept; aim for 50+ words for a reliable AI score.")
dp_count = gr.HTML(H.count_words(""))
dp_btn = gr.Button("Check for AI", variant="primary", size="lg")
dp_status = gr.HTML(H.status_idle("Paste text and press **Check for AI**."))
dp_pdf = gr.File(label="PDF report", interactive=False)
dp_dl = gr.HTML()
dp_to_smart = gr.Button("Humanize this text →")
with gr.Column(scale=2):
dp_html = gr.HTML()
with gr.Tab("Upload a file"):
_, dd_open, dd_lockbtn = lock_gate("documents for AI detection")
GOTO_ACCOUNT.append(dd_lockbtn)
with dd_open:
with gr.Row(equal_height=False):
with gr.Column(scale=1, elem_classes="cs-card"):
dd_in = gr.File(label="Document (.docx, .pdf, .txt)",
file_types=[".docx", ".pdf", ".txt", ".md"], type="filepath")
dd_btn = gr.Button("Generate report", variant="primary", size="lg")
dd_status = gr.HTML(H.status_idle("Upload a file and press **Generate report**."))
dd_pdf = gr.File(label="PDF report", interactive=False)
dd_dl = gr.HTML()
dd_to_smart = gr.Button("Humanize this .docx →")
with gr.Column(scale=2):
dd_html = gr.HTML()
# ============================ HUMANIZER ============================
with gr.Tab("Humanizer", id="humanize", visible=False): # Smart Humanize includes this engine
gr.HTML('<div class="cs-intro"><b>Full paraphrase.</b> Rewrites every eligible paragraph with our '
'rewriting engine, then shows the detector score before and after. For targeted rewriting of only the '
'AI-flagged sentences, use <b>Smart Humanize</b>.</div>')
with gr.Tabs(elem_classes="sub-tabs"):
with gr.Tab("Paste text (free)"):
with gr.Row(equal_height=True):
with gr.Column(elem_classes="cs-card"):
hp_in = gr.Textbox(label="Your text", lines=14, max_lines=30,
placeholder="Paste your text here — from Word, a web page or a PDF. Paragraphs and headings are kept; aim for 50+ words for a reliable AI score.")
hp_count = gr.HTML(H.count_words(""))
with gr.Column(elem_classes="cs-card"):
hp_out = gr.Textbox(label="Paraphrased text", lines=14, max_lines=30,
show_copy_button=True, interactive=False)
with gr.Row():
with gr.Column():
with gr.Accordion("⚙️ Advanced options (not needed)",
open=False):
hp_temp = gr.Slider(0.3, 1.5, value=0.9, step=0.1, label="Paraphrase variation")
hp_min = gr.Slider(3, 30, value=6, step=1, label="Minimum words per paragraph")
hp_mode = gr.Radio(MODES, value=MODES[0], label="Processing", visible=H.ON_GPU)
hp_vocab = gr.Slider(0, 90, value=0, step=5, label="Vocabulary change (%)",
info="Share of the vocabulary (content words) to replace (0 = off, standard rewrite). Higher = more "
"rewriting and less of the original wording; terms, numbers and citations always stay. "
"Ticking the 100% AI box sets 70.")
hp_full = gr.Checkbox(value=False, label="🤖 My text is 100% AI-written",
info="Deep rewrite: several versions of every sentence are written and the most natural one is kept. Slower. "
"Leave it off for your own writing or lightly edited AI text.",
elem_classes="cs-fullai")
hp_btn = gr.Button("Paraphrase text", variant="primary", size="lg")
with gr.Column():
hp_status = gr.HTML(H.status_idle("Paste text and press **Paraphrase text**."))
hp_file = gr.File(label="Download as .docx", interactive=False)
hp_dl = gr.HTML()
hp_to_det = gr.Button("Send result to AI Detector →")
hp_scores = gr.HTML(EMPTY)
with gr.Tab("Word document"):
_, hd_open, hd_lockbtn = lock_gate("Word documents")
GOTO_ACCOUNT.append(hd_lockbtn)
with hd_open:
with gr.Row(equal_height=False):
with gr.Column(elem_classes="cs-card"):
hd_in = gr.File(label="Word document (.docx)", file_types=[".docx"], type="filepath")
with gr.Accordion("⚙️ Advanced options (not needed)",
open=False):
hd_temp = gr.Slider(0.3, 1.5, value=0.9, step=0.1, label="Paraphrase variation")
hd_min = gr.Slider(5, 30, value=8, step=1, label="Minimum words per paragraph")
hd_mode = gr.Radio(MODES, value=MODES[0], label="Processing", visible=H.ON_GPU)
hd_vocab = gr.Slider(0, 90, value=0, step=5, label="Vocabulary change (%)",
info="Share of the vocabulary (content words) to replace (0 = off, standard rewrite). Higher = more "
"rewriting and less of the original wording; terms, numbers and citations always stay. "
"Ticking the 100% AI box sets 70.")
hd_full = gr.Checkbox(value=False, label="🤖 My text is 100% AI-written",
info="Deep rewrite: several versions of every sentence are written and the most natural one is kept. Slower. "
"Leave it off for your own writing or lightly edited AI text.",
elem_classes="cs-fullai")
hd_btn = gr.Button("Paraphrase document", variant="primary", size="lg")
with gr.Column(elem_classes="cs-card"):
hd_status = gr.HTML(H.status_idle("Upload a document and press **Paraphrase document**."))
hd_file = gr.File(label="Download paraphrased .docx", interactive=False)
hd_dl = gr.HTML()
hd_pr_btn = gr.Button("Final check & proofread (keeps the AI score)", variant="secondary")
hd_pr_status = gr.HTML()
hd_pr_file = gr.File(label="Download proofread .docx", interactive=False)
hd_pr_dl = gr.HTML()
gr.HTML(next_steps_html())
hd_pr_panel = gr.HTML()
hd_scores = gr.HTML(EMPTY)
with gr.Accordion("Preview of changes (first 10)", open=False):
hd_prev = gr.Markdown()
# ============================ COMPANION TOOLS ============================
# Both tools run inside this Space (formatter_tab.py / finder_tab.py). TOOLS_EMBED=1 shows the earlier
# separately hosted apps in a frame instead.
with gr.Tab("Journal Formatter", id="formatter"):
if TOOLS_EMBED:
gr.HTML(companion_tab_html(FORMAT_URL, "Journal Formatter.",
"Upload your (humanized) manuscript and give a template, the journal's "
"guidelines or just the journal name: the file is rearranged to that "
"format (Word and LaTeX)."))
else:
FMT.build(TOOLS_UI)
with gr.Tab("Journal Finder", id="finder"):
if TOOLS_EMBED:
gr.HTML(companion_tab_html(FINDER_URL, "Journal Finder.",
"Enter your paper's title to get matching journals with indexing (Scopus, "
"PubMed, ABDC), quartile, review time and acceptance odds."))
else:
FND.build(TOOLS_UI)
# ============================ PRICING ============================
with gr.Tab("Pricing", id="pricing") as pricing_tab:
pr_status = gr.HTML()
pr_html = gr.HTML(W.pricing_html(_FREE_NOTE))
with gr.Group(elem_classes="cs-card"):
gr.HTML('<div class="cs-auth-title" style="text-align:left">Add money to your wallet</div>')
with gr.Row():
pr_top = [gr.Button(f"Add {W.inr(x * 100)}", variant="primary" if i == 1 else "secondary")
for i, x in enumerate(W.TOPUPS)]
with gr.Row():
pr_amount = gr.Number(value=None, precision=0, label="Or another amount (₹)",
minimum=W.TOPUP_MIN, maximum=W.TOPUP_MAX, scale=3)
pr_go = gr.Button("Add this amount", scale=1)
pr_pay = gr.HTML()
pr_url = gr.Textbox(visible=False)
pr_hist = gr.Dataframe(headers=["When (UTC)", "What", "Amount", "Details"], label="Wallet activity",
interactive=False, wrap=True)
gr.HTML('<div class="cs-gauge-cap" style="text-align:left;margin-top:10px">Payments are processed by '
'PhonePe (UPI, debit and credit cards, net banking). The money is added to your wallet as soon '
'as PhonePe confirms it; if you close the page during payment it is still added within a few '
'minutes. Before every run the usual price is shown and that amount is reserved; the run is '
'then charged what it actually used, never more than the reservation, and the rest stays in your '
'wallet. A run that fails or is stopped costs nothing. Results are indicators from our own '
'detector and can differ from other checkers.</div>')
# ============================ ABOUT / CONTACT ============================
with gr.Tab("About", id="about"):
gr.HTML(SP.about_html())
with gr.Tab("Contact", id="contact"):
gr.HTML(SP.contact_html())
# ============================ USER DASHBOARD ============================
with gr.Tab("Dashboard", id="dashboard") as dash_tab:
_, ud_open, ud_lockbtn = lock_gate("dashboard")
GOTO_ACCOUNT.append(ud_lockbtn)
with ud_open:
with gr.Row():
gr.HTML('<div class="cs-auth-title" style="text-align:left">My dashboard</div>')
ud_refresh = gr.Button("Refresh", size="sm", scale=0, elem_classes="cs-topbtn")
ud_tiles = gr.HTML()
with gr.Row(equal_height=False):
with gr.Column(scale=3, elem_classes="cs-card"):
ud_chart = gr.BarPlot(x="Day", y="Runs", color="Tool", title="Your runs — last 14 days",
color_map=DB.TOOL_COLORS, height=280, x_label_angle=-45)
with gr.Column(scale=2, elem_classes="cs-card"):
gr.HTML('<div class="cs-settings-title">⬇️ Download a previous result</div>'
'<div class="cs-gauge-cap" style="text-align:left">Files are kept until the '
'server restarts.</div>')
ud_pick = gr.Dropdown(label="Recent results", choices=[], interactive=True)
ud_file = gr.File(label="File", interactive=False)
ud_dl = gr.HTML()
with gr.Column(elem_classes="cs-card"):
gr.HTML('<div class="cs-settings-title">🧾 My history (last 100 runs)</div>')
ud_hist = gr.Dataframe(headers=DB.HIST_COLS, interactive=False, wrap=True, max_height=460)
# ============================ ACCOUNT ============================
with gr.Tab("Account", id="account") as account_tab:
with gr.Column(visible=True, elem_classes="cs-auth") as auth_view:
with gr.Column(elem_classes="cs-card"):
gr.HTML('<div class="cs-auth-title">Welcome</div>'
'<div class="cs-auth-sub">Sign in or create a free account to upload documents. '
'Pasting text never needs an account.</div>')
with gr.Tabs():
with gr.Tab("Log in"):
li_email = gr.Textbox(label="Email", placeholder="you@university.edu")
li_pw = gr.Textbox(label="Password", type="password")
li_btn = gr.Button("Log in", variant="primary", size="lg")
with gr.Tab("Sign up"):
su_name = gr.Textbox(label="Full name")
su_email = gr.Textbox(label="Email", placeholder="you@university.edu")
su_pw = gr.Textbox(label="Password", type="password", info="At least 8 characters")
su_pw2 = gr.Textbox(label="Confirm password", type="password")
su_btn = gr.Button("Create account", variant="primary", size="lg")
with gr.Column(visible=False) as su_code_col: # step 2 when emails are confirmed
su_code = gr.Textbox(label="6-digit code from the email", max_lines=1,
placeholder="123456")
su_verify = gr.Button("Confirm email & create account", variant="primary", size="lg")
with gr.Tab("Forgot password"):
gr.HTML('<div class="cs-auth-sub">'
+ ('Get a 6-digit code by email, <b>or</b> use the recovery key you saved '
'when you created your account.' if M.ENABLED else
'Use the <b>recovery key</b> you saved when you created your account.')
+ '</div>')
fp_email = gr.Textbox(label="Your account email", placeholder="you@university.edu")
fp_send = gr.Button("Email me a 6-digit code", visible=M.ENABLED)
fp_key = gr.Textbox(label="6-digit code from the email, or recovery key" if M.ENABLED
else "Recovery key",
placeholder="123456 or XXXX-XXXX-XXXX-XXXX", max_lines=1)
fp_pw = gr.Textbox(label="New password", type="password", info="At least 8 characters")
fp_pw2 = gr.Textbox(label="Confirm new password", type="password")
fp_btn = gr.Button("Set new password & sign in", variant="primary", size="lg")
with gr.Accordion("No recovery key? Ask the admin", open=False):
gr.HTML('<div class="cs-auth-sub">The admin will check your request and send you '
'a temporary password. Change it after signing in (Account → Change '
'password).</div>')
fr_email = gr.Textbox(label="Your account email", placeholder="you@university.edu")
fr_contact = gr.Textbox(label="WhatsApp / phone number", placeholder="+91 98xxxxxxx")
fr_btn = gr.Button("Send reset request")
with gr.Column(visible=False, elem_classes="cs-auth") as profile_view:
with gr.Column(elem_classes="cs-card"):
profile_html = gr.HTML()
pref_mail = gr.Checkbox(value=True, label="📧 Email me my document results when they are ready",
visible=M.ENABLED)
with gr.Accordion("🔒 Change password", open=False):
cp_old = gr.Textbox(label="Current password", type="password")
cp_new = gr.Textbox(label="New password", type="password", info="At least 8 characters")
cp_new2 = gr.Textbox(label="Confirm new password", type="password")
cp_btn = gr.Button("Change password", variant="primary")
with gr.Accordion("🔑 Recovery key", open=False):
gr.HTML('<div class="cs-auth-sub">Lets you reset your password yourself if you forget it. '
'Creating a new key cancels the old one.</div>')
rk_btn = gr.Button("Create a new recovery key")
prof_logout = gr.Button("Log out")
auth_msg = gr.HTML()
# ============================ ADMIN ============================
with gr.Tab("Admin", id="admin", visible=False) as admin_tab:
if A.STORAGE_NOTE:
gr.HTML(f'<div class="cs-authmsg cs-bad">{A.STORAGE_NOTE}</div>')
with gr.Tabs(elem_classes="sub-tabs"):
# ---------- overview ----------
with gr.Tab("Overview") as ad_ov_tab:
with gr.Row():
gr.HTML('<div class="cs-auth-title" style="text-align:left">Platform overview</div>')
ad_ov_refresh = gr.Button("Refresh", size="sm", scale=0, elem_classes="cs-topbtn")
ad_ov_tiles = gr.HTML()
with gr.Row(equal_height=False):
with gr.Column(scale=3, elem_classes="cs-card"):
ad_ov_chart = gr.BarPlot(x="Day", y="Runs", color="Tool", title="Runs per day — last 30 days",
color_map=DB.TOOL_COLORS, height=300, x_label_angle=-45)
with gr.Column(scale=2, elem_classes="cs-card"):
ad_ov_tools = gr.BarPlot(x="Tool", y="Runs", color="Type", title="Tool usage — last 30 days",
color_map={"Document": "#07228B", "Text": "#F5C518", "Search": "#E92E7F"}, height=300)
with gr.Column(elem_classes="cs-card"):
gr.HTML('<div class="cs-settings-title">🏆 Most active users (30 days)</div>')
ad_ov_top = gr.Dataframe(headers=["Name", "Email", "Runs", "Documents", "Words", "Last run"],
interactive=False, wrap=True)
# ---------- activity ----------
with gr.Tab("Activity") as ad_act_tab:
with gr.Row():
ad_act_q = gr.Textbox(label="Search", placeholder="Email, file name or IP", scale=3)
ad_act_tool = gr.Dropdown(["All"] + DB.TOOLS, value="All", label="Tool", scale=2)
ad_act_status = gr.Dropdown(["All", "Errors only", "Guests only"], value="All",
label="Show", scale=2)
ad_act_go = gr.Button("Filter", scale=1)
ad_act_export = gr.Button("⬇ Export CSV", scale=1)
ad_act_table = gr.Dataframe(headers=DB.ACT_COLS, interactive=False, wrap=True, max_height=560)
ad_act_csv = gr.File(label="Activity export", interactive=False, visible=False)
# ---------- users ----------
with gr.Tab("Subscriptions") as ad_sub_tab:
ad_p_msg = gr.HTML()
with gr.Column(elem_classes="cs-card"):
gr.HTML('<div class="cs-settings-title">💎 Activate / renew / terminate a subscription</div>'
'<div class="cs-gauge-cap" style="text-align:left"><b>Activate / renew:</b> after a UPI / '
'bank payment, choose the plan and number of months. Renewing an active plan adds the '
'months to its end date. <b>Terminate:</b> ends a paid subscription immediately '
'(refund, misuse, user request); the user goes back to the free plan.</div>')
with gr.Row():
ad_p_email = gr.Textbox(label="User email", scale=3)
ad_p_plan = gr.Dropdown(list(P.ADMIN_PLANS), value="scholar",
label="Plan", scale=2)
ad_p_days = gr.Number(value=1, label="Months", precision=0, minimum=1, scale=1)
ad_p_amount = gr.Textbox(label="Amount received (optional)", scale=2)
with gr.Row():
ad_p_activate = gr.Button("Activate plan", variant="primary")
ad_p_credits_n = gr.Number(value=1, label="Credits (1 single, 5 pack)", precision=0)
ad_p_credits = gr.Button("Add credits")
with gr.Row():
ad_t_reason = gr.Textbox(label="Reason for terminating (optional, sent to the user)",
scale=3)
ad_t_notify = gr.Checkbox(value=True, label="Email the user", scale=1)
ad_t_confirm = gr.Checkbox(value=False, label="I confirm: end it now", scale=1)
ad_t_go = gr.Button("Terminate subscription", variant="stop", scale=1)
with gr.Row(equal_height=False):
with gr.Column(elem_classes="cs-card"):
gr.HTML('<div class="cs-settings-title">📥 Upgrade requests</div>')
ad_req = gr.Dataframe(headers=P.REQ_COLS, interactive=False, wrap=True, max_height=320)
with gr.Column(elem_classes="cs-card"):
gr.HTML('<div class="cs-settings-title">💰 Payments log</div>')
ad_pay = gr.Dataframe(headers=P.PAY_COLS, interactive=False, wrap=True, max_height=320)
with gr.Column(elem_classes="cs-card"):
gr.HTML('<div class="cs-settings-title">💳 Pay-per-use charges</div>'
'<div class="cs-gauge-cap" style="text-align:left">Documents run without credits: '
'the result is locked until its charge is paid. Enter the reference the customer '
'paid for and press Mark paid to unlock it. (A payment gateway will do this by '
'itself once it is connected.) For a yearly plan, activate the plan above with '
'12 months.</div>')
with gr.Row():
ad_chg_ref = gr.Textbox(label="Charge reference (CS-XXXXXXXX)", scale=2)
ad_chg_amt = gr.Textbox(label="Amount received (optional)", scale=2)
ad_chg_go = gr.Button("Mark paid & unlock", variant="primary", scale=1)
ad_chg_refresh = gr.Button("Refresh", scale=1)
ad_chg_msg = gr.HTML()
ad_chg = gr.Dataframe(headers=P.CHARGE_COLS, interactive=False, wrap=True, max_height=320)
with gr.Column(elem_classes="cs-card"):
gr.HTML('<div class="cs-settings-title">👛 Wallet: profit, top-ups, adjustments</div>')
with gr.Row():
ad_w_days = gr.Radio(["7 days", "30 days"], value="30 days", label="Profit over", scale=2)
ad_w_refresh = gr.Button("Refresh", scale=1)
ad_w_report = gr.HTML()
with gr.Row():
ad_w_email = gr.Textbox(label="Customer email", scale=3)
ad_w_amt = gr.Number(label="₹ to add (negative to remove)", precision=0, scale=2)
ad_w_note = gr.Textbox(label="Reason (e.g. refund, bank transfer)", scale=3)
ad_w_go = gr.Button("Apply", variant="primary", scale=1)
ad_w_msg = gr.HTML()
ad_w_tops = gr.Dataframe(headers=["Started (UTC)", "Email", "Amount", "PhonePe state",
"Done (UTC)"], interactive=False, wrap=True, max_height=320)
with gr.Column(elem_classes="cs-card"):
gr.HTML('<div class="cs-settings-title">🗓️ Subscriptions (start date, paid until, status)'
'</div>')
ad_subs = gr.Dataframe(headers=P.SUB_COLS, interactive=False, wrap=True, max_height=360)
with gr.Accordion("Plan history (started / renewed / expired)", open=False):
ad_hist = gr.Dataframe(headers=P.HIST_COLS, interactive=False, wrap=True, max_height=360)
ad_sub_refresh = gr.Button("Refresh subscriptions", size="sm")
with gr.Tab("Users"):
ad_stats = gr.HTML()
with gr.Row():
ad_query = gr.Textbox(label="Search users", placeholder="Name or email", scale=4)
ad_refresh = gr.Button("Refresh", scale=1)
ad_export = gr.Button("⬇ Export CSV", scale=1)
ad_table = gr.Dataframe(headers=A.ADMIN_COLS, interactive=False, wrap=True, max_height=420)
ad_csv = gr.File(label="Users export", interactive=False, visible=False)
with gr.Column(elem_classes="cs-card"):
gr.HTML('<div class="cs-settings-title">🔎 User details</div>')
with gr.Row():
ad_u_email = gr.Textbox(label="User email", scale=4,
placeholder="Type an email or click a row in the table above")
ad_u_go = gr.Button("Show", scale=1)
ad_u_summary = gr.HTML()
ad_u_hist = gr.Dataframe(headers=DB.HIST_COLS, interactive=False, wrap=True, max_height=380)
with gr.Row(equal_height=False):
with gr.Column(elem_classes="cs-card"):
gr.HTML('<div class="cs-settings-title">Daily limits for a user</div>'
'<div class="cs-gauge-cap" style="text-align:left">Blank = default, 0 = unlimited</div>')
ad_l_email = gr.Textbox(label="User email")
with gr.Row():
ad_l_doc = gr.Textbox(label=f"Documents / day (default {A.LIMITS[('user', 'doc')]})")
ad_l_text = gr.Textbox(label=f"Texts / day (default {A.LIMITS[('user', 'text')]})")
ad_l_save = gr.Button("Save limits")
with gr.Column(elem_classes="cs-card"):
gr.HTML('<div class="cs-settings-title">Enable / disable account</div>')
ad_email = gr.Textbox(label="User email")
with gr.Row():
ad_disable = gr.Button("Disable")
ad_enable = gr.Button("Enable")
with gr.Row(equal_height=False):
with gr.Column(elem_classes="cs-card"):
gr.HTML('<div class="cs-settings-title">Reset password</div>')
ad_pw_email = gr.Textbox(label="User email")
ad_new_pw = gr.Textbox(label="New password", type="password")
ad_reset = gr.Button("Reset password")
gr.HTML('<div class="cs-gauge-cap" style="text-align:left">Password reset requests from '
'users without a recovery key — set a temporary password above and send it '
'to the contact shown.</div>')
ad_rr = gr.Dataframe(headers=A.RESET_REQ_COLS, interactive=False, wrap=True,
max_height=220)
with gr.Column(elem_classes="cs-card"):
gr.HTML('<div class="cs-settings-title">📧 Email</div>'
f'<div class="cs-gauge-cap" style="text-align:left">{M.status()}</div>')
with gr.Row():
ad_m_to = gr.Textbox(label="Send a test email to (blank = you)", scale=3)
ad_m_test = gr.Button("Send test email", scale=1)
ad_m_log = gr.Dataframe(headers=M.LOG_COLS, interactive=False, wrap=True, max_height=260)
with gr.Column(elem_classes="cs-card"):
gr.HTML('<div class="cs-settings-title">✅ Language engine status (admin only)</div>')
ad_groq = gr.HTML()
with gr.Column(elem_classes="cs-card"):
gr.HTML('<div class="cs-settings-title">🔔 Result notifications</div>')
ad_push = gr.HTML()
with gr.Column(elem_classes="cs-card"):
gr.HTML('<div class="cs-settings-title">Delete account</div>')
ad_del_email = gr.Textbox(label="User email")
ad_del_confirm = gr.Textbox(label="Type the email again to confirm")
ad_delete = gr.Button("Delete user", variant="stop")
ad_msg = gr.HTML()
gr.HTML(FOOTER)
# ============================ EVENTS ============================
# navigation shortcuts
for b in GOTO_ACCOUNT + [top_signin]:
b.click(lambda: gr.Tabs(selected="account"), None, nav, **HID)
dp_to_smart.click(lambda t: (gr.Tabs(selected="smart"), t), dp_in, [nav, sp_in], **HID)
dd_to_smart.click(lambda f: (gr.Tabs(selected="smart"), f if f and str(f).lower().endswith(".docx") else None),
dd_in, [nav, sd_in], **HID)
hp_to_det.click(lambda t: (gr.Tabs(selected="detect"), t), hp_out, [nav, dp_in], **HID)
# auth
auth_out = ([user_state, auth_view, profile_view, profile_html, user_bar, auth_msg, admin_tab,
ad_stats, ad_table, top_signin, top_logout, top_upgrade, top_admin] + LOCKS + OPENS + [session])
demo.load(restore_session, session, auth_out, **HID)
# once signed in, the passwords typed into the forms are wiped (they stayed in the page after log-out)
pw_boxes = [li_pw, su_pw, su_pw2]
wipe = lambda u: ["", "", ""] if u else [gr.update()] * 3
li_btn.click(do_login, [li_email, li_pw], auth_out, **HID).then(wipe, user_state, pw_boxes, **HID)
li_pw.submit(do_login, [li_email, li_pw], auth_out, **HID).then(wipe, user_state, pw_boxes, **HID)
if A.verify_signups(): # email confirmed with a code before the account exists
su_btn.click(do_signup_start, [su_name, su_email, su_pw, su_pw2], [auth_msg, su_code_col], **HID)
su_pw2.submit(do_signup_start, [su_name, su_email, su_pw, su_pw2], [auth_msg, su_code_col], **HID)
su_verify.click(do_signup_verify, [su_email, su_code], auth_out, **HID).then(
wipe, user_state, pw_boxes, **HID)
su_code.submit(do_signup_verify, [su_email, su_code], auth_out, **HID).then(wipe, user_state, pw_boxes, **HID)
else:
su_btn.click(do_signup, [su_name, su_email, su_pw, su_pw2], auth_out, **HID).then(
wipe, user_state, pw_boxes, **HID)
su_pw2.submit(do_signup, [su_name, su_email, su_pw, su_pw2], auth_out, **HID).then(
wipe, user_state, pw_boxes, **HID)
fp_btn.click(do_reset_key, [fp_email, fp_key, fp_pw, fp_pw2], auth_out, **HID)
fp_send.click(do_send_code, fp_email, auth_msg, **HID)
pref_mail.input(lambda u, on: M.set_wants_results(u["email"], on) if u else None, [user_state, pref_mail], None,
**HID)
account_tab.select(lambda u: gr.update(value=M.wants_results(u["email"])) if u else gr.update(), user_state,
pref_mail, **HID)
fr_btn.click(do_admin_request, [fr_email, fr_contact], auth_msg, **HID)
cp_btn.click(do_change_pw, [user_state, cp_old, cp_new, cp_new2], [auth_msg, cp_old, cp_new, cp_new2], **HID)
rk_btn.click(do_new_key, user_state, auth_msg, **HID)
for b in (top_logout, prof_logout):
b.click(do_logout, None, auth_out, **HID)
# admin
ad_out = [ad_stats, ad_table, ad_msg]
ad_refresh.click(lambda u, q: A._admin_refresh(u, q, "Refreshed."), [user_state, ad_query], ad_out, **HID)
ad_query.submit(A.admin_search, [user_state, ad_query], ad_table, **HID)
ad_disable.click(lambda u, e, q: A.admin_set_active(u, e, q, False), [user_state, ad_email, ad_query], ad_out, **HID)
ad_enable.click(lambda u, e, q: A.admin_set_active(u, e, q, True), [user_state, ad_email, ad_query], ad_out, **HID)
ad_reset.click(A.admin_reset_pw, [user_state, ad_pw_email, ad_new_pw, ad_query], ad_out, **HID).then(
A.admin_reset_requests, user_state, ad_rr, **HID)
ad_delete.click(A.admin_delete, [user_state, ad_del_email, ad_del_confirm, ad_query], ad_out, **HID)
ad_export.click(lambda u: gr.update(value=A.admin_export(u), visible=True), [user_state], ad_csv, **HID)
ad_l_save.click(A.admin_set_limits, [user_state, ad_l_email, ad_l_doc, ad_l_text, ad_query], ad_out, **HID)
# admin analytics
ov_out = [ad_ov_tiles, ad_ov_chart, ad_ov_tools, ad_ov_top]
def overview(u):
tiles, chart, tools, top = DB.admin_overview(u)
return DB._tiles(P.admin_revenue_tiles(u)) + tiles, chart, tools, top
ad_ov_refresh.click(overview, user_state, ov_out, **HID)
ad_ov_tab.select(overview, user_state, ov_out, **HID)
admin_tab.select(overview, user_state, ov_out, **HID)
act_in = [user_state, ad_act_q, ad_act_tool, ad_act_status]
for ev in (ad_act_go.click, ad_act_q.submit, ad_act_tool.change, ad_act_status.change):
ev(DB.admin_activity, act_in, ad_act_table, **HID)
ad_act_tab.select(DB.admin_activity, act_in, ad_act_table, **HID)
ad_act_export.click(lambda u: gr.update(value=DB.admin_export_activity(u), visible=True), user_state,
ad_act_csv, **HID)
ad_u_go.click(DB.admin_user_detail, [user_state, ad_u_email], [ad_u_summary, ad_u_hist], **HID)
ad_u_email.submit(DB.admin_user_detail, [user_state, ad_u_email], [ad_u_summary, ad_u_hist], **HID)
def pick_user(u, table, evt: gr.SelectData):
"""Click a row in the users table -> fill every email box and show that user's details."""
try:
row = evt.index[0]
email = table.iloc[row, 1] if hasattr(table, "iloc") else table[row][1]
except Exception:
return [gr.update()] * 8
summary, hist = DB.admin_user_detail(u, email)
return [email] * 6 + [summary, hist]
ad_table.select(pick_user, [user_state, ad_table],
[ad_u_email, ad_l_email, ad_email, ad_pw_email, ad_del_email, ad_p_email, ad_u_summary, ad_u_hist], **HID)
# user dashboard
ud_out = [ud_tiles, ud_chart, ud_hist, ud_pick]
def load_dash(u):
tiles, chart, hist, choices = DB.user_dashboard(u)
plan = f'<div class="cs-nudge">{P.badge(u)} {html.escape(P.usage_line(u))}</div>' if u else ""
return plan + tiles + P.nudge(u, True), chart, hist, gr.update(choices=choices, value=None)
def pick_file(u, run_id):
path = DB.user_file(u, run_id)
return path, H.download_button(path) if path else ""
ud_refresh.click(load_dash, user_state, ud_out, **HID)
dash_tab.select(load_dash, user_state, ud_out, **HID)
ud_pick.change(pick_file, [user_state, ud_pick], [ud_file, ud_dl], **HID)
# smart humanize
for _cb, _sl in ((s_full, s_vocab), (hp_full, hp_vocab), (hd_full, hd_vocab)):
_cb.change(lambda v: gr.update(value=70 if v else 0), _cb, _sl, queue=False, **HID)
sp_in.change(count_words_for, [sp_in, user_state], sp_count, queue=False, **HID)
sp_run = sp_btn.click(lambda: WAIT, None, sp_status, queue=False, **HID).then(smart_text, [sp_in, s_target, s_passes, s_temp, s_mode, s_hi, s_mod, s_full, s_vocab, s_report, s_rkind, user_state],
[sp_status, sp_out, sp_prev, sp_scores, sp_file, sp_dl], **JOB(sp_status))
sd_run = sd_btn.click(lambda: WAIT, None, sd_status, queue=False, **HID).then(smart_docx, [sd_in, s_target, s_passes, s_temp, s_mode, s_hi, s_mod, s_full, s_vocab, s_report, s_rkind, user_state],
[sd_status, sd_prev, sd_scores, sd_file, sd_dl], **JOB(sd_status))
# dropping a file is enough: the run starts by itself
sd_auto = sd_in.upload(lambda: WAIT, None, sd_status, queue=False, **HID).then(smart_docx, [sd_in, s_target, s_passes, s_temp, s_mode, s_hi, s_mod, s_full, s_vocab, s_report, s_rkind, user_state],
[sd_status, sd_prev, sd_scores, sd_file, sd_dl], **JOB(sd_status))
# "Rephrase anyway" (pop-up shown when the text / document scored 0% AI and was left unchanged)
_smart_in = [s_target, s_passes, s_temp, s_mode, s_hi, s_mod, s_full, s_vocab, s_report, s_rkind, user_state]
sp_force_run = sp_force.click(lambda: WAIT, None, sp_status, queue=False, **HID).then(
smart_text_force, [sp_in] + _smart_in, [sp_status, sp_out, sp_prev, sp_scores, sp_file, sp_dl], **JOB(sp_status))
sd_force_run = sd_force.click(lambda: WAIT, None, sd_status, queue=False, **HID).then(
smart_docx_force, [sd_in] + _smart_in, [sd_status, sd_prev, sd_scores, sd_file, sd_dl], **JOB(sd_status))
# rigorous proofread of the humanized file
sd_pr_btn.click(lambda: WAIT, None, sd_pr_status, queue=False, **HID).then(
proofread_doc, [sd_file, sd_in, s_hi, s_mod, user_state], [sd_pr_status, sd_pr_file, sd_pr_dl, sd_pr_panel],
**JOB(sd_pr_status))
hd_pr_btn.click(lambda: WAIT, None, hd_pr_status, queue=False, **HID).then(
proofread_doc, [hd_file, hd_in, s_hi, s_mod, user_state], [hd_pr_status, hd_pr_file, hd_pr_dl, hd_pr_panel],
**JOB(hd_pr_status))
pn_btn.click(lambda: WAIT, None, pn_status, queue=False, **HID).then(
proofread_new, [pn_in, pn_orig, s_hi, s_mod, user_state], [pn_status, pn_file, pn_dl, pn_panel],
**JOB(pn_status))
pt_btn.click(lambda: WAIT, None, pt_status, queue=False, **HID).then(
proofread_text, [pt_in, s_hi, s_mod, user_state], [pt_status, pt_out, pt_file, pt_dl, pt_panel],
**JOB(pt_status))
# detector
dp_run = dp_btn.click(lambda: WAIT, None, dp_status, queue=False, **HID).then(detect_text, [dp_in, d_hi, d_mod, user_state], [dp_status, dp_html, dp_pdf, dp_dl], **JOB(dp_status))
dd_run = dd_btn.click(lambda: WAIT, None, dd_status, queue=False, **HID).then(detect_file, [dd_in, d_hi, d_mod, user_state], [dd_status, dd_html, dd_pdf, dd_dl], **JOB(dd_status))
# humanizer
hp_in.change(count_words_for, [hp_in, user_state], hp_count, queue=False, **HID)
dp_in.change(count_words_for, [dp_in, user_state], dp_count, queue=False, **HID)
hp_run = hp_btn.click(lambda: WAIT, None, hp_status, queue=False, **HID).then(humanize_text, [hp_in, hp_temp, hp_min, hp_mode, hp_full, hp_vocab, user_state],
[hp_out, hp_file, hp_dl, hp_status, hp_scores], **JOB(hp_status))
hd_run = hd_btn.click(lambda: WAIT, None, hd_status, queue=False, **HID).then(humanize_docx, [hd_in, hd_temp, hd_min, hd_mode, hd_full, hd_vocab, user_state],
[hd_file, hd_dl, hd_status, hd_scores, hd_prev], **JOB(hd_status))
# keep the account card's counters current
for ev in (sp_run, sd_run, sd_auto, sp_force_run, sd_force_run, dp_run, dd_run, hp_run, hd_run):
ev.then(lambda u: (_profile(u), _userbar(u), gr.update(visible=not P.state(u)["paid"])),
user_state, [profile_html, user_bar, top_upgrade], **HID)
# pricing
top_upgrade.click(lambda: gr.Tabs(selected="pricing"), None, nav, **HID)
top_admin.click(lambda: gr.Tabs(selected="admin"), None, nav, **HID)
go_pricing.click(lambda: gr.Tabs(selected="pricing"), None, nav, **HID)
go_formatter.click(lambda: gr.Tabs(selected="formatter"), None, nav, **HID)
go_finder.click(lambda: gr.Tabs(selected="finder"), None, nav, **HID)
def save_push(sub_json, u, request: gr.Request = None):
PU.save(PU.cid_from(request), sub_json, u)
def test_push(u, request: gr.Request = None):
PU.send_to(PU.cid_from(request), u, {"title": "🔔 Notifications are on",
"body": "This is how you'll be told your result is ready.",
"tag": "cs-test", "icon": ICON_URL, "url": "/"})
push_save.click(save_push, [push_sub, user_state], None, queue=False, **HID)
stop_srv.click(stop_jobs, [user_state], None, queue=False, **HID)
push_test.click(test_push, [user_state], None, queue=False, **HID)
go_account.click(lambda: gr.Tabs(selected="account"), None, nav, **HID)
def _wallet_view(u):
e = (u or {}).get("email")
return W.status_html(e), (W.history_rows(e) if e else [])
pricing_tab.select(_wallet_view, user_state, [pr_status, pr_hist], **HID)
def _topup(u, amount, request: gr.Request = None):
"""Start a PhonePe payment; the page then goes to PhonePe (pr_url -> js below)."""
if not u or not u.get("email"):
return A._msg("Please sign in (or create a free account) first, so the money goes to your wallet.",
ok=False), ""
import phonepe as PP
if not PP.configured(): # until PhonePe is connected: pay by UPI / WhatsApp,
from urllib.parse import quote # the admin adds it (Admin -> Wallet adjustments)
amount = int(amount or 0)
text = quote(f"Hi, I want to add Rs {amount} to my wallet ({u['email']})")
link = (f"https://wa.me/{P.PAY_WHATSAPP}?text={text}" if P.PAY_WHATSAPP else
f"mailto:{P.PAY_EMAIL}?subject={quote('Wallet top-up')}&body={text}")
return (A._msg(f'Online card/UPI checkout is launching shortly. To add {W.inr(amount * 100)} now, '
f'<a href="{html.escape(link, quote=True)}" target="_blank">message us on WhatsApp</a> and '
'pay by UPI; it is added to your wallet the same day.', ok=True), "")
try:
amount = int(amount or 0)
url = W.start_topup(u["email"], amount)
except ValueError as err:
return A._msg(str(err), ok=False), ""
except Exception as err:
print(f"[wallet] top-up could not start: {err}", flush=True)
return A._msg("PhonePe could not be reached just now. Please try again in a minute.", ok=False), ""
return (A._msg(f"Taking you to PhonePe to add {W.inr(amount * 100)}… If nothing happens, "
f'<a href="{html.escape(url, quote=True)}">continue to PhonePe</a>.', ok=True), url)
_go_js = "(m, u) => { if (u) { window.location.href = u; } return [m, u]; }"
def _topup_of(x):
def go(u, request: gr.Request = None):
return _topup(u, x, request)
return go
for _b, _x in zip(pr_top, W.TOPUPS):
_b.click(_topup_of(_x), user_state, [pr_pay, pr_url], **HID).then(
None, [pr_pay, pr_url], [pr_pay, pr_url], js=_go_js, **HID)
pr_go.click(_topup, [user_state, pr_amount], [pr_pay, pr_url], **HID).then(
None, [pr_pay, pr_url], [pr_pay, pr_url], js=_go_js, **HID)
def _topup_return(u, request: gr.Request = None):
"""Back from PhonePe (?topup=<order>): credit the wallet once PhonePe confirms the payment."""
try:
order = str(dict(request.query_params).get("topup", "")) if request is not None else ""
except Exception:
order = ""
if not order:
return gr.update(), gr.update(), gr.update()
try:
state, email, paise = W.resolve_topup(order)
except Exception as err:
print(f"[wallet] top-up {order} check failed: {err}", flush=True)
state, email, paise = "PENDING", None, 0
if state == "COMPLETED":
msg = (f'<div class="cs-status cs-done">✅ {W.inr(paise)} added to your wallet. Balance: '
f'<b>{W.inr(W.balance(email))}</b>. You can start your run now.</div>')
elif state == "FAILED":
msg = '<div class="cs-status cs-err">⚠️ The payment did not go through, so nothing was charged. ' \
'You can try again below.</div>'
elif state == "UNKNOWN":
return gr.update(), gr.update(), gr.update()
else:
msg = ('<div class="cs-status cs-q">⏳ PhonePe is still confirming your payment. It is added to your '
'wallet automatically within a few minutes; refresh this page to see it.</div>')
return msg, gr.Tabs(selected="pricing"), (W.history_rows(email) if email else [])
demo.load(_topup_return, user_state, [pr_status, nav, pr_hist], **HID)
# admin: plans, credits, requests, payments
def _plan_refresh(u, msg, ok):
return (A._msg(msg, ok=ok), P.admin_requests(u), P.admin_payments(u), A.admin_table(u),
P.admin_subscriptions(u), P.admin_history(u))
def admin_activate(u, email, plan, days, amount):
A._need_admin(u)
ok, msg = P.activate_plan(email, plan, int(days or 1), source="admin", amount=amount or "")
if ok and plan != "free":
st = P.state({"email": (email or "").strip().lower()})
M.send_plan_activated((email or "").strip().lower(), P.PLANS[plan]["name"], P._ist_date(st.get("until")))
return _plan_refresh(u, msg, ok)
def admin_terminate(u, email, reason, notify, confirm):
A._need_admin(u)
if not (email or "").strip():
return _plan_refresh(u, "Enter the user's email first.", False) + (gr.update(),)
if not confirm:
return _plan_refresh(u, "Tick “I confirm: end it now” to terminate the subscription.", False) + \
(gr.update(),)
ok, msg = P.terminate_plan(email, reason, bool(notify))
return _plan_refresh(u, msg, ok) + (gr.update(value=False),)
def admin_settle(u, ref, amount):
A._need_admin(u)
ok, msg = P.settle_charge(ref, source="admin", amount=amount or "")
return (f'<div class="cs-authmsg {"cs-ok" if ok else "cs-bad"}">{html.escape(msg)}</div>',
P.admin_charges(u), P.admin_payments(u))
ad_chg_go.click(admin_settle, [user_state, ad_chg_ref, ad_chg_amt], [ad_chg_msg, ad_chg, ad_pay], **HID)
def admin_wallet_view(u, days):
A._need_admin(u)
return W.report_html(7 if str(days).startswith("7") else 30), W.admin_topups()
def admin_wallet_adjust(u, email, amount, note, days):
A._need_admin(u)
email = (email or "").strip().lower()
if not email or not amount or not (note or "").strip():
msg = A._msg("Enter the email, a non-zero amount and the reason.", ok=False)
else:
W.adjust(email, int(amount) * 100, note.strip(), source=f"admin {u.get('email')}")
msg = A._msg(f"{W.inr(int(amount) * 100)} applied. {email} now has {W.inr(W.balance(email))}.", ok=True)
return (msg,) + admin_wallet_view(u, days)
ad_w_refresh.click(admin_wallet_view, [user_state, ad_w_days], [ad_w_report, ad_w_tops], **HID)
ad_w_days.change(admin_wallet_view, [user_state, ad_w_days], [ad_w_report, ad_w_tops], **HID)
ad_w_go.click(admin_wallet_adjust, [user_state, ad_w_email, ad_w_amt, ad_w_note, ad_w_days],
[ad_w_msg, ad_w_report, ad_w_tops], **HID)
ad_chg_refresh.click(lambda u: P.admin_charges(u), user_state, ad_chg, **HID)
unlock_smart.click(claim_paid, user_state, [sd_status, sd_file, sd_dl], **HID)
unlock_hum.click(claim_paid, user_state, [hd_status, hd_file, hd_dl], **HID)
def admin_credit(u, email, n, amount):
A._need_admin(u)
ok, msg = P.add_credits(email, int(n or 1), source="admin", amount=amount or "")
return _plan_refresh(u, msg, ok)
plan_out = [ad_p_msg, ad_req, ad_pay, ad_table, ad_subs, ad_hist]
ad_p_activate.click(admin_activate, [user_state, ad_p_email, ad_p_plan, ad_p_days, ad_p_amount], plan_out, **HID)
_sub_refresh = lambda u: (P.admin_requests(u), P.admin_payments(u), P.admin_subscriptions(u), P.admin_history(u))
for ev in (ad_sub_tab.select, ad_sub_refresh.click):
ev(_sub_refresh, user_state, [ad_req, ad_pay, ad_subs, ad_hist], **HID)
ad_t_go.click(admin_terminate, [user_state, ad_p_email, ad_t_reason, ad_t_notify, ad_t_confirm],
plan_out + [ad_t_confirm], **HID)
ad_p_credits.click(admin_credit, [user_state, ad_p_email, ad_p_credits_n, ad_p_amount], plan_out, **HID)
admin_tab.select(lambda u: (P.admin_requests(u), P.admin_payments(u), A.admin_reset_requests(u), M.admin_log(u),
f'<div class="cs-gauge-cap" style="text-align:left">{html.escape(H.G.status())}</div>',
f'<div class="cs-gauge-cap" style="text-align:left">{html.escape(H.ADV.status())}</div>',
f'<div class="cs-gauge-cap" style="text-align:left">{html.escape(H.AD.status())}</div>',
f'<div class="cs-gauge-cap" style="text-align:left">{html.escape(PU.status())}</div>',
P.admin_subscriptions(u), P.admin_history(u)),
user_state, [ad_req, ad_pay, ad_rr, ad_m_log, ad_groq, ad_push, ad_subs, ad_hist], **HID)
def admin_test_mail(u, to):
A._need_admin(u)
to = (to or u["email"]).strip()
ok, msg = M.send(to, "Test email", M._wrap("Test email", "<p>Email sending works. 🎉</p>"))
return A._msg(f"Test email to {html.escape(to)}: {html.escape(msg)}", ok=ok), M.admin_log(u)
ad_m_test.click(admin_test_mail, [user_state, ad_m_to], [ad_msg, ad_m_log], **HID)
ad_refresh.click(lambda u: (P.admin_requests(u), P.admin_payments(u), A.admin_reset_requests(u),
P.admin_subscriptions(u), P.admin_history(u)), user_state,
[ad_req, ad_pay, ad_rr, ad_subs, ad_hist], **HID)
account_tab.select(_profile, user_state, profile_html, **HID)
# Jobs of the same kind that may run at once (each button has its own slot pool).
W.start_background() # wallet: settle PhonePe top-ups, free stale reservations
demo.queue(default_concurrency_limit=int(os.getenv("CONCURRENCY", "4")),
max_size=int(os.getenv("QUEUE_SIZE", "60")))
if os.getenv("FAST_SELFTEST") == "1": # timing of the fast engine on the Space's own hardware
import threading
_sizes = tuple(int(x) for x in os.getenv("FAST_SELFTEST_SIZES", "1,3,8").split(",") if x.strip())
threading.Thread(target=F.selftest, args=(_sizes,), daemon=True).start()
def _webhook_order(order_id):
try:
print(f"[wallet] webhook order {order_id}: {W.resolve_topup(order_id)[0]}", flush=True)
except Exception as err:
print(f"[wallet] webhook order {order_id} check failed: {err}", flush=True)
if __name__ == "__main__":
_web, _, _ = demo.launch(show_api=False, ssr_mode=False, allowed_paths=[str(OUT_DIR)], max_threads=80,
favicon_path=str(FAVICON_PATH) if FAVICON_PATH.exists() else None,
prevent_thread_lock=True)
import phonepe as _PP
_PP.install_webhook(_web, _webhook_order) # PhonePe payment updates (POST /phonepe/webhook)
demo.block_thread()