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