From bf25aca95bb61c851d1d1d86b7a7f91b33343a88 Mon Sep 17 00:00:00 2001 From: Lee Penkman Date: Fri, 17 Apr 2026 17:39:15 +1200 Subject: [PATCH] Launch growth workflow tools with OpenPaths multi-LLM routing --- main.py | 138 +++++++++++++++++- questions/tool_fixtures.py | 36 +++++ static/css/checkout-dialog.css | 108 +++++++++++++- static/js/checkout-dialog.js | 99 ++++++++++++- static/js/marketing-workflow-tool.js | 65 +++++++++ static/js/subscription-modal.js | 43 +++++- static/js/unlimited-ai-modal.js | 11 +- static/templates/ai-text-editor.jinja2 | 18 ++- static/templates/shared/index.jinja2 | 24 +++ static/templates/shared/subscribe.jinja2 | 6 +- .../tools/marketing-workflow-tool.jinja2 | 57 ++++++++ test_subscription_system.py | 2 +- tests/test_checkout_session.py | 4 +- tests/unit/test_growth_tools_catalog.py | 23 +++ tests/unit/test_pricing_configuration.py | 24 +++ 15 files changed, 635 insertions(+), 23 deletions(-) create mode 100644 static/js/marketing-workflow-tool.js create mode 100644 static/templates/tools/marketing-workflow-tool.jinja2 create mode 100644 tests/unit/test_growth_tools_catalog.py create mode 100644 tests/unit/test_pricing_configuration.py diff --git a/main.py b/main.py index f3dfdc9..ef3df0c 100755 --- a/main.py +++ b/main.py @@ -7,6 +7,8 @@ from copy import deepcopy from pathlib import Path from typing import Any, Dict, List, Optional, Union +from urllib.error import HTTPError, URLError +from urllib.request import Request as UrlRequest, urlopen from urllib.parse import quote_plus, urlencode import boto3 @@ -14,7 +16,7 @@ from fastapi.staticfiles import StaticFiles from fastapi.templating import Jinja2Templates from PIL import Image -from pydantic import BaseModel +from pydantic import BaseModel, Field from sqlalchemy.orm import Session from starlette.datastructures import URL from starlette.responses import JSONResponse, RedirectResponse, Response @@ -120,6 +122,71 @@ class GameOnUtils: templates = Jinja2Templates(directory=".") +OPENPATHS_API_BASE = os.environ.get("OPENPATHS_API_BASE", "https://openpaths.io/v1") +OPENPATHS_API_KEY = os.environ.get("OPENPATHS_API_KEY", "") +OPENPATHS_ROUTER_MODELS = [ + model.strip() + for model in os.environ.get("OPENPATHS_ROUTER_MODELS", "openai-chat-latest,anthropic-opus-latest").split(",") + if model.strip() +] + + +class MarketingWorkflowRequest(BaseModel): + workflow: str + target_url: Optional[str] = "" + competitor_urls: List[str] = Field(default_factory=list) + business_context: Optional[str] = "" + + +def call_openpaths_chat(model: str, messages: List[Dict[str, str]], temperature: float = 0.2) -> str: + """ + Call OpenPaths' OpenAI-compatible endpoint (https://openpaths.io/v1/chat/completions). + """ + if not OPENPATHS_API_KEY: + raise HTTPException(status_code=503, detail="OPENPATHS_API_KEY is not configured on the server.") + + endpoint = f"{OPENPATHS_API_BASE.rstrip('/')}/chat/completions" + payload = json.dumps( + { + "model": model, + "messages": messages, + "temperature": temperature, + } + ).encode("utf-8") + + req = UrlRequest( + endpoint, + data=payload, + headers={ + "Content-Type": "application/json", + "Authorization": f"Bearer {OPENPATHS_API_KEY}", + }, + method="POST", + ) + + try: + with urlopen(req, timeout=40) as resp: + body = json.loads(resp.read().decode("utf-8")) + except HTTPError as e: + error_body = e.read().decode("utf-8", errors="ignore") + logger.error(f"OpenPaths HTTP error ({e.code}): {error_body}") + raise HTTPException(status_code=502, detail="OpenPaths request failed.") + except URLError as e: + logger.error(f"OpenPaths network error: {e}") + raise HTTPException(status_code=502, detail="Could not reach OpenPaths API.") + except Exception as e: + logger.error(f"OpenPaths unexpected error: {e}") + raise HTTPException(status_code=500, detail="Unexpected OpenPaths error.") + + content = ( + body.get("choices", [{}])[0] + .get("message", {}) + .get("content", "") + ) + if not content: + raise HTTPException(status_code=502, detail="OpenPaths returned an empty response.") + return content + def get_base_template_vars(request: Request) -> Dict[str, Any]: """ @@ -444,7 +511,8 @@ async def tool_page(request: Request, tool_name: str, db: Session = Depends(get_ "tool_keywords": tool_info.get("keywords", ""), "tool_url": f"/tools/{tool_name}", "tool_image": tool_info.get("image", ""), - "tooltemplate": f"templates/tools/{tool_name}.jinja2", + "tooltemplate": tool_info.get("template", f"templates/tools/{tool_name}.jinja2"), + "tool_workflow_key": tool_info.get("workflow_key", tool_name), "subscription_required": subscription_required and subscription_status and subscription_status.get("subscription_required", False) @@ -460,6 +528,62 @@ async def tool_page(request: Request, tool_name: str, db: Session = Depends(get_ ) +@app.post("/api/tools/marketing-workflow") +async def run_marketing_workflow(payload: MarketingWorkflowRequest): + workflow_instructions = { + "competitor-teardown": ( + "You are a conversion strategist. Produce: positioning summary, competitor strengths/weaknesses, " + "messaging gaps, and a prioritized 7-day action plan." + ), + "seo-content-gap-finder": ( + "You are an SEO strategist. Produce: topic clusters, content gaps vs competitors, " + "a prioritized publishing roadmap, and suggested briefs." + ), + "deep-researcher": ( + "You are a senior research analyst. Produce: key findings, assumptions, risks, " + "counterarguments, and next-step experiments." + ), + "keyword-glossary-explorer": ( + "You are a search growth strategist. Produce: keyword clusters, search intent buckets, " + "glossary page ideas, and internal link opportunities." + ), + } + + workflow_key = payload.workflow.strip().lower() + if workflow_key not in workflow_instructions: + raise HTTPException(status_code=400, detail="Unsupported workflow.") + + user_prompt = ( + f"Workflow: {workflow_key}\n\n" + f"Target URL: {payload.target_url or 'N/A'}\n" + f"Competitors: {', '.join(payload.competitor_urls) if payload.competitor_urls else 'N/A'}\n\n" + f"Context:\n{payload.business_context or 'N/A'}\n\n" + "Return markdown with clear headings and bullet points." + ) + + analyses = [] + for model in OPENPATHS_ROUTER_MODELS: + model_output = call_openpaths_chat( + model=model, + messages=[ + {"role": "system", "content": workflow_instructions[workflow_key]}, + {"role": "user", "content": user_prompt}, + ], + ) + analyses.append({"model": model, "content": model_output}) + + combined_markdown = "\n\n".join( + [f"## {analysis['model']}\n\n{analysis['content']}" for analysis in analyses] + ) + return JSONResponse( + { + "workflow": workflow_key, + "analyses": analyses, + "combined_markdown": combined_markdown, + } + ) + + @app.get("/subscribe") async def subscribe(request: Request): base_vars = get_base_template_vars(request) @@ -471,6 +595,8 @@ async def subscribe(request: Request): YOUR_DOMAIN = "https://text-generator.io" +MONTHLY_SUBSCRIPTION_PRICE_ID = "price_0TMflXDtz2XsjQROwqDwU3Pt" # $9.99/month +ANNUAL_SUBSCRIPTION_PRICE_ID = "price_0TMfntDtz2XsjQROebse1At5" # $99.99/year @app.post("/create-checkout-session") @@ -508,7 +634,7 @@ async def create_checkout_session( if type == "annual": line_item: Dict[str, Any] = { - "price": "price_0RXdd4Dtz2XsjQRO5hYsdfjx", # New annual price ID ($190/year) + "price": ANNUAL_SUBSCRIPTION_PRICE_ID, "quantity": 1, } elif type == "self-hosted": @@ -520,7 +646,7 @@ async def create_checkout_session( else: # Default monthly - metered subscription, no quantity line_item: Dict[str, Any] = { - "price": "price_0RXdbtDtz2XsjQROW0xgtU8H", # New monthly price ID ($19/month) + "price": MONTHLY_SUBSCRIPTION_PRICE_ID, "quantity": 1, } @@ -616,9 +742,9 @@ async def create_checkout_session_embedded( # Set up pricing based on subscription type if subscription_type and subscription_type == "annual": - subscription_price = "price_0RXdd4Dtz2XsjQRO5hYsdfjx" # $190/year + subscription_price = ANNUAL_SUBSCRIPTION_PRICE_ID else: - subscription_price = "price_0RXdbtDtz2XsjQROW0xgtU8H" # $19/month + subscription_price = MONTHLY_SUBSCRIPTION_PRICE_ID success_url = YOUR_DOMAIN + "/playground" diff --git a/questions/tool_fixtures.py b/questions/tool_fixtures.py index 6033057..efaf0c6 100755 --- a/questions/tool_fixtures.py +++ b/questions/tool_fixtures.py @@ -84,4 +84,40 @@ }, }, }, + "competitor-teardown": { + "name": "Competitor Teardown", + "description": "Analyze competing pages, uncover positioning gaps, and get actionable improvements.", + "url": "/tools/competitor-teardown", + "image": "img/prompt-optimizer.webp", + "keywords": "competitor analysis, positioning, conversion copy, landing page teardown", + "template": "static/templates/tools/marketing-workflow-tool.jinja2", + "workflow_key": "competitor-teardown", + }, + "seo-content-gap-finder": { + "name": "SEO Content Gap Finder", + "description": "Compare your site with competitors and generate a prioritized content gap plan.", + "url": "/tools/seo-content-gap-finder", + "image": "img/prompt-optimizer.webp", + "keywords": "seo content gap, topic clusters, content briefs, search growth", + "template": "static/templates/tools/marketing-workflow-tool.jinja2", + "workflow_key": "seo-content-gap-finder", + }, + "deep-researcher": { + "name": "Deep Researcher", + "description": "Build structured research summaries with risks, opportunities, and next actions.", + "url": "/tools/deep-researcher", + "image": "img/prompt-optimizer.webp", + "keywords": "deep research, market analysis, synthesis, strategic insights", + "template": "static/templates/tools/marketing-workflow-tool.jinja2", + "workflow_key": "deep-researcher", + }, + "keyword-glossary-explorer": { + "name": "Keyword & Glossary Explorer", + "description": "Turn a niche into keyword clusters and plain-English glossary pages you can publish.", + "url": "/tools/keyword-glossary-explorer", + "image": "img/prompt-optimizer.webp", + "keywords": "keyword research, glossary pages, long-tail seo, topical authority", + "template": "static/templates/tools/marketing-workflow-tool.jinja2", + "workflow_key": "keyword-glossary-explorer", + }, } diff --git a/static/css/checkout-dialog.css b/static/css/checkout-dialog.css index f0a888e..b60705c 100755 --- a/static/css/checkout-dialog.css +++ b/static/css/checkout-dialog.css @@ -76,7 +76,42 @@ font-size: 28px; font-weight: 600; color: #333; - margin: 0 0 16px 0; + margin: 0; +} + +.checkout-dialog-subtitle { + margin: 10px 0 0; + color: #666; + font-size: 14px; +} + +.checkout-price-highlight { + display: inline-flex; + align-items: baseline; + gap: 8px; + margin-top: 14px; + padding: 10px 14px; + border-radius: 999px; + background: #f3f7ff; + border: 1px solid #d9e6ff; +} + +.checkout-price-value { + color: #1155cc; + font-size: 34px; + font-weight: 800; + line-height: 1; +} + +.checkout-price-period { + color: #1155cc; + font-weight: 700; +} + +.checkout-price-note { + color: #5d6f92; + font-size: 12px; + font-weight: 600; } .checkout-pricing-toggle { @@ -84,6 +119,7 @@ gap: 16px; justify-content: center; flex-wrap: wrap; + margin-top: 14px; } .checkout-toggle-label { @@ -104,6 +140,10 @@ margin-right: 8px; } +.checkout-toggle-text { + font-weight: 600; +} + .checkout-toggle-label input[type="radio"]:checked + .checkout-toggle-text { color: #1a73e8; font-weight: 600; @@ -141,6 +181,30 @@ margin-top: 2px; } +.checkout-offer-chooser { + margin-top: 14px; + display: flex; + justify-content: center; + gap: 8px; +} + +.checkout-offer-btn { + border: 1px solid #d2d9e6; + background: #fff; + color: #334155; + border-radius: 999px; + padding: 8px 14px; + font-size: 13px; + font-weight: 600; + cursor: pointer; +} + +.checkout-offer-btn.is-active { + border-color: #1a73e8; + background: #e8f0fe; + color: #0d47a1; +} + .checkout-dialog-body { padding: 32px; } @@ -209,6 +273,42 @@ background: #1557b0; } +.checkout-btn-secondary { + background: #eef2f7; + color: #1f2937; +} + +.checkout-btn-secondary:hover { + background: #dde4ee; +} + +.checkout-credits-panel { + border: 1px solid #e2e8f0; + background: #f8fafc; + border-radius: 10px; + padding: 18px; + text-align: center; +} + +.checkout-credits-panel h3 { + margin: 0 0 8px; + font-size: 20px; + color: #1e293b; +} + +.checkout-credits-panel p { + margin: 0; + color: #475569; +} + +.checkout-credits-actions { + margin-top: 14px; + display: flex; + gap: 10px; + justify-content: center; + flex-wrap: wrap; +} + /* Mobile responsiveness */ @media (max-width: 768px) { .checkout-dialog-content { @@ -226,6 +326,10 @@ .checkout-dialog-title { font-size: 24px; } + + .checkout-price-value { + font-size: 30px; + } .checkout-pricing-toggle { flex-direction: column; @@ -261,4 +365,4 @@ .StripeElement--webkit-autofill { background-color: #fefde5 !important; -} \ No newline at end of file +} diff --git a/static/js/checkout-dialog.js b/static/js/checkout-dialog.js index c5ebccf..9dae2f5 100755 --- a/static/js/checkout-dialog.js +++ b/static/js/checkout-dialog.js @@ -1,4 +1,10 @@ // Checkout Dialog with Embedded Stripe Checkout +const CHECKOUT_PRICING = { + monthly: '$9.99', + annual: '$99.99', + savingsLabel: 'save 17%', +}; + class CheckoutDialog { constructor() { this.modal = null; @@ -8,6 +14,8 @@ class CheckoutDialog { this.elements = null; this.embeddedCheckout = null; this.subscriptionType = 'monthly'; + this.offerType = 'plan'; + this.allowCreditChoice = window.location.pathname === '/playground'; this.init(); } @@ -46,9 +54,37 @@ class CheckoutDialog {

Cloud AI Text Generator

+

Start instantly with our most popular plan.

+
+ ${CHECKOUT_PRICING.monthly} + /month + or ${CHECKOUT_PRICING.annual}/year (${CHECKOUT_PRICING.savingsLabel}) +
+
+ + +
+
+
@@ -88,6 +124,17 @@ class CheckoutDialog { } } }); + + document.addEventListener('click', (e) => { + const offerBtn = e.target.closest('.checkout-offer-btn'); + if (offerBtn) { + this.setOfferType(offerBtn.dataset.offer || 'plan'); + } + + if (e.target.id === 'checkout-switch-plan-btn') { + this.setOfferType('plan'); + } + }); } async getCurrentUser() { @@ -106,6 +153,11 @@ class CheckoutDialog { } async loadCheckout() { + if (this.offerType === 'credits') { + this.renderCreditsState(); + return; + } + const user = await this.getCurrentUser(); if (!user) { this.showError('Please log in to subscribe'); @@ -169,12 +221,54 @@ class CheckoutDialog { `; } + renderCreditsState() { + const checkoutContainer = document.getElementById('checkout-container'); + if (checkoutContainer) { + checkoutContainer.innerHTML = ''; + } + + const creditsPanel = document.getElementById('checkout-credits-panel'); + if (creditsPanel) { + creditsPanel.style.display = 'block'; + } + } + + setOfferType(offerType = 'plan') { + this.offerType = offerType; + const buttons = document.querySelectorAll('.checkout-offer-btn'); + buttons.forEach((btn) => { + btn.classList.toggle('is-active', btn.dataset.offer === offerType); + }); + + const creditsPanel = document.getElementById('checkout-credits-panel'); + if (creditsPanel) { + creditsPanel.style.display = offerType === 'credits' ? 'block' : 'none'; + } + + const checkoutContainer = document.getElementById('checkout-container'); + if (checkoutContainer) { + checkoutContainer.style.display = offerType === 'credits' ? 'none' : 'block'; + } + + if (this.isOpen && offerType === 'plan') { + this.loadCheckout(); + } else if (this.isOpen && offerType === 'credits') { + this.renderCreditsState(); + } + } + async show() { if (this.modal) { + const offerChooser = document.getElementById('checkout-offer-chooser'); + if (offerChooser) { + offerChooser.style.display = this.allowCreditChoice ? 'flex' : 'none'; + } + this.modal.style.display = 'flex'; this.modal.classList.add('show'); this.isOpen = true; document.body.style.overflow = 'hidden'; + this.setOfferType(this.allowCreditChoice ? this.offerType : 'plan'); // Load checkout when modal is shown await this.loadCheckout(); @@ -231,6 +325,7 @@ async function showCheckoutDialog(subscriptionType = 'monthly') { } } checkoutDialog.subscriptionType = subscriptionType; + checkoutDialog.offerType = window.location.pathname === '/playground' ? checkoutDialog.offerType : 'plan'; // Update radio button selection const radioBtn = document.querySelector(`input[name="pricing"][value="${subscriptionType}"]`); if (radioBtn) { @@ -239,7 +334,9 @@ async function showCheckoutDialog(subscriptionType = 'monthly') { await checkoutDialog.show(); } +window.showCheckoutDialog = showCheckoutDialog; + // Export for use in other modules if (typeof module !== 'undefined' && module.exports) { module.exports = { CheckoutDialog, checkoutDialog, showCheckoutDialog }; -} \ No newline at end of file +} diff --git a/static/js/marketing-workflow-tool.js b/static/js/marketing-workflow-tool.js new file mode 100644 index 0000000..6894342 --- /dev/null +++ b/static/js/marketing-workflow-tool.js @@ -0,0 +1,65 @@ +(function () { + const root = document.getElementById('marketing-workflow-tool'); + if (!root) return; + + const workflow = root.dataset.workflow || ''; + const runBtn = document.getElementById('run-workflow-btn'); + const status = document.getElementById('workflow-status'); + const output = document.getElementById('workflow-output'); + const outputText = document.getElementById('workflow-output-text'); + + const targetUrlInput = document.getElementById('tool-target-url'); + const competitorsInput = document.getElementById('tool-competitors'); + const contextInput = document.getElementById('tool-context'); + + function setStatus(text, isError) { + if (!status) return; + status.textContent = text || ''; + status.style.color = isError ? '#d32f2f' : '#666'; + } + + async function runWorkflow() { + const targetUrl = targetUrlInput ? targetUrlInput.value.trim() : ''; + const competitors = (competitorsInput ? competitorsInput.value : '') + .split('\n') + .map((v) => v.trim()) + .filter(Boolean); + const context = contextInput ? contextInput.value.trim() : ''; + + setStatus('Running analysis...', false); + if (runBtn) runBtn.disabled = true; + + try { + const response = await fetch('/api/tools/marketing-workflow', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + workflow, + target_url: targetUrl, + competitor_urls: competitors, + business_context: context + }) + }); + + const data = await response.json(); + if (!response.ok) { + throw new Error(data.detail || data.error || 'Workflow request failed'); + } + + if (output && outputText) { + outputText.textContent = data.combined_markdown || 'No output received.'; + output.style.display = 'block'; + } + setStatus(`Done (${(data.analyses || []).length} model(s))`, false); + } catch (error) { + console.error('Workflow tool error:', error); + setStatus(error.message || 'Failed to run workflow', true); + } finally { + if (runBtn) runBtn.disabled = false; + } + } + + if (runBtn) { + runBtn.addEventListener('click', runWorkflow); + } +})(); diff --git a/static/js/subscription-modal.js b/static/js/subscription-modal.js index 0e818cd..5088072 100755 --- a/static/js/subscription-modal.js +++ b/static/js/subscription-modal.js @@ -1,5 +1,11 @@ // Subscription Modal JavaScript if (typeof window.SubscriptionModal === 'undefined') { +const SUBSCRIPTION_MODAL_PRICING = { + monthly: '$9.99/mo', + annual: '$99.99/year', + savingsLabel: 'save 17%', +}; + window.SubscriptionModal = class SubscriptionModal { constructor() { this.modal = null; @@ -40,8 +46,8 @@ window.SubscriptionModal = class SubscriptionModal {
-
$19.00
-
per month
+
${SUBSCRIPTION_MODAL_PRICING.monthly}
+
or ${SUBSCRIPTION_MODAL_PRICING.annual} (${SUBSCRIPTION_MODAL_PRICING.savingsLabel})
    @@ -84,7 +90,8 @@ window.SubscriptionModal = class SubscriptionModal {
- Subscribe Now + +
@@ -318,6 +325,36 @@ window.SubscriptionModal = class SubscriptionModal { this.close(); } }); + + document.addEventListener('click', async (e) => { + const actionButton = e.target.closest('[data-subscription-action]'); + if (!actionButton) { + return; + } + + const action = actionButton.dataset.subscriptionAction; + if (action === 'plan') { + this.close(); + if (window.showCheckoutDialog) { + await window.showCheckoutDialog('monthly'); + } else { + window.location.href = '/subscribe'; + } + return; + } + + if (action === 'credits') { + this.close(); + if (window.showCheckoutDialog) { + if (window.checkoutDialog) { + window.checkoutDialog.offerType = 'credits'; + } + await window.showCheckoutDialog('monthly'); + } else { + window.location.href = '/contact'; + } + } + }); } async checkSubscription() { diff --git a/static/js/unlimited-ai-modal.js b/static/js/unlimited-ai-modal.js index 59808a9..74aa090 100755 --- a/static/js/unlimited-ai-modal.js +++ b/static/js/unlimited-ai-modal.js @@ -1,5 +1,10 @@ // Unlimited AI Offerings Modal JavaScript if (typeof window.UnlimitedAIModal === 'undefined') { +const UNLIMITED_AI_PRICING = { + monthly: '$9.99 USD', + period: 'Per month', +}; + window.UnlimitedAIModal = class UnlimitedAIModal { constructor() { this.modal = null; @@ -39,8 +44,8 @@ window.UnlimitedAIModal = class UnlimitedAIModal {
-
$19.00 USD
-
Monthly
+
${UNLIMITED_AI_PRICING.monthly}
+
${UNLIMITED_AI_PRICING.period}
    @@ -369,4 +374,4 @@ if (typeof window.unlimitedAIModal === 'undefined') { // Export for use in other modules if (typeof module !== 'undefined' && module.exports) { module.exports = { UnlimitedAIModal, unlimitedAIModal, showUnlimitedAIModal }; -} \ No newline at end of file +} diff --git a/static/templates/ai-text-editor.jinja2 b/static/templates/ai-text-editor.jinja2 index 77e0fa4..3c2e28b 100755 --- a/static/templates/ai-text-editor.jinja2 +++ b/static/templates/ai-text-editor.jinja2 @@ -93,8 +93,22 @@ /* Responsive styling */ @media (max-width: 768px) { + .tgdocs-main { + overflow-x: hidden; + } + .tgdocs-sidebar { - width: 200px; + width: 100%; + max-width: 100vw; + border-right: none; + } + + .tgdocs-sidebar-header { + padding: 12px 16px; + } + + .tgdocs-document-item { + padding: 12px 16px; } .tgdocs-toolbar { @@ -165,4 +179,4 @@ -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/static/templates/shared/index.jinja2 b/static/templates/shared/index.jinja2 index 5058205..3aeb39e 100755 --- a/static/templates/shared/index.jinja2 +++ b/static/templates/shared/index.jinja2 @@ -77,6 +77,30 @@

    Image Captioning AI

    Extract useful insight from images with fast multimodal analysis suited for real customer workflows.

    + +
    + +

    Competitor Teardown

    +

    Audit competing pages and get a prioritized list of positioning and conversion opportunities.

    +
    + +
    + +

    SEO Content Gap Finder

    +

    Find missing topics, prioritize content opportunities, and build practical publishing roadmaps.

    +
    + +
    + +

    Keyword & Glossary Explorer

    +

    Generate keyword clusters and glossary plans that improve long-tail topical coverage.

    +
    + +
    + +

    Deep Researcher

    +

    Synthesize complex topics into clear findings, risks, and next-step recommendations.

    +
diff --git a/static/templates/shared/subscribe.jinja2 b/static/templates/shared/subscribe.jinja2 index 062d9b9..f8e676f 100755 --- a/static/templates/shared/subscribe.jinja2 +++ b/static/templates/shared/subscribe.jinja2 @@ -96,12 +96,12 @@ window.addEventListener('load', function() { function updatePricing() { if ($('.subscription-toggle').is(':checked')) { $('.subscription-period').text('Annually'); - $('.money-amount').text('$190.00 USD'); + $('.money-amount').text('$99.99 USD'); $('input[name="type"]').val('annual'); $('.discount-chip').show(); } else { $('.subscription-period').text('Monthly'); - $('.money-amount').text('$19.00 USD'); + $('.money-amount').text('$9.99 USD'); $('input[name="type"]').val('monthly'); $('.discount-chip').hide(); } @@ -187,7 +187,7 @@ window.addEventListener('load', function() { ">cloud

Cloud Text Generator

-

$190.00 USD Annually

+

$99.99 USD Annually