P0-R5-R2 no historical completed UI

#139
Files changed (5) hide show
  1. .gitattributes +1 -0
  2. app.py +24 -10
  3. i18n.py +78 -54
  4. managed_auth.py +118 -25
  5. static/payment_wechat_qr.png +3 -0
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ static/payment_wechat_qr.png filter=lfs diff=lfs merge=lfs -text
app.py CHANGED
@@ -821,11 +821,9 @@ def _process_pending_launch(lang_code: str) -> None:
821
  if "product_entitlement_required" in raw:
822
  st.session_state["sp_payment_required"] = True
823
  st.error(tr(lang_code, "product_entitlement_required"))
824
- render_payment_purchase(lang_code)
825
  elif "payment_required" in raw:
826
  st.session_state["sp_payment_required"] = True
827
  st.error(tr(lang_code, "quota_exhausted"))
828
- render_payment_purchase(lang_code)
829
  elif "a5_product_task_path_not_active" in raw or "a5_product_task_path_not_authorized" in raw:
830
  st.error(tr(lang_code, "product_generation_not_active"))
831
  else:
@@ -1289,10 +1287,6 @@ def _render_task_status_once(lang_code: str) -> None:
1289
  return
1290
  status = str(task.get("status") or "")
1291
 
1292
- # First-paint invariant: render useful, localized progress and manuscript
1293
- # placeholders before any browser bridge call that may yield once via
1294
- # st.stop(). The browser watcher mutates these stable DOM targets in place
1295
- # after the bridge handshake, so no duplicate monitor/preview is needed.
1296
  st.session_state["sp_last_ui_task_status"] = status
1297
  _render_monitor_box(task, lang_code)
1298
  _render_checkpoint_preview(task, lang_code)
@@ -1590,7 +1584,9 @@ def render_workspace_controls(user):
1590
  st.markdown(tr(lang_code, "template_help_undetermined"))
1591
 
1592
  product_ui = get_product_ui_projection()
1593
- if product_ui.get("mode") == "a5_product":
 
 
1594
  products = product_ui.get("products") or {}
1595
  tier_options = [tier for tier in ("STANDARD", "FLAGSHIP") if tier in products]
1596
  if len(tier_options) != 2:
@@ -1610,6 +1606,10 @@ def render_workspace_controls(user):
1610
  key="sp_product_tier",
1611
  )
1612
  selected_product = products[selected_tier]
 
 
 
 
1613
  word_min = int(selected_product["min_body_words"])
1614
  word_max = int(selected_product["max_body_words"])
1615
  word_label = tr(
@@ -1628,6 +1628,7 @@ def render_workspace_controls(user):
1628
  min_words=word_min,
1629
  max_words=word_max,
1630
  )
 
1631
 
1632
  current_words = int(st.session_state.get("sp_words") or APP_CONFIG["target_words_default"])
1633
  bounded_words = min(word_max, max(word_min, current_words))
@@ -1649,7 +1650,17 @@ def render_workspace_controls(user):
1649
  st.session_state["sp_pending_cancel_task_id"] = str(task.get("task_id") or "")
1650
  st.rerun()
1651
  else:
1652
- can_start = bool(preflight.get("can_start_generation"))
 
 
 
 
 
 
 
 
 
 
1653
  if st.button(
1654
  tr(lang_code, "start_generation_unified"),
1655
  disabled=not can_start,
@@ -1696,13 +1707,16 @@ _process_pending_resume()
1696
  _process_pending_launch(lang_code)
1697
 
1698
  if st.session_state.get("sp_payment_required"):
1699
- render_payment_purchase(lang_code)
 
 
1700
 
1701
  # R3.2.4 keeps the approved task-monitor location. Active progress is updated
1702
  # inside the browser by read-only edge polling; Streamlit reruns only on a
1703
  # user action or a terminal/resumable boundary, so there is no periodic flicker.
1704
  _task_for_monitor = st.session_state.get("sp_generation_task") or {}
1705
- if str(_task_for_monitor.get("task_id") or "").strip():
 
1706
  st.markdown("<div id='sp-task-monitor-anchor'></div>", unsafe_allow_html=True)
1707
  # Install the one-time navigation script before task-status bridge calls.
1708
  # If the first bridge handshake yields via st.stop(), the browser still
 
821
  if "product_entitlement_required" in raw:
822
  st.session_state["sp_payment_required"] = True
823
  st.error(tr(lang_code, "product_entitlement_required"))
 
824
  elif "payment_required" in raw:
825
  st.session_state["sp_payment_required"] = True
826
  st.error(tr(lang_code, "quota_exhausted"))
 
827
  elif "a5_product_task_path_not_active" in raw or "a5_product_task_path_not_authorized" in raw:
828
  st.error(tr(lang_code, "product_generation_not_active"))
829
  else:
 
1287
  return
1288
  status = str(task.get("status") or "")
1289
 
 
 
 
 
1290
  st.session_state["sp_last_ui_task_status"] = status
1291
  _render_monitor_box(task, lang_code)
1292
  _render_checkpoint_preview(task, lang_code)
 
1584
  st.markdown(tr(lang_code, "template_help_undetermined"))
1585
 
1586
  product_ui = get_product_ui_projection()
1587
+ product_ui_ready = product_ui.get("mode") == "a5_product"
1588
+ selected_remaining: int | None = None
1589
+ if product_ui_ready:
1590
  products = product_ui.get("products") or {}
1591
  tier_options = [tier for tier in ("STANDARD", "FLAGSHIP") if tier in products]
1592
  if len(tier_options) != 2:
 
1606
  key="sp_product_tier",
1607
  )
1608
  selected_product = products[selected_tier]
1609
+ entitlement = product_ui.get("entitlement") or {}
1610
+ selected_remaining = int(
1611
+ entitlement.get("standard_remaining" if selected_tier == "STANDARD" else "flagship_remaining") or 0
1612
+ )
1613
  word_min = int(selected_product["min_body_words"])
1614
  word_max = int(selected_product["max_body_words"])
1615
  word_label = tr(
 
1628
  min_words=word_min,
1629
  max_words=word_max,
1630
  )
1631
+ st.info(tr(lang_code, "product_data_loading"))
1632
 
1633
  current_words = int(st.session_state.get("sp_words") or APP_CONFIG["target_words_default"])
1634
  bounded_words = min(word_max, max(word_min, current_words))
 
1650
  st.session_state["sp_pending_cancel_task_id"] = str(task.get("task_id") or "")
1651
  st.rerun()
1652
  else:
1653
+ entitlement_exhausted = bool(product_ui_ready and selected_remaining is not None and selected_remaining <= 0)
1654
+ if entitlement_exhausted:
1655
+ st.warning(tr(lang_code, "product_entitlement_required"))
1656
+ if st.button(tr(lang_code, "payment_open"), key="btn_open_payment_for_selected_tier"):
1657
+ render_payment_purchase(lang_code)
1658
+ can_start = bool(
1659
+ preflight.get("can_start_generation")
1660
+ and product_ui_ready
1661
+ and selected_remaining is not None
1662
+ and selected_remaining > 0
1663
+ )
1664
  if st.button(
1665
  tr(lang_code, "start_generation_unified"),
1666
  disabled=not can_start,
 
1707
  _process_pending_launch(lang_code)
1708
 
1709
  if st.session_state.get("sp_payment_required"):
1710
+ st.warning(tr(lang_code, "product_entitlement_required"))
1711
+ if st.button(tr(lang_code, "payment_open"), key="btn_open_payment_after_rejection"):
1712
+ render_payment_purchase(lang_code)
1713
 
1714
  # R3.2.4 keeps the approved task-monitor location. Active progress is updated
1715
  # inside the browser by read-only edge polling; Streamlit reruns only on a
1716
  # user action or a terminal/resumable boundary, so there is no periodic flicker.
1717
  _task_for_monitor = st.session_state.get("sp_generation_task") or {}
1718
+ _task_for_monitor_id = str(_task_for_monitor.get("task_id") or "").strip()
1719
+ if _task_for_monitor_id:
1720
  st.markdown("<div id='sp-task-monitor-anchor'></div>", unsafe_allow_html=True)
1721
  # Install the one-time navigation script before task-status bridge calls.
1722
  # If the first bridge handshake yields via st.stop(), the browser still
i18n.py CHANGED
@@ -411,30 +411,36 @@ TEXTS: dict[str, dict[str, str]] = {
411
  "login_pending": "支付宝授权尚未完成,请扫码并确认授权。",
412
  "account_title": "用户账户",
413
  "account_id": "用户编号:**{value}**",
414
- "daily_free": "每日免费:**{value}篇**",
415
- "bonus_papers": "长期有效额度:**{value}篇**",
416
  "account_chip_user": "用户 {value}",
417
- "account_chip_free": "免费 {value}篇",
418
- "account_chip_bonus": "长期 {value}篇",
419
  "account_chip_standard": "标准版 {value}篇",
420
  "account_chip_flagship": "旗舰版 {value}篇",
421
  "logout_short": "退出",
422
- "payment_dialog_title": "支付宝付款",
423
- "payment_notice": "今日免费额度和长期额度均已用完。支付9.9元可增加3篇长期有效额度。",
424
- "product_entitlement_required": "当前所选版本额度不足,请使用支付宝购买对应版本额度。",
425
- "payment_notice_product": "购买{tier}:支付 {amount} {currency} 可增加{credits}篇长期有效额度。",
426
- "payment_paid_product": "支付成功,已增加{credits}篇{tier}长期有效额度。请重新点击生成论文。",
427
- "quota_exhausted": "今日免费额度和长期额度均已用完。请使用支付宝购买长期额度。",
428
- "payment_create": "生成支付宝付款二维码",
429
- "payment_creating": "正在创建支付宝订单……",
 
 
 
 
 
 
430
  "payment_scan": "请使用支付宝 App 扫码支付。支付成功后点击“查询支付结果”。",
431
- "payment_scan_manual": "请使用支付宝 App 扫码支付。支付后请将订单号发送给管理员,管理员核实实际到账后会自动增加额度。随后可点击“查询支付结果”。",
432
  "payment_order_no": "订单号:{order_no}",
433
  "payment_support_prompt": "支付遇到问题或者提升论文质量?请联系管理员",
434
  "payment_support_wechat": "微信:{value}",
435
  "payment_support_whatsapp": "WhatsApp:{value}",
436
  "payment_check": "查询支付结果",
437
- "payment_paid": "支付成功,已增加3篇长期有效额度。请重新点击生成论文。",
438
  "payment_expired": "付款二维码已过期,请重新生成。",
439
  "browse_files": "选择文件",
440
  "drop_docs": "将文件拖放到此处",
@@ -652,30 +658,36 @@ TEXTS: dict[str, dict[str, str]] = {
652
  "login_pending": "Alipay authorization is still pending. Scan the code and confirm authorization.",
653
  "account_title": "User Account",
654
  "account_id": "User ID: **{value}**",
655
- "daily_free": "Daily free allowance: **{value} papers**",
656
- "bonus_papers": "Long-term allowance: **{value} papers**",
657
  "account_chip_user": "User {value}",
658
- "account_chip_free": "Free {value}",
659
- "account_chip_bonus": "Bonus {value}",
660
  "account_chip_standard": "Standard {value}",
661
  "account_chip_flagship": "Flagship {value}",
662
  "logout_short": "Sign out",
663
- "payment_dialog_title": "Alipay Payment",
664
- "payment_notice": "Your daily free quota and long-term quota are exhausted. Pay CNY 9.9 to add three long-term papers.",
665
- "product_entitlement_required": "The selected plan has no remaining credits. Purchase credits for this plan with Alipay.",
666
- "payment_notice_product": "Purchase {tier}: pay {currency} {amount} to add {credits} long-term paper credits.",
667
- "payment_paid_product": "Payment succeeded. {credits} long-term {tier} paper credits were added. Start the paper again.",
668
- "quota_exhausted": "Your daily free quota and long-term quota are exhausted. Purchase long-term quota with Alipay.",
669
- "payment_create": "Create Alipay QR code",
670
- "payment_creating": "Creating the Alipay order…",
 
 
 
 
 
 
671
  "payment_scan": "Scan the code with the Alipay app. After payment, select “Check payment result.”",
672
- "payment_scan_manual": "Scan with the Alipay app. After payment, send the order number to the administrator. Credits will be added automatically after the administrator verifies the actual receipt. You can then select “Check payment result.”",
673
  "payment_order_no": "Order number: {order_no}",
674
  "payment_support_prompt": "Need help with payment or improving your paper's quality? Contact the administrator.",
675
  "payment_support_wechat": "WeChat: {value}",
676
  "payment_support_whatsapp": "WhatsApp: {value}",
677
  "payment_check": "Check payment result",
678
- "payment_paid": "Payment succeeded. Three long-term papers were added. Start the paper again.",
679
  "payment_expired": "The payment QR code has expired. Create a new one.",
680
  "browse_files": "Choose files",
681
  "drop_docs": "Drag and drop files here",
@@ -893,30 +905,36 @@ TEXTS: dict[str, dict[str, str]] = {
893
  "login_pending": "Авторизация Alipay ещё не завершена. Отсканируйте код и подтвердите доступ.",
894
  "account_title": "Учётная запись",
895
  "account_id": "Идентификатор пользователя: **{value}**",
896
- "daily_free": "Ежедневно бесплатно: **{value} статьи**",
897
- "bonus_papers": "Долгосрочный остаток: **{value} статьи**",
898
  "account_chip_user": "Пользователь {value}",
899
- "account_chip_free": "Бесплатно {value}",
900
- "account_chip_bonus": "Запас {value}",
901
  "account_chip_standard": "Стандарт {value}",
902
  "account_chip_flagship": "Флагман {value}",
903
  "logout_short": "Выйти",
904
- "payment_dialog_title": "Оплата через Alipay",
905
- "payment_notice": "Ежедневная бесплатная и долгосрочная квоты исчерпаны. Оплата 9,9 юаня добавляет квоту на три статьи без срока действия.",
906
- "product_entitlement_required": "Квота выбранного тарифа исчерпана. Приобретите квот�� этого тарифа через Alipay.",
907
- "payment_notice_product": "Покупка тарифа {tier}: оплата {amount} {currency} добавит {credits} долгосрочных квот на статьи.",
908
- "payment_paid_product": "Оплата прошла успешно. Добавлено {credits} долгосрочных квот тарифа {tier}. Запустите создание статьи повторно.",
909
- "quota_exhausted": "Ежедневная бесплатная и долгосрочная квоты исчерпаны. Приобретите долгосрочную квоту через Alipay.",
 
 
 
 
 
 
910
  "payment_create": "Создать QR-код для оплаты",
911
- "payment_creating": "Создаётся заказ Alipay…",
912
  "payment_scan": "Отсканируйте QR-код в приложении Alipay. После оплаты нажмите «Проверить результат оплаты».",
913
- "payment_scan_manual": "Отсканируйте QR-код в приложении Alipay. После оплаты отправьте номер заказа администратору. После проверки фактического поступления средств квота будет добавлена автоматически. Затем нажмите «Проверить результат оплаты».",
914
  "payment_order_no": "Номер заказа: {order_no}",
915
  "payment_support_prompt": "Возникли вопросы с оплатой или хотите повысить качество научной статьи? Свяжитесь с администратором.",
916
  "payment_support_wechat": "WeChat: {value}",
917
  "payment_support_whatsapp": "WhatsApp: {value}",
918
  "payment_check": "Проверить результат оплаты",
919
- "payment_paid": "Оплата прошла успешно. Добавлена долгосрочная квота на три статьи. Запустите создание статьи повторно.",
920
  "payment_expired": "Срок действия QR-кода истёк. Создайте новый код.",
921
  "browse_files": "Выбрать файлы",
922
  "drop_docs": "Перетащите файлы сюда",
@@ -1134,30 +1152,36 @@ TEXTS: dict[str, dict[str, str]] = {
1134
  "login_pending": "Alipay рұқсаты әлі аяқталған жоқ. QR-кодты сканерлеп, рұқсатты растаңыз.",
1135
  "account_title": "Пайдаланушы есептік жазбасы",
1136
  "account_id": "Пайдаланушы нөмірі: **{value}**",
1137
- "daily_free": "Күнделікті тегін: **{value} мақала**",
1138
- "bonus_papers": "Ұзақ мерзімді қалдық: **{value} мақала**",
1139
  "account_chip_user": "Пайдаланушы {value}",
1140
- "account_chip_free": "Тегін {value}",
1141
- "account_chip_bonus": "Қор {value}",
1142
  "account_chip_standard": "Стандарт {value}",
1143
  "account_chip_flagship": "Флагман {value}",
1144
  "logout_short": "Шығу",
1145
- "payment_dialog_title": "Alipay арқылы төлеу",
1146
- "payment_notice": "Күнделікті тегін және ұзақ мерзімді квота таусылды. 9,9 юань төлегенде мерзімі шектелмеген үш мақала квотасы қосылады.",
1147
- "product_entitlement_required": "Таңдалған нұсқаның квотасы таусылды. Осы нұсқаға арналған квотаны Alipay арқылы сатып алыңыз.",
1148
- "payment_notice_product": "{tier} нұсқасын сатып алу: {amount} {currency} төлемі {credits} ұзақ мерзімді мақала квотасын қосады.",
1149
- "payment_paid_product": "Төлем сәтті өтті. {tier} нұсқасына {credits} ұзақ мерзімді мақала квотасы қосылды. Мақала жасауды қайта бастаңыз.",
1150
- "quota_exhausted": "Күнделікті тегін және ұзақ мерзімді квота таусылды. Alipay арқылы ұзақ мерзімді квота сатып алыңыз.",
 
 
 
 
 
 
1151
  "payment_create": "Төлемге арналған QR-код жасау",
1152
- "payment_creating": "Alipay тапсырысы жасалуда…",
1153
  "payment_scan": "QR-кодты Alipay қолданбасымен сканерлеңіз. Төлемнен кейін «Төлем нәтижесін тексеру» түймесін басыңыз.",
1154
- "payment_scan_manual": "QR-кодты Alipay қолданбасымен сканерлеңіз. Төлемнен кейін тапсырыс нөмірін әкімшіге жіберіңіз. Әкімші төлемнің нақты түскенін тексерген соң квота автоматты түрде қосылады. Содан кейін «Төлем нәтижесін тексеру» түймесін басыңыз.",
1155
  "payment_order_no": "Тапсырыс нөмірі: {order_no}",
1156
  "payment_support_prompt": "Төлем бойынша мәселе туындады ма немесе ғылыми мақаланың сапасын жақсартқыңыз келе ме? Әкімшіге хабарласыңыз.",
1157
  "payment_support_wechat": "WeChat: {value}",
1158
  "payment_support_whatsapp": "WhatsApp: {value}",
1159
  "payment_check": "Төлем нәтижесін тексеру",
1160
- "payment_paid": "Төлем сәтті өтті. Мерзімі шектелмеген үш мақала квотасы қосылды. Мақала жасауды қайта бастаңыз.",
1161
  "payment_expired": "QR-кодтың қолданылу мерзімі аяқталды. Жаңа код жасаңыз.",
1162
  "browse_files": "Файлдарды таңдау",
1163
  "drop_docs": "Файлдарды осы жерге сүйреп әкеліңіз",
 
411
  "login_pending": "支付宝授权尚未完成,请扫码并确认授权。",
412
  "account_title": "用户账户",
413
  "account_id": "用户编号:**{value}**",
414
+ "daily_free": "标准版:**{value}篇**",
415
+ "bonus_papers": "旗舰版:**{value}篇**",
416
  "account_chip_user": "用户 {value}",
417
+ "account_chip_free": "标准版 {value}篇",
418
+ "account_chip_bonus": "旗舰版 {value}篇",
419
  "account_chip_standard": "标准版 {value}篇",
420
  "account_chip_flagship": "旗舰版 {value}篇",
421
  "logout_short": "退出",
422
+ "payment_dialog_title": "购买论文额度",
423
+ "payment_notice": "当前版本额度不足,请购买相应版本的论文额度。",
424
+ "product_entitlement_required": "当前所选版本额度为0,暂时不能生成论文。购买并到账后即可继续生成该版本论文。",
425
+ "product_data_loading": "正在加载标准版 / 旗舰版额度与产品策略,请稍候。加载完成前暂不能生成论文。",
426
+ "payment_open": "购买额度",
427
+ "payment_channel_label": "本次付款方式",
428
+ "payment_channel_alipay": "Alipay",
429
+ "payment_channel_wechat": "WeChat",
430
+ "payment_scan_dual": "请使用与本次付款方式一致的二维码(当前选择:{channel})。付款后将订单号发送给管理员,核实到账后会增加对应版本额度。",
431
+ "payment_notice_product": "购买{tier}:支付 {amount} {currency} 可增加{credits}篇该版本论文额度。",
432
+ "payment_paid_product": "付款已确认,已增加{credits}篇{tier}论文额度。现在可以继续生成论文。",
433
+ "quota_exhausted": "当前所选版本额度不足,请购买相应版本额度。",
434
+ "payment_create": "创建付款订单",
435
+ "payment_creating": "正在创建付款订单……",
436
  "payment_scan": "请使用支付宝 App 扫码支付。支付成功后点击“查询支付结果”。",
437
+ "payment_scan_manual": "请使用所选付款方式扫码。付款后请将订单号发送给管理员,管理员核实实际到账后会增加对应版本额度。",
438
  "payment_order_no": "订单号:{order_no}",
439
  "payment_support_prompt": "支付遇到问题或者提升论文质量?请联系管理员",
440
  "payment_support_wechat": "微信:{value}",
441
  "payment_support_whatsapp": "WhatsApp:{value}",
442
  "payment_check": "查询支付结果",
443
+ "payment_paid": "付款已确认,额度已更新。现在可以继续生成论文。",
444
  "payment_expired": "付款二维码已过期,请重新生成。",
445
  "browse_files": "选择文件",
446
  "drop_docs": "将文件拖放到此处",
 
658
  "login_pending": "Alipay authorization is still pending. Scan the code and confirm authorization.",
659
  "account_title": "User Account",
660
  "account_id": "User ID: **{value}**",
661
+ "daily_free": "Standard: **{value} papers**",
662
+ "bonus_papers": "Flagship: **{value} papers**",
663
  "account_chip_user": "User {value}",
664
+ "account_chip_free": "Standard {value}",
665
+ "account_chip_bonus": "Flagship {value}",
666
  "account_chip_standard": "Standard {value}",
667
  "account_chip_flagship": "Flagship {value}",
668
  "logout_short": "Sign out",
669
+ "payment_dialog_title": "Purchase Paper Credits",
670
+ "payment_notice": "The selected plan has no remaining credits. Purchase credits for that plan to continue.",
671
+ "product_entitlement_required": "The selected plan has 0 remaining credits, so generation is disabled. Purchase credits for this plan and wait for confirmation before generating again.",
672
+ "product_data_loading": "Loading Standard / Flagship credits and product policy. Generation stays disabled until authoritative data is ready.",
673
+ "payment_open": "Purchase credits",
674
+ "payment_channel_label": "Payment method for this order",
675
+ "payment_channel_alipay": "Alipay",
676
+ "payment_channel_wechat": "WeChat",
677
+ "payment_scan_dual": "Use the QR code that matches the selected payment method (current: {channel}). After payment, send the order number to the administrator; the matching plan credits will be added after receipt is verified.",
678
+ "payment_notice_product": "Purchase {tier}: pay {currency} {amount} to add {credits} paper credits for this plan.",
679
+ "payment_paid_product": "Payment was confirmed. {credits} {tier} paper credits were added. You can generate again now.",
680
+ "quota_exhausted": "The selected plan has no remaining credits. Purchase credits for that plan to continue.",
681
+ "payment_create": "Create payment order",
682
+ "payment_creating": "Creating the payment order…",
683
  "payment_scan": "Scan the code with the Alipay app. After payment, select “Check payment result.”",
684
+ "payment_scan_manual": "Scan with the selected payment method. After payment, send the order number to the administrator. Credits will be added after the administrator verifies the actual receipt.",
685
  "payment_order_no": "Order number: {order_no}",
686
  "payment_support_prompt": "Need help with payment or improving your paper's quality? Contact the administrator.",
687
  "payment_support_wechat": "WeChat: {value}",
688
  "payment_support_whatsapp": "WhatsApp: {value}",
689
  "payment_check": "Check payment result",
690
+ "payment_paid": "Payment was confirmed and your credits were updated. You can generate again now.",
691
  "payment_expired": "The payment QR code has expired. Create a new one.",
692
  "browse_files": "Choose files",
693
  "drop_docs": "Drag and drop files here",
 
905
  "login_pending": "Авторизация Alipay ещё не завершена. Отсканируйте код и подтвердите доступ.",
906
  "account_title": "Учётная запись",
907
  "account_id": "Идентификатор пользователя: **{value}**",
908
+ "daily_free": "Стандарт: **{value} статьи**",
909
+ "bonus_papers": "Флагман: **{value} статьи**",
910
  "account_chip_user": "Пользователь {value}",
911
+ "account_chip_free": "Стандарт {value}",
912
+ "account_chip_bonus": "Флагман {value}",
913
  "account_chip_standard": "Стандарт {value}",
914
  "account_chip_flagship": "Флагман {value}",
915
  "logout_short": "Выйти",
916
+ "payment_dialog_title": "Покупка лимита статей",
917
+ "payment_notice": "Лимит выбранной версии исчерпан. Приобретите лимит для этой версии, чтобы продолжить.",
918
+ "product_entitlement_required": "Остаток выбранной версии равен 0, поэтому создание статьи отключено. После покупки и подтверждения лимита можно продолжить.",
919
+ "product_data_loading": "Загружаются лимиты Standard / Flagship и политика продукта. До получения подтверждённых данных создание статьи отключено.",
920
+ "payment_open": "Купить лимит",
921
+ "payment_channel_label": "Способ оплаты для этого заказа",
922
+ "payment_channel_alipay": "Alipay",
923
+ "payment_channel_wechat": "WeChat",
924
+ "payment_scan_dual": "Используйте QR-код, соответствующий выбранному способу оплаты (сейчас: {channel}). После оплаты отправьте номер заказа администратору; лимит нужной версии будет добавлен после проверки поступления средств.",
925
+ "payment_notice_product": "Покупка {tier}: оплата {amount} {currency} добавит {credits} квоты на статьи этой версии.",
926
+ "payment_paid_product": "Платёж подтверждён. Добавлено {credits} квоты {tier}. Теперь можно продолжить создание статьи.",
927
+ "quota_exhausted": "Лимит выбранной версии исчерпан. Приобретите лимит для этой версии, чтобы продолжить.",
928
  "payment_create": "Создать QR-код для оплаты",
929
+ "payment_creating": "Создаётся платёжный заказ…",
930
  "payment_scan": "Отсканируйте QR-код в приложении Alipay. После оплаты нажмите «Проверить результат оплаты».",
931
+ "payment_scan_manual": "Используйте выбранный способ оплаты. После оплаты отправьте номер заказа администратору; лимит будет добавлен после проверки фактического поступления средств.",
932
  "payment_order_no": "Номер заказа: {order_no}",
933
  "payment_support_prompt": "Возникли вопросы с оплатой или хотите повысить качество научной статьи? Свяжитесь с администратором.",
934
  "payment_support_wechat": "WeChat: {value}",
935
  "payment_support_whatsapp": "WhatsApp: {value}",
936
  "payment_check": "Проверить результат оплаты",
937
+ "payment_paid": "Платёж подтверждён, лимит обновлён. Теперь можно продолжить создание статьи.",
938
  "payment_expired": "Срок действия QR-кода истёк. Создайте новый код.",
939
  "browse_files": "Выбрать файлы",
940
  "drop_docs": "Перетащите файлы сюда",
 
1152
  "login_pending": "Alipay рұқсаты әлі аяқталған жоқ. QR-кодты сканерлеп, рұқсатты растаңыз.",
1153
  "account_title": "Пайдаланушы есептік жазбасы",
1154
  "account_id": "Пайдаланушы нөмірі: **{value}**",
1155
+ "daily_free": "Стандарт: **{value} мақала**",
1156
+ "bonus_papers": "Флагман: **{value} мақала**",
1157
  "account_chip_user": "Пайдаланушы {value}",
1158
+ "account_chip_free": "Стандарт {value}",
1159
+ "account_chip_bonus": "Флагман {value}",
1160
  "account_chip_standard": "Стандарт {value}",
1161
  "account_chip_flagship": "Флагман {value}",
1162
  "logout_short": "Шығу",
1163
+ "payment_dialog_title": "Мақала лимитін сатып алу",
1164
+ "payment_notice": "Таңдалған нұсқаның лимиті таусылды. Жалғастыру үшін осы нұсқаға лимит сатып алыңыз.",
1165
+ "product_entitlement_required": "Таңдалған нұсқаның қалдығы 0, сондықтан мақала жасау өшірілді. Сатып алу расталғаннан кейін жалғастыруға болады.",
1166
+ "product_data_loading": "Standard / Flagship лимиттері мен өнім саясаты жүктелуде. Расталған деректер дайын болғанша мақала жасау өшіріледі.",
1167
+ "payment_open": "Лимит сатып алу",
1168
+ "payment_channel_label": "Осы тапсырыстың төлем тәсілі",
1169
+ "payment_channel_alipay": "Alipay",
1170
+ "payment_channel_wechat": "WeChat",
1171
+ "payment_scan_dual": "Таңдалған төлем тәсіліне сәйкес QR-кодты пайдаланыңыз (қазір: {channel}). Төлемнен кейін тапсырыс нөмірін әкімшіге жіберіңіз; түсім расталғаннан кейін тиісті нұсқа лимиті қосылады.",
1172
+ "payment_notice_product": "{tier} нұсқасын сатып алу: {amount} {currency} төлемі осы нұсқаға {credits} мақала лимитін қосады.",
1173
+ "payment_paid_product": "Төлем расталды. {tier} нұсқасына {credits} мақала лимиті қосылды. Енді мақала жасауды жалғастыруға болады.",
1174
+ "quota_exhausted": "Таңдалған нұсқаның лимиті таусылды. Жалғастыру үшін осы нұсқаға лимит сатып алыңыз.",
1175
  "payment_create": "Төлемге арналған QR-код жасау",
1176
+ "payment_creating": "Төлем тапсырысы жасалуда…",
1177
  "payment_scan": "QR-кодты Alipay қолданбасымен сканерлеңіз. Төлемнен кейін «Төлем нәтижесін тексеру» түймесін басыңыз.",
1178
+ "payment_scan_manual": "Таңдалған төлем тәсілін пайдаланыңыз. Төлемнен кейін тапсырыс нөм��рін әкімшіге жіберіңіз; нақты түсім тексерілгеннен кейін лимит қосылады.",
1179
  "payment_order_no": "Тапсырыс нөмірі: {order_no}",
1180
  "payment_support_prompt": "Төлем бойынша мәселе туындады ма немесе ғылыми мақаланың сапасын жақсартқыңыз келе ме? Әкімшіге хабарласыңыз.",
1181
  "payment_support_wechat": "WeChat: {value}",
1182
  "payment_support_whatsapp": "WhatsApp: {value}",
1183
  "payment_check": "Төлем нәтижесін тексеру",
1184
+ "payment_paid": "Төлем расталды, лимит жаңартылды. Енді мақала жасауды жалғастыруға болады.",
1185
  "payment_expired": "QR-кодтың қолданылу мерзімі аяқталды. Жаңа код жасаңыз.",
1186
  "browse_files": "Файлдарды таңдау",
1187
  "drop_docs": "Файлдарды осы жерге сүйреп әкеліңіз",
managed_auth.py CHANGED
@@ -140,6 +140,7 @@ def _clear_generation_session_state() -> None:
140
  "rag_engine", "up_amj_ref", "up_amj_data", "up_asq_ref",
141
  "up_asq_data", "sp_payment_required", "sp_payment_order",
142
  "sp_payment_request_id", "sp_payment_package_sku",
 
143
  ):
144
  st.session_state.pop(key, None)
145
 
@@ -389,6 +390,8 @@ def start_generation_task(
389
  started_task = result.get("task")
390
  if isinstance(started_task, dict) and started_task.get("task_id"):
391
  st.session_state["sp_tracked_task_id"] = str(started_task["task_id"])
 
 
392
  return dict(result)
393
 
394
 
@@ -450,10 +453,23 @@ def recover_generation_task() -> dict[str, Any]:
450
  _merge_quota_into_session(result)
451
  task = result.get("task")
452
  if isinstance(task, dict) and task.get("task_id"):
453
- _restore_completed_artifact_if_needed(task)
454
- st.session_state["sp_generation_task"] = dict(task)
455
- st.session_state["sp_tracked_task_id"] = str(task["task_id"])
456
- st.session_state["sp_last_ui_task_status"] = str(task.get("status") or "")
 
 
 
 
 
 
 
 
 
 
 
 
 
457
  else:
458
  st.session_state["sp_generation_task"] = None
459
  st.session_state["sp_last_ui_task_status"] = ""
@@ -517,12 +533,18 @@ def logout_alipay_session(on_logout: Callable[[str, str], None] | None = None) -
517
  def create_payment_order(
518
  idempotency_key: str,
519
  package_sku: str | None = None,
 
520
  ) -> dict[str, Any]:
521
  normalized = str(idempotency_key or "").strip()
522
  payload: dict[str, Any] = {"idempotency_key": normalized}
523
  normalized_sku = str(package_sku or "").strip()
524
  if normalized_sku:
525
  payload["package_sku"] = normalized_sku
 
 
 
 
 
526
  result = _edge_call(
527
  "pay_create",
528
  normalized,
@@ -564,6 +586,7 @@ def _render_payment_purchase_body(language: str) -> None:
564
  product_mode = projection.get("mode") == "a5_product"
565
  selected_product: dict[str, Any] | None = None
566
  package_sku = ""
 
567
  if product_mode:
568
  selected_tier = str(st.session_state.get("sp_product_tier") or "STANDARD").strip().upper()
569
  raw_product = (projection.get("products") or {}).get(selected_tier)
@@ -590,7 +613,6 @@ def _render_payment_purchase_body(language: str) -> None:
590
  credits=credits,
591
  )
592
  )
593
- existing_order = st.session_state.get("sp_payment_order")
594
  existing_sku = str(st.session_state.get("sp_payment_package_sku") or "").strip()
595
  order_sku = str((existing_order or {}).get("package_sku") or "").strip() if isinstance(existing_order, dict) else ""
596
  if (existing_sku and existing_sku != package_sku) or (existing_order and order_sku != package_sku):
@@ -599,17 +621,39 @@ def _render_payment_purchase_body(language: str) -> None:
599
  else:
600
  st.warning(tr(language, "payment_notice"))
601
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
602
  if st.button(tr(language, "payment_create"), key="sp_create_payment_order"):
603
  st.session_state["sp_payment_request_id"] = (
604
  f"pay_{int(time.time() * 1000)}_{secrets.token_urlsafe(12)}"
605
  )
606
  st.session_state["sp_payment_package_sku"] = package_sku
 
607
 
608
  request_id = str(st.session_state.get("sp_payment_request_id") or "").strip()
609
  order = st.session_state.get("sp_payment_order")
610
  if request_id and not order:
611
  st.info(tr(language, "payment_creating"))
612
- order_result = create_payment_order(request_id, package_sku or None)
 
 
 
 
613
  order = dict(order_result.get("order") or {})
614
  if not order.get("merchant_order_no") or not order.get("qr_code"):
615
  raise RuntimeError("payment_order_missing_qr_code")
@@ -623,15 +667,69 @@ def _render_payment_purchase_body(language: str) -> None:
623
  order = st.session_state.get("sp_payment_order") or {}
624
  qr_code = str(order.get("qr_code") or "").strip()
625
  if qr_code:
626
- st.image(_qr_png_bytes(qr_code), width=280)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
627
  order_no = str(order.get("merchant_order_no") or "").strip()
628
  if order_no:
629
  st.caption(tr(language, "payment_order_no", order_no=order_no))
630
  st.code(order_no)
631
- payment_provider = str(order.get("payment_provider") or "").strip().upper()
632
- if payment_provider == "MANUAL_ALIPAY":
633
- st.caption(tr(language, "payment_scan_manual"))
634
- else:
635
  st.caption(tr(language, "payment_scan"))
636
  support = order.get("payment_support") if isinstance(order.get("payment_support"), dict) else {}
637
  support_wechat = str((support or {}).get("wechat") or "").strip()
@@ -660,6 +758,7 @@ def _render_payment_purchase_body(language: str) -> None:
660
  st.session_state["sp_payment_order"] = None
661
  st.session_state["sp_payment_request_id"] = ""
662
  st.session_state["sp_payment_package_sku"] = ""
 
663
  if product_mode and selected_product is not None:
664
  tier_name = tr(language, f"product_tier_{str(selected_product.get('product_tier') or '').lower()}")
665
  st.success(
@@ -676,6 +775,7 @@ def _render_payment_purchase_body(language: str) -> None:
676
  st.session_state["sp_payment_order"] = None
677
  st.session_state["sp_payment_request_id"] = ""
678
  st.session_state["sp_payment_package_sku"] = ""
 
679
  st.warning(tr(language, "payment_expired"))
680
 
681
 
@@ -711,8 +811,8 @@ def render_logged_in_account(
711
  standard_remaining = entitlement["standard_remaining"]
712
  flagship_remaining = entitlement["flagship_remaining"]
713
  else:
714
- free_remaining = user.get("daily_free_remaining", user.get("daily_free_limit", 3))
715
- bonus_papers = user.get("bonus_papers", 0)
716
 
717
  st.markdown(
718
  """
@@ -834,18 +934,11 @@ def render_logged_in_account(
834
 
835
  info_col, logout_col = st.columns([5.9, 1.65])
836
  with info_col:
837
- if product_mode:
838
- balance_markup = (
839
- f"<span>{html.escape(tr(language, 'account_chip_standard', value=standard_remaining))}</span>"
840
- f'<span class="sp-account-separator">•</span>'
841
- f"<span>{html.escape(tr(language, 'account_chip_flagship', value=flagship_remaining))}</span>"
842
- )
843
- else:
844
- balance_markup = (
845
- f"<span>{html.escape(tr(language, 'account_chip_free', value=free_remaining))}</span>"
846
- f'<span class="sp-account-separator">•</span>'
847
- f"<span>{html.escape(tr(language, 'account_chip_bonus', value=bonus_papers))}</span>"
848
- )
849
  st.markdown(
850
  f"""
851
  <div class="sp-account-card">
 
140
  "rag_engine", "up_amj_ref", "up_amj_data", "up_asq_ref",
141
  "up_asq_data", "sp_payment_required", "sp_payment_order",
142
  "sp_payment_request_id", "sp_payment_package_sku",
143
+ "sp_payment_channel", "sp_payment_order_channel",
144
  ):
145
  st.session_state.pop(key, None)
146
 
 
390
  started_task = result.get("task")
391
  if isinstance(started_task, dict) and started_task.get("task_id"):
392
  st.session_state["sp_tracked_task_id"] = str(started_task["task_id"])
393
+ # A user-started task must never inherit the prior-login completed-task UI mode.
394
+ st.session_state["sp_recovered_completed_task_id"] = ""
395
  return dict(result)
396
 
397
 
 
453
  _merge_quota_into_session(result)
454
  task = result.get("task")
455
  if isinstance(task, dict) and task.get("task_id"):
456
+ recovered_status = str(task.get("status") or "").strip().lower()
457
+ if recovered_status == "completed":
458
+ # Historical completed tasks remain authoritative in D1/Bucket, but a
459
+ # fresh login must open a clean workspace. Do not restore the artifact,
460
+ # attach the completed task to Streamlit session state, or render any
461
+ # historical generation/download UI.
462
+ st.session_state["sp_generation_task"] = None
463
+ st.session_state["sp_last_ui_task_status"] = ""
464
+ st.session_state.pop("sp_tracked_task_id", None)
465
+ logging.info(
466
+ "[SP_AUTH] completed_task_hidden_on_fresh_login task_id=%s",
467
+ str(task.get("task_id") or ""),
468
+ )
469
+ else:
470
+ st.session_state["sp_generation_task"] = dict(task)
471
+ st.session_state["sp_tracked_task_id"] = str(task["task_id"])
472
+ st.session_state["sp_last_ui_task_status"] = recovered_status
473
  else:
474
  st.session_state["sp_generation_task"] = None
475
  st.session_state["sp_last_ui_task_status"] = ""
 
533
  def create_payment_order(
534
  idempotency_key: str,
535
  package_sku: str | None = None,
536
+ payment_channel: str | None = None,
537
  ) -> dict[str, Any]:
538
  normalized = str(idempotency_key or "").strip()
539
  payload: dict[str, Any] = {"idempotency_key": normalized}
540
  normalized_sku = str(package_sku or "").strip()
541
  if normalized_sku:
542
  payload["package_sku"] = normalized_sku
543
+ normalized_channel = str(payment_channel or "").strip().upper()
544
+ if normalized_channel:
545
+ if normalized_channel not in {"ALIPAY", "WECHAT"}:
546
+ raise RuntimeError("payment_channel_invalid")
547
+ payload["payment_channel"] = normalized_channel
548
  result = _edge_call(
549
  "pay_create",
550
  normalized,
 
586
  product_mode = projection.get("mode") == "a5_product"
587
  selected_product: dict[str, Any] | None = None
588
  package_sku = ""
589
+ existing_order = st.session_state.get("sp_payment_order")
590
  if product_mode:
591
  selected_tier = str(st.session_state.get("sp_product_tier") or "STANDARD").strip().upper()
592
  raw_product = (projection.get("products") or {}).get(selected_tier)
 
613
  credits=credits,
614
  )
615
  )
 
616
  existing_sku = str(st.session_state.get("sp_payment_package_sku") or "").strip()
617
  order_sku = str((existing_order or {}).get("package_sku") or "").strip() if isinstance(existing_order, dict) else ""
618
  if (existing_sku and existing_sku != package_sku) or (existing_order and order_sku != package_sku):
 
621
  else:
622
  st.warning(tr(language, "payment_notice"))
623
 
624
+ frozen_order_channel = (
625
+ str((existing_order or {}).get("payment_channel") or "").strip().upper()
626
+ if isinstance(existing_order, dict)
627
+ else ""
628
+ )
629
+ if frozen_order_channel in {"ALIPAY", "WECHAT"}:
630
+ st.session_state["sp_payment_channel"] = frozen_order_channel
631
+ payment_channel = st.radio(
632
+ tr(language, "payment_channel_label"),
633
+ ("ALIPAY", "WECHAT"),
634
+ index=0 if str(st.session_state.get("sp_payment_channel") or "ALIPAY").upper() != "WECHAT" else 1,
635
+ format_func=lambda value: tr(language, f"payment_channel_{value.lower()}"),
636
+ horizontal=True,
637
+ disabled=bool(existing_order),
638
+ key="sp_payment_channel",
639
+ )
640
+
641
  if st.button(tr(language, "payment_create"), key="sp_create_payment_order"):
642
  st.session_state["sp_payment_request_id"] = (
643
  f"pay_{int(time.time() * 1000)}_{secrets.token_urlsafe(12)}"
644
  )
645
  st.session_state["sp_payment_package_sku"] = package_sku
646
+ st.session_state["sp_payment_order_channel"] = str(payment_channel).upper()
647
 
648
  request_id = str(st.session_state.get("sp_payment_request_id") or "").strip()
649
  order = st.session_state.get("sp_payment_order")
650
  if request_id and not order:
651
  st.info(tr(language, "payment_creating"))
652
+ order_result = create_payment_order(
653
+ request_id,
654
+ package_sku or None,
655
+ str(st.session_state.get("sp_payment_order_channel") or payment_channel),
656
+ )
657
  order = dict(order_result.get("order") or {})
658
  if not order.get("merchant_order_no") or not order.get("qr_code"):
659
  raise RuntimeError("payment_order_missing_qr_code")
 
667
  order = st.session_state.get("sp_payment_order") or {}
668
  qr_code = str(order.get("qr_code") or "").strip()
669
  if qr_code:
670
+ payment_provider = str(order.get("payment_provider") or "").strip().upper()
671
+ payment_channel = str(order.get("payment_channel") or st.session_state.get("sp_payment_order_channel") or "ALIPAY").strip().upper()
672
+ if payment_provider == "MANUAL_ALIPAY":
673
+ alipay_bytes = _qr_png_bytes(qr_code)
674
+ wechat_path = Path(__file__).resolve().parent / "static" / "payment_wechat_qr.png"
675
+ if not wechat_path.is_file():
676
+ raise RuntimeError("payment_wechat_qr_missing")
677
+ wechat_bytes = wechat_path.read_bytes()
678
+ alipay_uri = "data:image/png;base64," + base64.b64encode(alipay_bytes).decode("ascii")
679
+ wechat_uri = "data:image/png;base64," + base64.b64encode(wechat_bytes).decode("ascii")
680
+ st.markdown(
681
+ f"""
682
+ <style media="all">
683
+ .sp-payment-grid {{
684
+ display:grid;
685
+ grid-template-columns:repeat(2,minmax(0,1fr));
686
+ gap:1rem;
687
+ align-items:start;
688
+ margin:.35rem 0 .6rem;
689
+ }}
690
+ .sp-payment-card {{text-align:center; min-width:0;}}
691
+ .sp-payment-card strong {{display:block; margin-bottom:.4rem;}}
692
+ .sp-payment-qr-frame {{
693
+ width:min(100%,300px);
694
+ aspect-ratio:1/1;
695
+ margin:0 auto;
696
+ display:flex;
697
+ align-items:center;
698
+ justify-content:center;
699
+ overflow:hidden;
700
+ }}
701
+ .sp-payment-qr-frame img {{
702
+ width:100%;
703
+ height:100%;
704
+ object-fit:contain;
705
+ display:block;
706
+ }}
707
+ @media (max-width:640px) {{
708
+ .sp-payment-grid {{grid-template-columns:1fr;}}
709
+ .sp-payment-qr-frame {{width:min(86vw,300px);}}
710
+ }}
711
+ </style>
712
+ <div class="sp-payment-grid">
713
+ <div class="sp-payment-card">
714
+ <strong>{html.escape(tr(language, 'payment_channel_alipay'))}</strong>
715
+ <div class="sp-payment-qr-frame"><img src="{alipay_uri}" alt="Alipay QR"></div>
716
+ </div>
717
+ <div class="sp-payment-card">
718
+ <strong>{html.escape(tr(language, 'payment_channel_wechat'))}</strong>
719
+ <div class="sp-payment-qr-frame"><img src="{wechat_uri}" alt="WeChat QR"></div>
720
+ </div>
721
+ </div>
722
+ """,
723
+ unsafe_allow_html=True,
724
+ )
725
+ st.caption(tr(language, "payment_scan_dual", channel=tr(language, f"payment_channel_{payment_channel.lower()}")))
726
+ else:
727
+ st.image(_qr_png_bytes(qr_code), width=280)
728
  order_no = str(order.get("merchant_order_no") or "").strip()
729
  if order_no:
730
  st.caption(tr(language, "payment_order_no", order_no=order_no))
731
  st.code(order_no)
732
+ if payment_provider != "MANUAL_ALIPAY":
 
 
 
733
  st.caption(tr(language, "payment_scan"))
734
  support = order.get("payment_support") if isinstance(order.get("payment_support"), dict) else {}
735
  support_wechat = str((support or {}).get("wechat") or "").strip()
 
758
  st.session_state["sp_payment_order"] = None
759
  st.session_state["sp_payment_request_id"] = ""
760
  st.session_state["sp_payment_package_sku"] = ""
761
+ st.session_state["sp_payment_order_channel"] = ""
762
  if product_mode and selected_product is not None:
763
  tier_name = tr(language, f"product_tier_{str(selected_product.get('product_tier') or '').lower()}")
764
  st.success(
 
775
  st.session_state["sp_payment_order"] = None
776
  st.session_state["sp_payment_request_id"] = ""
777
  st.session_state["sp_payment_package_sku"] = ""
778
+ st.session_state["sp_payment_order_channel"] = ""
779
  st.warning(tr(language, "payment_expired"))
780
 
781
 
 
811
  standard_remaining = entitlement["standard_remaining"]
812
  flagship_remaining = entitlement["flagship_remaining"]
813
  else:
814
+ standard_remaining = "—"
815
+ flagship_remaining = "—"
816
 
817
  st.markdown(
818
  """
 
934
 
935
  info_col, logout_col = st.columns([5.9, 1.65])
936
  with info_col:
937
+ balance_markup = (
938
+ f"<span>{html.escape(tr(language, 'account_chip_standard', value=standard_remaining))}</span>"
939
+ f'<span class="sp-account-separator">•</span>'
940
+ f"<span>{html.escape(tr(language, 'account_chip_flagship', value=flagship_remaining))}</span>"
941
+ )
 
 
 
 
 
 
 
942
  st.markdown(
943
  f"""
944
  <div class="sp-account-card">
static/payment_wechat_qr.png ADDED

Git LFS Details

  • SHA256: 68649b6cac41ced215e60e47288a0f225ef37f36046817de2a0bb1f84d7e631e
  • Pointer size: 131 Bytes
  • Size of remote file: 184 kB