diff --git a/frontend_multi_user/src/app.py b/frontend_multi_user/src/app.py index b96fade4..05d3c622 100644 --- a/frontend_multi_user/src/app.py +++ b/frontend_multi_user/src/app.py @@ -1038,113 +1038,126 @@ def inject_current_user_name(): @self.app.route('/') def index(): user = None - recent_tasks: list[SimpleNamespace] = [] - total_tasks_count = 0 + admin_account = None is_admin = False - nonce = None - user_id = None - can_create_plan = False - example_prompts: list[str] = [] - credits_balance_display = "0" + onboarding_steps: list[dict] = [] + if current_user.is_authenticated: is_admin = current_user.is_admin try: if is_admin: admin_account = _get_current_user_account() - user_id = str(admin_account.id) if admin_account else self.admin_username user = SimpleNamespace(name="Admin", given_name=None) - credits_balance_display = "Full access" - can_create_plan = True + user_id = str(admin_account.id) if admin_account else self.admin_username else: user_uuid = uuid.UUID(str(current_user.id)) user = self.db.session.get(UserAccount, user_uuid) - if user: - user_id = str(user.id) + user_id = str(user.id) if user else None + + if user and user_id: + account_id = getattr(admin_account, 'id', None) if is_admin else getattr(user, 'id', None) + + # Step 1: Account created (always done if logged in) + onboarding_steps.append({ + "title": "Create account", + "description": "Sign up for PlanExe to get started.", + "done": True, + "detail": "Signed in", + "link": None, + }) + + # Step 2: Deposit credits + if is_admin: + has_credits = True + credit_detail = "Full access" + else: credits_balance = to_credit_decimal(user.credits_balance) - credits_balance_display = format_credit_display(user.credits_balance) - min_credits = Decimal(os.environ.get("PLANEXE_MIN_CREDITS_TO_CREATE_PLAN", "2")) - can_create_plan = credits_balance >= min_credits - - if user_id: - # Generate a nonce so the user can start a plan from the dashboard - nonce = 'DASH_' + str(uuid.uuid4()) + tx_count = CreditHistory.query.filter_by(user_id=user.id).count() + has_credits = credits_balance > 0 or tx_count > 1 + credit_detail = format_credit_display(user.credits_balance) if has_credits else "No credits yet" + onboarding_steps.append({ + "title": "Deposit credits", + "description": 'Credits pay for the AI models that generate your plan. Go to Account to add credits.', + "done": has_credits, + "detail": credit_detail, + "link": url_for('account') if not has_credits else None, + }) + + # Step 3: Create API key + key_count = UserApiKey.query.filter_by(user_id=account_id, revoked_at=None).count() if account_id else 0 + has_key = key_count >= 1 + if key_count == 0: + key_detail = "No API keys yet" + elif key_count == 1: + key_detail = "1 API key" + else: + key_detail = f"{key_count} API keys" + onboarding_steps.append({ + "title": "Create API key", + "description": 'Your AI assistant uses an API key to connect to PlanExe. Create one on the Account page.', + "done": has_key, + "detail": key_detail, + "link": url_for('account') if not has_key else None, + }) + + # Step 4: Use MCP (check if any API key has LLM calls) + total_llm_calls = 0 + if has_key: + user_key_ids = [ + str(k.id) for k in UserApiKey.query + .filter_by(user_id=account_id, revoked_at=None) + .all() + ] if account_id else [] + if user_key_ids: + try: + total_llm_calls = ( + self.db.session.query(func.count(TokenMetrics.id)) + .filter(TokenMetrics.api_key_id.in_(user_key_ids)) + .scalar() or 0 + ) + except Exception: + self.db.session.rollback() + used_mcp = total_llm_calls >= 1 + onboarding_steps.append({ + "title": "Connect via MCP", + "description": "Add PlanExe to your AI tool (Claude, Cursor, Windsurf, etc.) using your API key. Your AI will then be able to create plans for you.", + "done": used_mcp, + "detail": f"{total_llm_calls} LLM calls" if used_mcp else "Not connected yet", + "link": "https://docs.planexe.org/mcp/mcp_welcome/" if not used_mcp else None, + }) + + # Step 5: Create 5+ plans uid_filter = ( PlanItem.user_id.in_(_admin_user_ids()) if is_admin else PlanItem.user_id == str(user_id) ) - try: - recent_task_rows = ( - self.db.session.query( - PlanItem.id, - PlanItem.state, - PlanItem.stop_requested, - func.substr(PlanItem.prompt, 1, 240).label("prompt_preview"), - ) - .filter(uid_filter) - .order_by(PlanItem.timestamp_created.desc()) - .limit(10) - .all() - ) - except DataError: - self.db.session.rollback() - logger.warning( - "Detected invalid UTF-8 in task_item.prompt for user_id=%s while loading dashboard; " - "falling back without prompt previews.", - user_id, - exc_info=True, - ) - recent_task_rows = ( - self.db.session.query( - PlanItem.id, - PlanItem.state, - PlanItem.stop_requested, - ) - .filter(uid_filter) - .order_by(PlanItem.timestamp_created.desc()) - .limit(10) - .all() - ) - recent_tasks = [] - for task in recent_task_rows: - prompt_preview = getattr(task, "prompt_preview", None) - if prompt_preview is None: - from src.plan_routes import _load_prompt_preview_safe - prompt_text = _load_prompt_preview_safe(task.id) - else: - prompt_text = (prompt_preview or "").strip() or "[Prompt unavailable]" - state = task.state if isinstance(task.state, PlanState) else None - recent_tasks.append( - SimpleNamespace( - id=str(task.id), - state=state, - prompt=prompt_text, - ) - ) - total_tasks_count = ( - PlanItem.query - .filter(uid_filter) - .count() - ) - # Load example prompts for the "Start New Plan" form - for prompt_uuid in DEMO_FORM_RUN_PROMPT_UUIDS: - prompt_item = self.prompt_catalog.find(prompt_uuid) - if prompt_item: - example_prompts.append(prompt_item.prompt) + total_plans = PlanItem.query.filter(uid_filter).count() + is_superuser = total_plans >= 5 + onboarding_steps.append({ + "title": "Superuser", + "description": "Create 5 or more plans to earn the Superuser badge.", + "done": is_superuser, + "detail": f"{total_plans} plans created" if is_superuser else f"{total_plans}/5 plans", + "link": None, + }) except Exception: logger.debug("Could not load dashboard data", exc_info=True) + + # Debug overrides: /?debug=1&step1=0&step2=1&step3=0&step4=1&step5=0 + if request.args.get("debug") == "1" and onboarding_steps: + step_keys = ["step1", "step2", "step3", "step4", "step5"] + for i, key in enumerate(step_keys): + val = request.args.get(key) + if val is not None and i < len(onboarding_steps): + onboarding_steps[i]["done"] = val == "1" + return render_template( 'index.html', user=user, - credits_balance_display=credits_balance_display, - can_create_plan=can_create_plan, - total_tasks_count=total_tasks_count, - recent_tasks=recent_tasks, is_admin=is_admin, - nonce=nonce, - user_id=user_id, - example_prompts=example_prompts, - model_profile_options=_model_profile_options(), + onboarding_steps=onboarding_steps, + onboarding_debug=request.args.get("debug") == "1", ) @self.app.route('/models') diff --git a/frontend_multi_user/src/plan_routes.py b/frontend_multi_user/src/plan_routes.py index e39a61d5..cfe2d225 100644 --- a/frontend_multi_user/src/plan_routes.py +++ b/frontend_multi_user/src/plan_routes.py @@ -695,6 +695,34 @@ def run(): return render_template("run_via_database.html", run_id=task_id) +@plan_routes_bp.route("/plan/create", methods=["GET"]) +@login_required +def plan_create_page(): + from src.app import _model_profile_options, DEMO_FORM_RUN_PROMPT_UUIDS + prompt_catalog = current_app.config["PROMPT_CATALOG"] + example_prompts: list[str] = [] + for prompt_uuid in DEMO_FORM_RUN_PROMPT_UUIDS: + prompt_item = prompt_catalog.find(prompt_uuid) + if prompt_item: + example_prompts.append(prompt_item.prompt) + + can_create_plan = True + if not current_user.is_admin: + user = _get_current_user_account() + if user: + min_credits = Decimal(os.environ.get("PLANEXE_MIN_CREDITS_TO_CREATE_PLAN", "2")) + can_create_plan = to_credit_decimal(user.credits_balance) >= min_credits + + nonce = "CREATE_" + str(uuid.uuid4()) + return render_template( + "plan_create.html", + can_create_plan=can_create_plan, + nonce=nonce, + example_prompts=example_prompts, + model_profile_options=_model_profile_options(), + ) + + @plan_routes_bp.route("/create_plan", methods=["POST"]) @login_required @_nocache diff --git a/frontend_multi_user/templates/base.html b/frontend_multi_user/templates/base.html index c2087b1c..25a4509c 100644 --- a/frontend_multi_user/templates/base.html +++ b/frontend_multi_user/templates/base.html @@ -245,6 +245,7 @@ Home {% if current_user_name %} + Create Plans Models {% endif %} diff --git a/frontend_multi_user/templates/index.html b/frontend_multi_user/templates/index.html index 1ee9ae5d..56e85285 100644 --- a/frontend_multi_user/templates/index.html +++ b/frontend_multi_user/templates/index.html @@ -130,70 +130,103 @@ text-decoration: underline; } - /* ── Dashboard (signed in) ──────────────────────── */ - .dashboard-welcome { - margin-bottom: 32px; - } - .dashboard-welcome h1 { - font-size: 1.75rem; + /* ── Onboarding steps ──────────────────────────── */ + .onboarding-title { + font-size: 1.1rem; font-weight: 700; margin: 0 0 4px; } - .dashboard-welcome p { + .onboarding-subtitle { + font-size: 0.82rem; color: var(--color-text-secondary); - margin: 0; - font-size: 0.95rem; + margin: 0 0 20px; } - - .stats-row { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); - gap: 16px; - margin-bottom: 32px; + .onboarding-steps { + max-width: 480px; } - .stat-card { - background: var(--color-bg-soft); - border: 1px solid var(--color-border); - border-radius: var(--radius-lg); - padding: 20px 24px; - text-align: center; + .onboarding-step { + display: flex; + align-items: flex-start; + gap: 12px; + padding: 12px 0; + border-bottom: 1px solid var(--color-border); } - .stat-value { - font-size: 1.75rem; - font-weight: 800; - color: var(--color-text); - line-height: 1.2; + .onboarding-step:last-child { + border-bottom: none; } - .stat-value.credits { - color: #059669; + .onboarding-check { + flex-shrink: 0; + width: 22px; + height: 22px; + border-radius: 50%; + border: 2px solid var(--color-border); + display: flex; + align-items: center; + justify-content: center; + margin-top: 1px; + font-size: 0.7rem; + color: transparent; } - .stat-card-link { - display: block; - text-decoration: none; - color: inherit; - transition: border-color 0.15s, box-shadow 0.15s; + .onboarding-check.done { + background: #059669; + border-color: #059669; + color: #fff; } - .stat-card-link:hover { - border-color: var(--color-primary); - box-shadow: 0 0 0 1px var(--color-primary); + .onboarding-check.done::after { + content: "\2713"; } - .stat-card-link .stat-value.credits { - text-decoration: underline; - text-underline-offset: 3px; - text-decoration-color: rgba(5, 150, 105, 0.4); + .onboarding-step-body { + flex: 1; + min-width: 0; } - .stat-card-link:hover .stat-value.credits { - text-decoration-color: var(--color-primary); + .onboarding-step-title { + font-size: 0.88rem; + font-weight: 600; + color: var(--color-text); + margin: 0; + } + .onboarding-step.pending .onboarding-step-title { + color: var(--color-text-secondary); } - .stat-label { - font-size: 0.8rem; + .onboarding-step-desc { + font-size: 0.78rem; color: var(--color-text-secondary); - text-transform: uppercase; - letter-spacing: 0.05em; + margin: 4px 0 0; + line-height: 1.5; + } + .onboarding-step-desc a { + color: var(--color-primary, #0066cc); font-weight: 600; - margin-top: 4px; + text-decoration: none; + } + .onboarding-step-desc a:hover { + text-decoration: underline; + } + .onboarding-step-detail { + font-size: 0.75rem; + color: var(--color-text-secondary); + margin: 2px 0 0; + } + .onboarding-step-link { + font-size: 0.75rem; + color: var(--color-primary, #0066cc); + text-decoration: none; + } + .onboarding-step-link:hover { + text-decoration: underline; + } + .superuser-badge { + display: inline-block; + padding: 2px 8px; + background: #fef3c7; + border: 1px solid #f59e0b; + border-radius: 10px; + font-size: 0.7rem; + font-weight: 700; + color: #92400e; + margin-left: 6px; + vertical-align: middle; } - .quick-actions { display: flex; gap: 12px; @@ -234,353 +267,8 @@ color: var(--color-text); } - /* Recent tasks table */ - .section-title { - font-size: 1.1rem; - font-weight: 700; - margin: 0 0 16px; - } - .tasks-list { - border: 1px solid var(--color-border); - border-radius: var(--radius-lg); - overflow: hidden; - } - .task-row { - display: flex; - align-items: center; - gap: 16px; - padding: 14px 20px; - border-bottom: 1px solid var(--color-border); - background: var(--color-bg); - transition: background 0.1s; - } - .task-row-link { - display: block; - text-decoration: none; - color: inherit; - } - .task-row:last-child { - border-bottom: none; - } - .task-row:hover { - background: var(--color-bg-soft); - } - .task-row-link:hover { - text-decoration: none; - } - .task-status { - flex-shrink: 0; - width: 10px; - height: 10px; - border-radius: 50%; - } - .task-status.completed { background: #10b981; } - .task-status.processing { background: #f59e0b; } - .task-status.pending { background: #94a3b8; } - .task-status.failed { background: #ef4444; } - .task-status.stopped { background: #ea580c; } - .task-prompt { - flex: 1; - font-size: 0.9rem; - color: var(--color-text); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - min-width: 0; - } - .task-meta { - flex-shrink: 0; - font-size: 0.8rem; - color: var(--color-text-secondary); - white-space: nowrap; - } - .empty-state { - text-align: center; - padding: 48px 24px; - color: var(--color-text-secondary); - } - .empty-state p { - margin: 0 0 16px; - font-size: 0.95rem; - } - /* ── New plan form ──────────────────────────────── */ - .new-plan-section { - background: var(--color-card-bg); - border: 1px solid var(--color-border); - border-radius: var(--radius-lg); - margin-bottom: 32px; - box-shadow: 0 1px 3px var(--color-card-shadow); - } - .new-plan-header { - display: flex; - align-items: center; - gap: 10px; - padding: 16px 20px; - border-bottom: 1px solid var(--color-border); - } - .new-plan-header svg { - width: 20px; - height: 20px; - color: var(--color-primary); - flex-shrink: 0; - } - .new-plan-header h2 { - font-size: 1.05rem; - font-weight: 700; - margin: 0; - } - .new-plan-body { - padding: 20px; - } - .new-plan-body textarea { - width: 100%; - min-height: 120px; - padding: 12px 14px; - border: 1px solid var(--color-border); - border-radius: var(--radius); - font-family: var(--font-sans); - font-size: 0.9rem; - line-height: 1.5; - resize: vertical; - outline: none; - transition: border-color 0.15s; - box-sizing: border-box; - color: var(--color-text); - background: var(--color-bg); - } - .new-plan-body textarea:focus { - border-color: var(--color-primary); - box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1); - } - .new-plan-body textarea::placeholder { - color: var(--color-text-secondary); - } - .example-chips { - display: flex; - gap: 8px; - flex-wrap: wrap; - margin-bottom: 12px; - } - .example-chip { - display: inline-flex; - align-items: center; - gap: 4px; - padding: 5px 12px; - background: var(--color-bg-soft); - border: 1px solid var(--color-border); - border-radius: 20px; - font-size: 0.78rem; - font-weight: 500; - color: var(--color-text-secondary); - cursor: pointer; - transition: border-color 0.15s, color 0.15s; - } - .example-chip:hover { - border-color: var(--color-primary); - color: var(--color-primary); - } - .new-plan-footer { - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; - margin-top: 14px; - } - .new-plan-footer .footer-spacer { - flex: 1 1 auto; - } - .new-plan-footer .model-profile-inline { - width: auto; - min-width: 0; - max-width: none; - } - .model-profile-picker { - position: relative; - } - .model-profile-button { - width: auto; - display: flex; - align-items: center; - justify-content: flex-start; - gap: 0; - padding: 6px 8px; - border-radius: 8px; - border: 1px solid var(--color-border); - background: var(--color-bg-soft); - color: var(--color-text); - cursor: pointer; - text-align: left; - } - .model-profile-button:hover { - border-color: var(--color-primary); - } - .model-profile-selected { - display: flex; - flex-direction: column; - line-height: 1.2; - } - .model-profile-title { - font-size: 0.82rem; - font-weight: 700; - } - .model-profile-subtitle { - font-size: 0.75rem; - color: var(--color-text-secondary); - } - .model-profile-caret { - color: var(--color-text); - opacity: 0.95; - display: inline-flex; - align-items: center; - justify-content: center; - width: 16px; - height: 16px; - flex: 0 0 16px; - } - .model-profile-caret svg { - width: 16px; - height: 16px; - display: block; - } - .model-profile-menu { - position: absolute; - top: calc(100% + 6px); - left: 0; - width: max-content; - min-width: 260px; - z-index: 20; - background: var(--color-bg); - border: 1px solid var(--color-border); - border-radius: 8px; - box-shadow: 0 8px 24px var(--color-card-shadow); - overflow: hidden; - display: none; - } - .model-profile-menu.open { - display: block; - } - .model-profile-menu-header { - padding: 8px 12px; - font-size: 0.72rem; - font-weight: 700; - text-transform: uppercase; - letter-spacing: 0.03em; - color: var(--color-text-secondary); - border-bottom: 1px solid var(--color-border); - background: var(--color-bg-soft); - } - .model-profile-option { - width: 100%; - border: none; - border-bottom: 1px solid var(--color-border); - background: transparent; - color: var(--color-text); - text-align: left; - padding: 10px 12px; - cursor: pointer; - display: flex; - flex-direction: column; - gap: 2px; - } - .model-profile-option:last-child { - border-bottom: none; - } - .model-profile-option:hover { - background: var(--color-bg-soft); - } - /* ── Start date picker ─────────────────────────── */ - .start-date-wrapper { - display: flex; - align-items: center; - } - .start-date-toggle { - display: inline-flex; - align-items: center; - gap: 4px; - padding: 6px 8px; - background: none; - border: 1px solid var(--color-border); - border-radius: var(--radius); - font-family: var(--font-sans); - font-size: 0.8rem; - color: var(--color-text-secondary); - cursor: pointer; - transition: border-color 0.15s, color 0.15s; - } - .start-date-toggle:hover { - border-color: var(--color-primary); - color: var(--color-primary); - } - .start-date-toggle svg { - width: 14px; - height: 14px; - } - .start-date-row { - display: none; - align-items: center; - gap: 8px; - } - .start-date-row.open { - display: flex; - } - .start-date-row input[type="date"] { - padding: 6px 10px; - border: 1px solid var(--color-border); - border-radius: var(--radius); - font-family: var(--font-sans); - font-size: 0.82rem; - color: var(--color-text); - background: var(--color-bg); - outline: none; - transition: border-color 0.15s; - } - .start-date-row input[type="datetime-local"]:focus { - border-color: var(--color-primary); - box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1); - } - .start-date-clear { - padding: 4px 8px; - background: none; - border: 1px solid var(--color-border); - border-radius: var(--radius); - font-family: var(--font-sans); - font-size: 0.75rem; - color: var(--color-text-secondary); - cursor: pointer; - transition: border-color 0.15s, color 0.15s; - } - .start-date-clear:hover { - border-color: var(--color-primary); - color: var(--color-primary); - } - .btn-start-plan { - display: inline-flex; - align-items: center; - gap: 8px; - padding: 10px 28px; - background: var(--color-primary); - color: #fff; - border: none; - border-radius: var(--radius); - font-size: 0.9rem; - font-weight: 600; - cursor: pointer; - transition: background 0.15s, transform 0.1s; - } - .btn-start-plan:hover { - background: var(--color-primary-hover); - transform: translateY(-1px); - } - .btn-start-plan:disabled { - opacity: 0.5; - cursor: not-allowed; - transform: none; - } - .btn-start-plan svg { - width: 16px; - height: 16px; - } + /* ── Admin view ─────────────────────────────────── */ .admin-notice { text-align: center; @@ -601,7 +289,6 @@ .hero { padding: 40px 0 32px; } .hero-actions { flex-direction: column; align-items: center; } .btn-hero-outline { margin-left: 0; } - .stats-row { grid-template-columns: 1fr; } } {% endblock %} @@ -609,106 +296,54 @@ {% block content %} {% if user %} {# ─── Dashboard for signed-in users ─────────────────────────── #} -
Your PlanExe dashboard — manage your plans and credits.
-Connect PlanExe with your AI assistant to create plans.
-{# ── Start New Plan form ────────────────────────────────────── #} -+ {{ step.title }} + {% if step.title == "Superuser" and step.done %} + Superuser + {% endif %} +
+ {% if not step.done and step.description %} +{{ step.description | safe }}
+ {% endif %} ++ {{ step.detail }} + {% if step.link %} + · {% if "docs.planexe" in step.link %}Setup guide{% else %}Go{% endif %} → + {% endif %} +
You haven't created any plans yet. Describe your idea above and click Generate Plan to get started.
+ ++ Need help? Join the PlanExe Discord for setup assistance and community support. +
{% for step in onboarding_steps %}
+{{ step.title }}:
+ done: {{ step.done }}
+ detail: {{ step.detail }}
+ link: {{ step.link or "—" }}
+{% endfor %}
+Override steps with URL parameters (1=done, 0=pending):
+ /?debug=1&step1=1&step2=1&step3=1&step4=1&step5=1 — all done
+ /?debug=1&step1=1&step2=0&step3=0&step4=0&step5=0 — new user
+ /?debug=1&step5=1 — force superuser
++ Tip: You can type a prompt above to try PlanExe out, however the intended way is when your AI assistant writes the prompt for you. A good prompt is detailed — it includes your goal, constraints, budget, timeline, and context. Writing all of that by hand takes effort and it's easy to leave out important details. +
++ The recommended way to use PlanExe is through MCP (Model Context Protocol). Connect PlanExe to your preferred AI tool — such as Claude, Cursor, or Windsurf — and let it interview you about your idea, then compose a thorough prompt on your behalf. This consistently produces higher-quality plans because the AI ensures nothing important is missing. +
++ See the MCP documentation for how to set up MCP with your AI tool. +
+