""" 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"""
{"".join(gauges)}
Indicators only. 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.
""" EMPTY = """
Scores appear here after processing.
""" def _locked(what): if what == "dashboard": # not an upload: the customer's own history and files return ('
πŸ”’
' '
Sign in to see your dashboard
' '
Your runs, scores, downloads and plan are kept in your free account. ' 'Pasting text works without signing in.
') return (f'
{UI.LOCK_ICON}πŸ”’
' f'
Sign in to upload {what}
' f'
A free account takes a minute, and your first document is free. ' f'No account is needed to paste text: use Paste text above.
') def _userbar(user): if not user: return ('
Guest' f'{P.FREE_TRIAL_DOCS} free document{"" if P.FREE_TRIAL_DOCS == 1 else "s"} after sign-up
') return (f'
{html.escape(user["name"])}' f' {P.badge(user)}
') 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'
My account
' f'
Name{html.escape(user["name"])}
' f'
Email{html.escape(user["email"])}
' f'
Member since{(created or "")[:10]}
' f'
Document runs{runs or 0}
' f'
Pasted-text runs{text_runs or 0}
' f'
Plan{P.badge(user) or "Free"}
' + _limits_row(user) + '
' + f'
{html.escape(P.usage_line(user))}
') def _limits_row(user): if A.is_admin(user): return '
Daily limitUnlimited (admin)
' 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'
Used today{" Β· ".join(parts)}
' 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'
  • βœ“{html.escape(x)}
  • ' for x in perks) btns = (f'' if go else "") + \ f'' modal = (f'') link = (f' {go[0]}' if go else "") return modal + f'
    {icon} {text}{link}
    ' # 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 = ('') return modal + (f'
    βœ… Already human-written (0% AI): nothing was changed. ' f'Rephrase anyway
    ') # ---------------------------------------------------------------- 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 = ('
    πŸ”’ The text is shown after payment. Your scores are above.' '
    ') 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 = ('') if modal else "" return card + (f'
    πŸ”’ Locked until paid: {amount} ({ref}). ' f'Pay now Β· ' f'I have paid: unlock
    ') 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'
    πŸ’³ {W.inr(paid[0])} was taken from your wallet for this run (balance ' f'{W.inr(paid[1])}).
    ' + tip) sent_to = job.extra.get("mailed") if sent_to: tip = f'
    πŸ“§ The result is also being emailed to {html.escape(sent_to)}.
    ' + tip return tip + f'' _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"
    " "πŸ“ Format for a journal" "Word file + Overleaf-ready LaTeX in the journal's format" "πŸ”Ž Find a journal" "Journals that match your title, with indexing and acceptance odds
    ") return (f"
    " f"πŸ“ Format for a journal" "Arrange your humanized file to a journal's template" f"πŸ”Ž Find a journal" "Journals that match your title, with indexing and acceptance odds
    ") def companion_tab_html(url, what, how): embed = url.rstrip("/") + "/?embed=true" return (f"" f"
    {what} {how}
    " f"
    β†— Open in a new tab" "Recommended for large files
    " f"" "

    If the window shows a wake-up message, press the button inside it and wait a few seconds.

    ") # ---------------- 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": 'Rephrased', "kept": 'Kept: no safe rewrite', "locked": f'Kept: contains a {html.escape(r.get("reason") or "citation field or link")}' f' (not edited, to keep it intact)'}[r["status"]] if r.get("likely"): st += ('⚠ Likely still flagged: revise this sentence in your own words ' '(change its structure, add your own reasoning or an example)') rows.append(f'{i}{html.escape(lvl)}' f'
    {html.escape(r["orig"])}
    ' f'
    {html.escape(r["new"]) if r["status"] == "rephrased" else "β€”"}
    ' f'{st}') 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'
    {title} and how each was rephrased' f'
    ' '' + "".join(rows) + '
    #{head}Rephrased
    ') # 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'logo' if LOGO_SRC else "" _title = html.escape(APP_NAME) _title = _title.rsplit(" ", 1)[0] + f" {_title.rsplit(' ', 1)[-1]}" if " " in _title else _title HEADER = f"""
    {_logo}

    {_title}

    Detect AI-written passages and humanize them β€” sentence by sentence, with your Word layout kept intact.

    """ BRAND_HTML = (f'
    {_logo}
    {_title}
    ' '
    AI detection and humanizing for academic writing
    ') FOOTER = f""" """ # Unscoped CSS (Gradio prefixes the css= string, so the outer containers are pinned here) HEAD = """ """ # 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 += """""" HEAD += (f'') 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 = ('
    ' '
    You\'re in the queue
    ' '
    Getting a slot ready for you…
    ' '
    ' '
    πŸ”” You can switch tabs or apps β€” we\'ll notify you the moment it\'s ready.
    ') 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('

    Make AI-flagged writing read as your own

    ' '

    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.

    ' '
    1Drop your .docx2We rewrite and ' 're-check3Download your file
    ' '
    Meaning checked sentence by sentence' 'Formatting, tables and equations untouchedYour files are not kept for training' '
    ') 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('
    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.
    ') 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("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.") if P.PAY_PER_USE: gr.Markdown("πŸ’³ No credits or plan? 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.") 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("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.") 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('
    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.' '
    ') 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('
    AI Writing Report. Sentence-level highlighting ' '(cyan = high, purple = moderate) plus a downloadable PDF report. Results are probabilistic indicators.
    ') 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('
    Full paraphrase. 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 Smart Humanize.
    ') 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('
    Add money to your wallet
    ') 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('
    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.
    ') # ============================ 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('
    My dashboard
    ') 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('
    ⬇️ Download a previous result
    ' '
    Files are kept until the ' 'server restarts.
    ') 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('
    🧾 My history (last 100 runs)
    ') 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('
    Welcome
    ' '
    Sign in or create a free account to upload documents. ' 'Pasting text never needs an account.
    ') 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('
    ' + ('Get a 6-digit code by email, or use the recovery key you saved ' 'when you created your account.' if M.ENABLED else 'Use the recovery key you saved when you created your account.') + '
    ') 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('
    The admin will check your request and send you ' 'a temporary password. Change it after signing in (Account β†’ Change ' 'password).
    ') 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('
    Lets you reset your password yourself if you forget it. ' 'Creating a new key cancels the old one.
    ') 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'
    {A.STORAGE_NOTE}
    ') with gr.Tabs(elem_classes="sub-tabs"): # ---------- overview ---------- with gr.Tab("Overview") as ad_ov_tab: with gr.Row(): gr.HTML('
    Platform overview
    ') 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('
    πŸ† Most active users (30 days)
    ') 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('
    πŸ’Ž Activate / renew / terminate a subscription
    ' '
    Activate / renew: after a UPI / ' 'bank payment, choose the plan and number of months. Renewing an active plan adds the ' 'months to its end date. Terminate: ends a paid subscription immediately ' '(refund, misuse, user request); the user goes back to the free plan.
    ') 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('
    πŸ“₯ Upgrade requests
    ') ad_req = gr.Dataframe(headers=P.REQ_COLS, interactive=False, wrap=True, max_height=320) with gr.Column(elem_classes="cs-card"): gr.HTML('
    πŸ’° Payments log
    ') ad_pay = gr.Dataframe(headers=P.PAY_COLS, interactive=False, wrap=True, max_height=320) with gr.Column(elem_classes="cs-card"): gr.HTML('
    πŸ’³ Pay-per-use charges
    ' '
    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.
    ') 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('
    πŸ‘› Wallet: profit, top-ups, adjustments
    ') 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('
    πŸ—“οΈ Subscriptions (start date, paid until, status)' '
    ') 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('
    πŸ”Ž User details
    ') 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('
    Daily limits for a user
    ' '
    Blank = default, 0 = unlimited
    ') 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('
    Enable / disable account
    ') 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('
    Reset password
    ') 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('
    Password reset requests from ' 'users without a recovery key β€” set a temporary password above and send it ' 'to the contact shown.
    ') 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('
    πŸ“§ Email
    ' f'
    {M.status()}
    ') 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('
    βœ… Language engine status (admin only)
    ') ad_groq = gr.HTML() with gr.Column(elem_classes="cs-card"): gr.HTML('
    πŸ”” Result notifications
    ') ad_push = gr.HTML() with gr.Column(elem_classes="cs-card"): gr.HTML('
    Delete account
    ') 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'
    {P.badge(u)} {html.escape(P.usage_line(u))}
    ' 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'message us on WhatsApp 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'continue to PhonePe.', 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=): 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'
    βœ… {W.inr(paise)} added to your wallet. Balance: ' f'{W.inr(W.balance(email))}. You can start your run now.
    ') elif state == "FAILED": msg = '
    ⚠️ The payment did not go through, so nothing was charged. ' \ 'You can try again below.
    ' elif state == "UNKNOWN": return gr.update(), gr.update(), gr.update() else: msg = ('
    ⏳ PhonePe is still confirming your payment. It is added to your ' 'wallet automatically within a few minutes; refresh this page to see it.
    ') 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'
    {html.escape(msg)}
    ', 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'
    {html.escape(H.G.status())}
    ', f'
    {html.escape(H.ADV.status())}
    ', f'
    {html.escape(H.AD.status())}
    ', f'
    {html.escape(PU.status())}
    ', 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", "

    Email sending works. πŸŽ‰

    ")) 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()