Spaces:
Paused
Paused
Upload 3 files
Browse files
app.py
CHANGED
|
@@ -104,11 +104,55 @@ def _limits_row(user):
|
|
| 104 |
# =====================================================================
|
| 105 |
# Handlers — status panel streaming via H._run_live
|
| 106 |
# =====================================================================
|
| 107 |
-
def
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
meta = {}
|
| 113 |
|
| 114 |
def wrapped(*a): # cores record details into rep.meta
|
|
@@ -177,9 +221,9 @@ def _smart_docx_core(path, target, passes, temp, mode, hi, mod, rep):
|
|
| 177 |
|
| 178 |
|
| 179 |
def smart_text(text, target, passes, temp, mode, hi, mod, user, request: gr.Request = None):
|
| 180 |
-
P.check_paste(user, text)
|
| 181 |
passes = P.max_passes(user, passes)
|
| 182 |
-
yield from _stream(_smart_text_core, (text, target, passes, temp, mode, hi, mod), 6, 0, user, request=request,
|
|
|
|
| 183 |
|
| 184 |
|
| 185 |
def smart_docx(path, target, passes, temp, mode, hi, mod, user, request: gr.Request = None):
|
|
@@ -201,13 +245,16 @@ def _det_core(file, text, hi, mod, rep):
|
|
| 201 |
def detect_text(text, hi, mod, user, request: gr.Request = None):
|
| 202 |
if not (text or "").strip():
|
| 203 |
raise gr.Error("Please paste some text.")
|
| 204 |
-
|
| 205 |
-
yield from _stream(_det_core, (None, text, hi, mod), 4, 0, user, request=request, tool="AI Detector")
|
| 206 |
|
| 207 |
|
| 208 |
def detect_file(file, hi, mod, user, request: gr.Request = None):
|
| 209 |
if not file:
|
| 210 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 211 |
raise gr.Error("Please upload a .docx, .pdf or .txt file.")
|
| 212 |
yield from _stream(_det_core, (file, None, hi, mod), 4, 0, user, True, request, tool="AI Detector",
|
| 213 |
words=P.doc_words(file))
|
|
@@ -240,9 +287,8 @@ def _hum_docx_core(path, temp, min_words, mode, rep):
|
|
| 240 |
def humanize_text(text, temp, min_words, mode, user, request: gr.Request = None):
|
| 241 |
if not (text or "").strip():
|
| 242 |
raise gr.Error("Please paste some text to paraphrase.")
|
| 243 |
-
P.check_paste(user, text)
|
| 244 |
yield from _stream(_hum_text_core, (text, temp, min_words, mode, P.paste_limit(user)), 5, 3, user,
|
| 245 |
-
request=request, tool="Humanizer")
|
| 246 |
|
| 247 |
|
| 248 |
def humanize_docx(path, temp, min_words, mode, user, request: gr.Request = None):
|
|
@@ -578,6 +624,59 @@ FOOTER = f"""
|
|
| 578 |
|
| 579 |
# Unscoped CSS (Gradio prefixes the css= string, so the outer containers are pinned here)
|
| 580 |
HEAD = """<style>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 581 |
@media (max-width: 700px) {
|
| 582 |
html, body { overflow-x: hidden !important; }
|
| 583 |
.gradio-container { min-width: 0 !important; width: 100% !important; max-width: 100vw !important;
|
|
@@ -587,7 +686,38 @@ HEAD = """<style>
|
|
| 587 |
min-width: 0 !important; max-width: 100% !important; }
|
| 588 |
.gradio-container .table-wrap { max-width: 100% !important; overflow-x: auto !important; }
|
| 589 |
}
|
| 590 |
-
</style>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 591 |
|
| 592 |
theme = gr.themes.Soft(primary_hue="indigo", secondary_hue="pink", neutral_hue="slate",
|
| 593 |
font=[gr.themes.GoogleFont("Poppins"), "ui-sans-serif", "sans-serif"], radius_size="lg")
|
|
@@ -631,6 +761,8 @@ with gr.Blocks(title=f"{APP_NAME} · CS Software Solutions", theme=theme, css=CS
|
|
| 631 |
top_signin = gr.Button("🔐 Sign in", size="sm", elem_classes="cs-topbtn", scale=0)
|
| 632 |
top_upgrade = gr.Button("💎 Upgrade", size="sm", elem_classes="cs-topbtn cs-upbtn", scale=0)
|
| 633 |
top_logout = gr.Button("Log out", size="sm", elem_classes="cs-topbtn", visible=False, scale=0)
|
|
|
|
|
|
|
| 634 |
|
| 635 |
with gr.Tabs(elem_id="cs-nav", selected="smart") as nav:
|
| 636 |
# ============================ SMART HUMANIZE ============================
|
|
@@ -1100,6 +1232,8 @@ with gr.Blocks(title=f"{APP_NAME} · CS Software Solutions", theme=theme, css=CS
|
|
| 1100 |
|
| 1101 |
# pricing
|
| 1102 |
top_upgrade.click(lambda: gr.Tabs(selected="pricing"), None, nav, **HID)
|
|
|
|
|
|
|
| 1103 |
pricing_tab.select(lambda u: f'<div class="cs-nudge">{html.escape(P.usage_line(u))}</div>', user_state,
|
| 1104 |
pr_status, **HID)
|
| 1105 |
for b, item in ((pr_student, "Student plan"), (pr_pro, "Pro researcher plan"), (pr_agency, "Agency plan"),
|
|
|
|
| 104 |
# =====================================================================
|
| 105 |
# Handlers — status panel streaming via H._run_live
|
| 106 |
# =====================================================================
|
| 107 |
+
def upsell_html(up):
|
| 108 |
+
"""Centred, animated pop-up for plan / sign-in / limit messages (never shown as an error)."""
|
| 109 |
+
kind = getattr(up, "kind", "upgrade")
|
| 110 |
+
icon = {"upgrade": "💎", "signin": "🔐", "limit": "⏳"}.get(kind, "✨")
|
| 111 |
+
title = html.escape(getattr(up, "headline", "") or "Heads up")
|
| 112 |
+
text = html.escape(getattr(up, "text", "") or str(up))
|
| 113 |
+
st = P.PLANS["student"]
|
| 114 |
+
perks = {
|
| 115 |
+
"upgrade": [f"{st['docs']} documents a month", f"{st['paste_words']:,}-word pastes",
|
| 116 |
+
"Word layout kept + PDF report", f"from ${st['usd']}/mo · ₹{st['inr']}"],
|
| 117 |
+
"signin": [f"{P.FREE_TRIAL_DOCS} documents free", "Word & PDF upload", "Run history & re-downloads",
|
| 118 |
+
"No card needed"],
|
| 119 |
+
}.get(kind, [])
|
| 120 |
+
go = {"upgrade": ("💎 See plans", "cs-go-pricing"),
|
| 121 |
+
"signin": ("🔐 Sign in / Create free account", "cs-go-account")}.get(kind)
|
| 122 |
+
perks_html = "".join(f'<li><span>✓</span>{html.escape(x)}</li>' for x in perks)
|
| 123 |
+
btns = (f'<button class="cs-up-go" data-cs-go="{go[1]}">{go[0]}</button>' if go else "") + \
|
| 124 |
+
f'<button class="cs-up-later" data-cs-close="1">{"Maybe later" if go else "OK"}</button>'
|
| 125 |
+
modal = (f'<div class="cs-upsell cs-up-{kind}" role="dialog" aria-modal="true" aria-label="{title}">'
|
| 126 |
+
'<div class="cs-up-backdrop" data-cs-close="1"></div>'
|
| 127 |
+
'<div class="cs-up-card"><div class="cs-up-ring"></div>'
|
| 128 |
+
'<button class="cs-up-x" data-cs-close="1" aria-label="Close">×</button>'
|
| 129 |
+
f'<div class="cs-up-icon">{icon}</div><div class="cs-up-title">{title}</div>'
|
| 130 |
+
f'<div class="cs-up-text">{text}</div>'
|
| 131 |
+
+ (f'<ul class="cs-up-perks">{perks_html}</ul>' if perks_html else "")
|
| 132 |
+
+ f'<div class="cs-up-actions">{btns}</div>'
|
| 133 |
+
'<i class="cs-sp s1">✦</i><i class="cs-sp s2">✦</i><i class="cs-sp s3">✧</i><i class="cs-sp s4">✦</i>'
|
| 134 |
+
'</div></div>')
|
| 135 |
+
link = (f' <a class="cs-up-link" data-cs-go="{go[1]}">{go[0]}</a>' if go else "")
|
| 136 |
+
return modal + f'<div class="cs-nudge">{icon} {text}{link}</div>'
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
def _upsell_frame(up, n, idx):
|
| 140 |
+
frame = [gr.update()] * n
|
| 141 |
+
frame[idx] = upsell_html(up)
|
| 142 |
+
return tuple(frame)
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
def _stream(core, args, n, idx, user=None, need_login=False, request=None, tool="", words=None, paste=None):
|
| 146 |
+
try:
|
| 147 |
+
if paste is not None:
|
| 148 |
+
P.check_paste(user, paste) # words per paste for this plan
|
| 149 |
+
if need_login:
|
| 150 |
+
A._require(user)
|
| 151 |
+
P.check_doc(user, words) # plan quota / credits / free trial + word cap
|
| 152 |
+
A.check_limit(user, request, need_login)
|
| 153 |
+
except A.Upsell as up: # plan / sign-in / limit -> friendly pop-up, not an error
|
| 154 |
+
yield _upsell_frame(up, n, idx)
|
| 155 |
+
return
|
| 156 |
meta = {}
|
| 157 |
|
| 158 |
def wrapped(*a): # cores record details into rep.meta
|
|
|
|
| 221 |
|
| 222 |
|
| 223 |
def smart_text(text, target, passes, temp, mode, hi, mod, user, request: gr.Request = None):
|
|
|
|
| 224 |
passes = P.max_passes(user, passes)
|
| 225 |
+
yield from _stream(_smart_text_core, (text, target, passes, temp, mode, hi, mod), 6, 0, user, request=request,
|
| 226 |
+
tool="Smart Humanize", paste=text)
|
| 227 |
|
| 228 |
|
| 229 |
def smart_docx(path, target, passes, temp, mode, hi, mod, user, request: gr.Request = None):
|
|
|
|
| 245 |
def detect_text(text, hi, mod, user, request: gr.Request = None):
|
| 246 |
if not (text or "").strip():
|
| 247 |
raise gr.Error("Please paste some text.")
|
| 248 |
+
yield from _stream(_det_core, (None, text, hi, mod), 4, 0, user, request=request, tool="AI Detector", paste=text)
|
|
|
|
| 249 |
|
| 250 |
|
| 251 |
def detect_file(file, hi, mod, user, request: gr.Request = None):
|
| 252 |
if not file:
|
| 253 |
+
try:
|
| 254 |
+
A._require(user)
|
| 255 |
+
except A.Upsell as up:
|
| 256 |
+
yield _upsell_frame(up, 4, 0)
|
| 257 |
+
return
|
| 258 |
raise gr.Error("Please upload a .docx, .pdf or .txt file.")
|
| 259 |
yield from _stream(_det_core, (file, None, hi, mod), 4, 0, user, True, request, tool="AI Detector",
|
| 260 |
words=P.doc_words(file))
|
|
|
|
| 287 |
def humanize_text(text, temp, min_words, mode, user, request: gr.Request = None):
|
| 288 |
if not (text or "").strip():
|
| 289 |
raise gr.Error("Please paste some text to paraphrase.")
|
|
|
|
| 290 |
yield from _stream(_hum_text_core, (text, temp, min_words, mode, P.paste_limit(user)), 5, 3, user,
|
| 291 |
+
request=request, tool="Humanizer", paste=text)
|
| 292 |
|
| 293 |
|
| 294 |
def humanize_docx(path, temp, min_words, mode, user, request: gr.Request = None):
|
|
|
|
| 624 |
|
| 625 |
# Unscoped CSS (Gradio prefixes the css= string, so the outer containers are pinned here)
|
| 626 |
HEAD = """<style>
|
| 627 |
+
#cs-go-pricing, #cs-go-account, .cs-hidden-nav { display: none !important; }
|
| 628 |
+
.cs-upsell { position: fixed; inset: 0; z-index: 10000; display: flex; align-items: center; justify-content: center;
|
| 629 |
+
padding: 16px; font-family: Poppins, ui-sans-serif, sans-serif; }
|
| 630 |
+
.cs-up-backdrop { position: absolute; inset: 0; background: rgba(12, 16, 45, .55); backdrop-filter: blur(4px);
|
| 631 |
+
-webkit-backdrop-filter: blur(4px); animation: csFade .3s ease both; }
|
| 632 |
+
.cs-up-card { position: relative; width: min(440px, 100%); background: #fff; border-radius: 22px;
|
| 633 |
+
padding: 30px 26px 22px; text-align: center; color: #1d2040; overflow: hidden;
|
| 634 |
+
box-shadow: 0 30px 80px rgba(7, 34, 139, .35); animation: csPop .55s cubic-bezier(.2, 1.3, .35, 1) both; }
|
| 635 |
+
.cs-up-ring { position: absolute; inset: 0; border-radius: 22px; padding: 2px; pointer-events: none;
|
| 636 |
+
background: conic-gradient(from var(--a, 0deg), #07228B, #47148B, #E92E7F, #50B4EF, #07228B);
|
| 637 |
+
-webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
|
| 638 |
+
-webkit-mask-composite: xor; mask-composite: exclude; animation: csSpin 4s linear infinite; }
|
| 639 |
+
@property --a { syntax: '<angle>'; initial-value: 0deg; inherits: false; }
|
| 640 |
+
.cs-up-icon { font-size: 46px; line-height: 1; display: inline-block; margin-bottom: 10px;
|
| 641 |
+
animation: csFloat 2.4s ease-in-out infinite; filter: drop-shadow(0 8px 14px rgba(233, 46, 127, .35)); }
|
| 642 |
+
.cs-up-title { font-size: 21px; font-weight: 700; margin-bottom: 8px;
|
| 643 |
+
background: linear-gradient(90deg, #07228B, #47148B 55%, #E92E7F); -webkit-background-clip: text;
|
| 644 |
+
background-clip: text; color: transparent; }
|
| 645 |
+
.cs-up-text { font-size: 14.5px; line-height: 1.6; color: #444a6b; margin-bottom: 14px; }
|
| 646 |
+
.cs-up-perks { list-style: none; padding: 0; margin: 0 0 18px; display: grid; grid-template-columns: 1fr 1fr;
|
| 647 |
+
gap: 8px; text-align: left; }
|
| 648 |
+
.cs-up-perks li { font-size: 13px; background: #f5f3ff; border-radius: 10px; padding: 8px 10px; color: #2b2f55;
|
| 649 |
+
opacity: 0; animation: csRise .45s ease forwards; }
|
| 650 |
+
.cs-up-perks li:nth-child(1) { animation-delay: .25s } .cs-up-perks li:nth-child(2) { animation-delay: .35s }
|
| 651 |
+
.cs-up-perks li:nth-child(3) { animation-delay: .45s } .cs-up-perks li:nth-child(4) { animation-delay: .55s }
|
| 652 |
+
.cs-up-perks span { color: #E92E7F; font-weight: 800; margin-right: 6px; }
|
| 653 |
+
.cs-up-actions { display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; }
|
| 654 |
+
.cs-up-go { border: 0; cursor: pointer; color: #fff; font-weight: 700; font-size: 15px; padding: 12px 22px;
|
| 655 |
+
border-radius: 12px; background: linear-gradient(90deg, #07228B, #47148B 60%, #E92E7F); background-size: 200% 100%;
|
| 656 |
+
box-shadow: 0 10px 24px rgba(233, 46, 127, .35); animation: csShine 3s ease-in-out infinite; }
|
| 657 |
+
.cs-up-go:hover { transform: translateY(-1px); }
|
| 658 |
+
.cs-up-later { border: 1px solid #e3e5f2; background: #fff; color: #555b7c; cursor: pointer; font-size: 14px;
|
| 659 |
+
padding: 12px 18px; border-radius: 12px; }
|
| 660 |
+
.cs-up-x { position: absolute; top: 10px; right: 12px; border: 0; background: transparent; font-size: 24px;
|
| 661 |
+
color: #9aa0bd; cursor: pointer; line-height: 1; }
|
| 662 |
+
.cs-sp { position: absolute; font-style: normal; color: #E92E7F; opacity: 0; animation: csTwinkle 2.6s ease-in-out infinite; }
|
| 663 |
+
.cs-sp.s1 { top: 18px; left: 26px; } .cs-sp.s2 { top: 60px; right: 40px; color: #50B4EF; animation-delay: .7s; }
|
| 664 |
+
.cs-sp.s3 { bottom: 70px; left: 18px; color: #47148B; animation-delay: 1.3s; }
|
| 665 |
+
.cs-sp.s4 { bottom: 26px; right: 22px; animation-delay: 1.9s; }
|
| 666 |
+
.cs-upsell.cs-out .cs-up-card { animation: csPopOut .22s ease forwards; }
|
| 667 |
+
.cs-upsell.cs-out .cs-up-backdrop { animation: csFadeOut .22s ease forwards; }
|
| 668 |
+
.cs-up-link { cursor: pointer; font-weight: 600; color: #47148B; text-decoration: underline; margin-left: 4px; }
|
| 669 |
+
@keyframes csFade { from { opacity: 0 } to { opacity: 1 } }
|
| 670 |
+
@keyframes csFadeOut { to { opacity: 0 } }
|
| 671 |
+
@keyframes csPop { from { opacity: 0; transform: translateY(24px) scale(.85) } to { opacity: 1; transform: none } }
|
| 672 |
+
@keyframes csPopOut { to { opacity: 0; transform: translateY(12px) scale(.92) } }
|
| 673 |
+
@keyframes csSpin { to { --a: 360deg } }
|
| 674 |
+
@keyframes csFloat { 0%, 100% { transform: translateY(0) rotate(-4deg) } 50% { transform: translateY(-8px) rotate(4deg) } }
|
| 675 |
+
@keyframes csRise { from { opacity: 0; transform: translateY(8px) } to { opacity: 1; transform: none } }
|
| 676 |
+
@keyframes csShine { 0%, 100% { background-position: 0% 0 } 50% { background-position: 100% 0 } }
|
| 677 |
+
@keyframes csTwinkle { 0%, 100% { opacity: 0; transform: scale(.6) } 50% { opacity: .9; transform: scale(1.1) } }
|
| 678 |
+
@media (max-width: 480px) { .cs-up-perks { grid-template-columns: 1fr; } .cs-up-card { padding: 26px 18px 18px; } }
|
| 679 |
+
@media (prefers-reduced-motion: reduce) { .cs-upsell * { animation: none !important; opacity: 1 !important; } }
|
| 680 |
@media (max-width: 700px) {
|
| 681 |
html, body { overflow-x: hidden !important; }
|
| 682 |
.gradio-container { min-width: 0 !important; width: 100% !important; max-width: 100vw !important;
|
|
|
|
| 686 |
min-width: 0 !important; max-width: 100% !important; }
|
| 687 |
.gradio-container .table-wrap { max-width: 100% !important; overflow-x: auto !important; }
|
| 688 |
}
|
| 689 |
+
</style>
|
| 690 |
+
<script>
|
| 691 |
+
(function () {
|
| 692 |
+
// The pop-up arrives inside a status panel; move it to <body> so it is centred on the whole page.
|
| 693 |
+
function mount() {
|
| 694 |
+
document.querySelectorAll('.cs-upsell').forEach(function (m) {
|
| 695 |
+
if (m.parentElement === document.body) return;
|
| 696 |
+
document.querySelectorAll('body > .cs-upsell').forEach(function (o) { o.remove(); });
|
| 697 |
+
document.body.appendChild(m);
|
| 698 |
+
});
|
| 699 |
+
}
|
| 700 |
+
function closeAll(el) {
|
| 701 |
+
var list = el ? [el] : Array.prototype.slice.call(document.querySelectorAll('body > .cs-upsell'));
|
| 702 |
+
list.forEach(function (m) { m.classList.add('cs-out'); setTimeout(function () { m.remove(); }, 230); });
|
| 703 |
+
}
|
| 704 |
+
function start() {
|
| 705 |
+
new MutationObserver(mount).observe(document.body, { childList: true, subtree: true });
|
| 706 |
+
document.addEventListener('click', function (e) {
|
| 707 |
+
var go = e.target.closest('[data-cs-go]');
|
| 708 |
+
var close = e.target.closest('[data-cs-close]');
|
| 709 |
+
if (go) {
|
| 710 |
+
var b = document.getElementById(go.getAttribute('data-cs-go'));
|
| 711 |
+
if (b) { (b.tagName === 'BUTTON' ? b : (b.querySelector('button') || b)).click(); }
|
| 712 |
+
window.scrollTo({ top: 0, behavior: 'smooth' });
|
| 713 |
+
}
|
| 714 |
+
if (go || close) { closeAll(e.target.closest('.cs-upsell')); }
|
| 715 |
+
});
|
| 716 |
+
document.addEventListener('keydown', function (e) { if (e.key === 'Escape') closeAll(); });
|
| 717 |
+
}
|
| 718 |
+
if (document.body) start(); else document.addEventListener('DOMContentLoaded', start);
|
| 719 |
+
})();
|
| 720 |
+
</script>"""
|
| 721 |
|
| 722 |
theme = gr.themes.Soft(primary_hue="indigo", secondary_hue="pink", neutral_hue="slate",
|
| 723 |
font=[gr.themes.GoogleFont("Poppins"), "ui-sans-serif", "sans-serif"], radius_size="lg")
|
|
|
|
| 761 |
top_signin = gr.Button("🔐 Sign in", size="sm", elem_classes="cs-topbtn", scale=0)
|
| 762 |
top_upgrade = gr.Button("💎 Upgrade", size="sm", elem_classes="cs-topbtn cs-upbtn", scale=0)
|
| 763 |
top_logout = gr.Button("Log out", size="sm", elem_classes="cs-topbtn", visible=False, scale=0)
|
| 764 |
+
go_pricing = gr.Button("pricing", elem_id="cs-go-pricing", elem_classes="cs-hidden-nav", scale=0)
|
| 765 |
+
go_account = gr.Button("account", elem_id="cs-go-account", elem_classes="cs-hidden-nav", scale=0)
|
| 766 |
|
| 767 |
with gr.Tabs(elem_id="cs-nav", selected="smart") as nav:
|
| 768 |
# ============================ SMART HUMANIZE ============================
|
|
|
|
| 1232 |
|
| 1233 |
# pricing
|
| 1234 |
top_upgrade.click(lambda: gr.Tabs(selected="pricing"), None, nav, **HID)
|
| 1235 |
+
go_pricing.click(lambda: gr.Tabs(selected="pricing"), None, nav, **HID)
|
| 1236 |
+
go_account.click(lambda: gr.Tabs(selected="account"), None, nav, **HID)
|
| 1237 |
pricing_tab.select(lambda u: f'<div class="cs-nudge">{html.escape(P.usage_line(u))}</div>', user_state,
|
| 1238 |
pr_status, **HID)
|
| 1239 |
for b, item in ((pr_student, "Student plan"), (pr_pro, "Pro researcher plan"), (pr_agency, "Agency plan"),
|
auth.py
CHANGED
|
@@ -199,9 +199,21 @@ def _count_run(user, document=True):
|
|
| 199 |
pass
|
| 200 |
|
| 201 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 202 |
def _require(user):
|
| 203 |
if not user or not user.get("email"):
|
| 204 |
-
raise
|
|
|
|
| 205 |
with _db() as con:
|
| 206 |
row = con.execute("SELECT active FROM users WHERE email=?", (user["email"],)).fetchone()
|
| 207 |
if not row or row[0] == 0:
|
|
@@ -408,8 +420,13 @@ def check_limit(user, request, document):
|
|
| 408 |
return
|
| 409 |
if usage_today(_who(user, request), kind) >= lim:
|
| 410 |
what = "document runs" if document else "pasted-text runs"
|
| 411 |
-
|
| 412 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 413 |
|
| 414 |
|
| 415 |
def add_usage(user, request, document):
|
|
|
|
| 199 |
pass
|
| 200 |
|
| 201 |
|
| 202 |
+
class Upsell(gr.Error):
|
| 203 |
+
"""Not an error: a plan / sign-in / limit message. The app shows it as a friendly centred pop-up.
|
| 204 |
+
kind: "upgrade" (paid plan unlocks it), "signin" (free account unlocks it), "limit" (try again later)."""
|
| 205 |
+
|
| 206 |
+
def __init__(self, message, kind="upgrade", title=None):
|
| 207 |
+
titles = {"upgrade": "Unlock more with a plan", "signin": "Create your free account",
|
| 208 |
+
"limit": "You've reached today's limit"}
|
| 209 |
+
super().__init__(message, title=title or titles.get(kind, "Heads up"), print_exception=False)
|
| 210 |
+
self.kind, self.headline, self.text = kind, title or titles.get(kind, "Heads up"), message
|
| 211 |
+
|
| 212 |
+
|
| 213 |
def _require(user):
|
| 214 |
if not user or not user.get("email"):
|
| 215 |
+
raise Upsell("Sign in or create a free account to upload Word and PDF documents — it takes 20 seconds. "
|
| 216 |
+
"Pasting text works without an account.", kind="signin", title="Upload documents for free")
|
| 217 |
with _db() as con:
|
| 218 |
row = con.execute("SELECT active FROM users WHERE email=?", (user["email"],)).fetchone()
|
| 219 |
if not row or row[0] == 0:
|
|
|
|
| 420 |
return
|
| 421 |
if usage_today(_who(user, request), kind) >= lim:
|
| 422 |
what = "document runs" if document else "pasted-text runs"
|
| 423 |
+
if not user:
|
| 424 |
+
raise Upsell(f"Guests can do {lim} {what} a day. Create a free account for a higher limit — "
|
| 425 |
+
"or come back after 00:00 UTC.", kind="signin", title="You've reached today's free limit")
|
| 426 |
+
paid = P.state(user)["key"] in P.PAID
|
| 427 |
+
raise Upsell(f"You've used all {lim} {what} for today. It resets at 00:00 UTC."
|
| 428 |
+
+ ("" if paid else " Paid plans have much higher daily limits."),
|
| 429 |
+
kind="limit" if paid else "upgrade", title="You've reached today's limit")
|
| 430 |
|
| 431 |
|
| 432 |
def add_usage(user, request, document):
|
plans.py
CHANGED
|
@@ -115,15 +115,16 @@ def check_doc(user, words=None):
|
|
| 115 |
return
|
| 116 |
if docs_available(st) <= 0:
|
| 117 |
if st["key"] in PAID:
|
| 118 |
-
raise
|
| 119 |
-
|
| 120 |
-
raise
|
| 121 |
-
|
| 122 |
-
|
|
|
|
| 123 |
cap = st["doc_words"] if (st["key"] in PAID and st["used"] < st["docs"]) else SINGLE["doc_words"]
|
| 124 |
if words and words > cap:
|
| 125 |
-
raise
|
| 126 |
-
|
| 127 |
|
| 128 |
|
| 129 |
def paste_limit(user):
|
|
@@ -135,10 +136,11 @@ def check_paste(user, text):
|
|
| 135 |
n = len((text or "").split())
|
| 136 |
if n > st["paste_words"]:
|
| 137 |
if st["key"] in ("guest", "free"):
|
| 138 |
-
raise
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
|
|
|
| 142 |
|
| 143 |
|
| 144 |
def daily_paste_limit(user):
|
|
|
|
| 115 |
return
|
| 116 |
if docs_available(st) <= 0:
|
| 117 |
if st["key"] in PAID:
|
| 118 |
+
raise A.Upsell(f"You've used all {st['docs']} documents in this 30-day period. Buy a single document "
|
| 119 |
+
"or move up a plan to keep going.", title="Your monthly documents are used up")
|
| 120 |
+
raise A.Upsell(f"You've used your {FREE_TRIAL_DOCS} free documents. Plans start at "
|
| 121 |
+
f"${PLANS['student']['usd']}/month (₹{PLANS['student']['inr']}), or "
|
| 122 |
+
f"${SINGLE['usd']} (₹{SINGLE['inr']}) for a single document.",
|
| 123 |
+
title="Loved your free documents?")
|
| 124 |
cap = st["doc_words"] if (st["key"] in PAID and st["used"] < st["docs"]) else SINGLE["doc_words"]
|
| 125 |
if words and words > cap:
|
| 126 |
+
raise A.Upsell(f"This document has {words:,} words; your plan allows up to {cap:,} words per document. "
|
| 127 |
+
"A bigger plan handles longer documents.", title="This document is a bit long")
|
| 128 |
|
| 129 |
|
| 130 |
def paste_limit(user):
|
|
|
|
| 136 |
n = len((text or "").split())
|
| 137 |
if n > st["paste_words"]:
|
| 138 |
if st["key"] in ("guest", "free"):
|
| 139 |
+
raise A.Upsell(f"Free pastes are up to {st['paste_words']} words (yours: {n:,}). The Student plan allows "
|
| 140 |
+
f"{PLANS['student']['paste_words']:,} words per paste — or upload a .docx.",
|
| 141 |
+
title="Your text is longer than the free limit")
|
| 142 |
+
raise A.Upsell(f"Your plan allows {st['paste_words']:,} words per paste (yours: {n:,}). Upload a .docx for "
|
| 143 |
+
"longer text.", kind="limit", title="Your text is a bit long")
|
| 144 |
|
| 145 |
|
| 146 |
def daily_paste_limit(user):
|