From 7d8a7fb92bf9c576047cb7269a3178f084e967be Mon Sep 17 00:00:00 2001 From: otdoges Date: Mon, 8 Sep 2025 00:30:15 +0000 Subject: [PATCH 1/2] Capy jam: Unify pricing to Free and Pro (0/mo); wire Autumn checkout/portal; consolidate Autumn wrapper; reduce Convex writes on home; default sequential thinking to Kimi K2; update env example --- .env.example | 2 + AutumnWrapper.tsx | 14 +- app/api/autumn/billing-portal/route.ts | 45 ++++ app/api/autumn/checkout/route.ts | 49 ++++ app/components/AutumnFallback.tsx | 53 +--- app/page.tsx | 82 +++---- app/pricing/page.tsx | 138 +++++------ app/test-pricing/page.tsx | 117 ++------- components/ConvexChat.tsx | 24 +- components/EnhancedSettingsModal.tsx | 46 +--- components/MasterDashboard.tsx | 4 +- components/PricingModal.tsx | 150 +++--------- components/stripe/SubscriptionPlans.tsx | 47 +--- lib/groq-sequential-thinking.ts | 310 +++++++----------------- 14 files changed, 406 insertions(+), 675 deletions(-) create mode 100644 app/api/autumn/billing-portal/route.ts create mode 100644 app/api/autumn/checkout/route.ts diff --git a/.env.example b/.env.example index 76232cd4..da17254d 100644 --- a/.env.example +++ b/.env.example @@ -23,6 +23,8 @@ GROQ_API_KEY=your_groq_api_key_here # Get yours at https://dashboard.stripe.com/apikeys NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_your_stripe_publishable_key_here STRIPE_SECRET_KEY=sk_test_your_stripe_secret_key_here +# Stripe Pro price ID (fallback when Autumn isn't configured) +NEXT_PUBLIC_STRIPE_PRO_PRICE_ID=price_your_pro_monthly_price_id # Stripe Webhook Secret (get from Stripe Dashboard > Webhooks) STRIPE_WEBHOOK_SECRET=whsec_your_webhook_secret_here diff --git a/AutumnWrapper.tsx b/AutumnWrapper.tsx index 0f8e6828..71db4d48 100644 --- a/AutumnWrapper.tsx +++ b/AutumnWrapper.tsx @@ -1,14 +1,2 @@ "use client"; -import { AutumnProvider } from "autumn-js/react"; -import { api } from "./convex/_generated/api"; -import { useConvex } from "convex/react"; - -export function AutumnWrapper({ children }: { children: React.ReactNode }) { - const convex = useConvex(); - - return ( - - {children} - - ); -} \ No newline at end of file +export { AutumnWrapper } from "./app/components/AutumnWrapper"; diff --git a/app/api/autumn/billing-portal/route.ts b/app/api/autumn/billing-portal/route.ts new file mode 100644 index 00000000..c1416906 --- /dev/null +++ b/app/api/autumn/billing-portal/route.ts @@ -0,0 +1,45 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getAuth } from '@clerk/nextjs/server'; +import { autumn } from '@/convex/autumn'; + +export async function POST(request: NextRequest) { + try { + const auth = getAuth(request); + if (!auth.userId) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const { returnUrl } = await request.json().catch(() => ({ returnUrl: undefined })); + const origin = request.headers.get('origin') || process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'; + + if (process.env.AUTUMN_SECRET_KEY) { + try { + const mockCtx = { auth: { getUserIdentity: () => ({ subject: auth.userId, name: '', email: '' }) } } as any; + const result: any = await autumn.billingPortal(mockCtx, { returnUrl: returnUrl || `${origin}/pricing` }); + const url = result?.url || result?.portalUrl || result?.redirectUrl; + if (url) { + return NextResponse.json({ url }); + } + } catch (e) { + // fall through to Stripe + } + } + + // Fallback to Stripe portal + const stripeRes = await fetch(`${origin}/api/stripe/customer-portal`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ returnUrl: returnUrl || `${origin}/pricing` }) + }); + + if (!stripeRes.ok) { + const err = await stripeRes.text(); + return NextResponse.json({ error: err || 'Failed to create portal session' }, { status: 500 }); + } + + const data = await stripeRes.json(); + return NextResponse.json({ url: data.url }); + } catch (error) { + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }); + } +} diff --git a/app/api/autumn/checkout/route.ts b/app/api/autumn/checkout/route.ts new file mode 100644 index 00000000..78af7151 --- /dev/null +++ b/app/api/autumn/checkout/route.ts @@ -0,0 +1,49 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getAuth } from '@clerk/nextjs/server'; +import { autumn } from '@/convex/autumn'; + +export async function POST(request: NextRequest) { + try { + const auth = getAuth(request); + if (!auth.userId) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const { priceId } = await request.json().catch(() => ({ priceId: undefined })); + + const mockCtx = { auth: { getUserIdentity: () => ({ subject: auth.userId, name: '', email: '' }) } } as any; + + if (process.env.AUTUMN_SECRET_KEY) { + try { + const result: any = await autumn.checkout(mockCtx, { priceId }); + const url = result?.url || result?.sessionUrl || result?.redirectUrl; + if (url) { + return NextResponse.json({ url }); + } + } catch (e) { + // fall through to Stripe + } + } + + // Fallback to Stripe + const origin = request.headers.get('origin') || process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'; + const stripeRes = await fetch(`${origin}/api/stripe/create-checkout-session`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + priceId: priceId || process.env.NEXT_PUBLIC_STRIPE_PRO_PRICE_ID, + mode: 'subscription' + }) + }); + + if (!stripeRes.ok) { + const err = await stripeRes.text(); + return NextResponse.json({ error: err || 'Failed to create checkout session' }, { status: 500 }); + } + + const { sessionId } = await stripeRes.json(); + return NextResponse.json({ provider: 'stripe', sessionId }); + } catch (error) { + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }); + } +} diff --git a/app/components/AutumnFallback.tsx b/app/components/AutumnFallback.tsx index 9cdeca18..7de973c2 100644 --- a/app/components/AutumnFallback.tsx +++ b/app/components/AutumnFallback.tsx @@ -4,9 +4,8 @@ import React, { createContext, useContext, useState, useEffect } from 'react'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; -import { CheckCircle, XCircle, AlertCircle } from "lucide-react"; +import { CheckCircle } from "lucide-react"; -// Types interface Customer { id: string; email: string; @@ -25,7 +24,6 @@ interface UsageLimit { resetAt?: Date; } -// Mock context const AutumnContext = createContext<{ customer: Customer | null; isLoading: boolean; @@ -36,16 +34,13 @@ const AutumnContext = createContext<{ error: null }); -// Mock provider export const AutumnProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { const [customer, setCustomer] = useState(null); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); - // Mock loading customer data useEffect(() => { setIsLoading(true); - // Simulate API call setTimeout(() => { setCustomer({ id: 'cus_mock123', @@ -54,7 +49,7 @@ export const AutumnProvider: React.FC<{ children: React.ReactNode }> = ({ childr subscription: { status: 'active', planId: 'pro', - currentPeriodEnd: Date.now() + 30 * 24 * 60 * 60 * 1000 // 30 days from now + currentPeriodEnd: Date.now() + 30 * 24 * 60 * 60 * 1000 } }); setIsLoading(false); @@ -68,7 +63,6 @@ export const AutumnProvider: React.FC<{ children: React.ReactNode }> = ({ childr ); }; -// Mock hook for customer data export const useCustomer = () => { const context = useContext(AutumnContext); return { @@ -78,7 +72,6 @@ export const useCustomer = () => { }; }; -// Mock pricing table component export const PricingTable: React.FC = () => { const plans = [ { @@ -88,10 +81,9 @@ export const PricingTable: React.FC = () => { period: 'forever', description: 'Perfect for getting started', features: [ - '5 AI chat messages per day', - '1 sandbox environment', - 'Basic code generation', - 'Community support' + '5 chats', + '1 sandbox', + 'Basic models' ], buttonText: 'Get Started', popular: false @@ -99,41 +91,20 @@ export const PricingTable: React.FC = () => { { id: 'pro', name: 'Pro', - price: '$29', + price: '$20', period: 'per month', - description: 'For serious developers', + description: 'For builders who want more', features: [ - 'Unlimited AI chat messages', - '5 concurrent sandboxes', - 'Advanced code generation', - 'Priority support', - 'Autonomous agents', - 'Custom domains' + 'Unlimited chats', + 'Advanced models' ], - buttonText: 'Start Free Trial', + buttonText: 'Upgrade to Pro', popular: true - }, - { - id: 'enterprise', - name: 'Enterprise', - price: 'Custom', - period: '', - description: 'For teams and organizations', - features: [ - 'Everything in Pro', - 'Unlimited sandboxes', - 'Team collaboration', - 'SLA guarantee', - 'Custom integrations', - 'Dedicated support' - ], - buttonText: 'Contact Sales', - popular: false } ]; return ( -
+
{plans.map((plan) => ( { ); }; -// Mock usage limit hook export const useUsageLimits = (featureId: string) => { const [limits, setLimits] = useState(null); const [isLoading, setIsLoading] = useState(false); useEffect(() => { setIsLoading(true); - // Simulate API call setTimeout(() => { setLimits({ featureId, diff --git a/app/page.tsx b/app/page.tsx index ca859a95..b3c7cd04 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -493,36 +493,7 @@ Tip: I automatically detect and install npm packages from your code imports (lik iframeRef.current.src = data.url; } }, 100); - - // Capture screenshot for chat preview after sandbox is ready and update Convex - if (currentChatId && isSignedIn) { - setTimeout(async () => { - try { - console.log('[createSandbox] Capturing screenshot for chat preview...'); - const screenshotResponse = await fetch('/api/scrape-screenshot', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ url: data.url }) - }); - - const screenshotData = await screenshotResponse.json(); - if (screenshotData.success && screenshotData.screenshot) { - console.log('[createSandbox] Screenshot captured, updating chat...'); - await updateChatScreenshot({ - chatId: currentChatId, - screenshot: screenshotData.screenshot, - sandboxId: data.sandboxId, - sandboxUrl: data.url - }); - console.log('[createSandbox] Chat updated with screenshot'); - } else { - console.warn('[createSandbox] Failed to capture screenshot:', screenshotData.error); - } - } catch (error) { - console.error('[createSandbox] Error capturing screenshot:', error); - } - }, 3000); // Wait 3 seconds for the sandbox to fully load - } + } else { throw new Error(data.error || 'Unknown error'); } @@ -1127,6 +1098,29 @@ Tip: I automatically detect and install npm packages from your code imports (lik } }; + const savePreview = async () => { + try { + if (!sandboxData?.url || !currentChatId) return; + const screenshotResponse = await fetch('/api/scrape-screenshot', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ url: sandboxData.url }) + }); + const screenshotData = await screenshotResponse.json(); + if (screenshotData.success && screenshotData.screenshot) { + await updateChatScreenshot({ + chatId: currentChatId, + screenshot: screenshotData.screenshot, + sandboxId: sandboxData.sandboxId, + sandboxUrl: sandboxData.url + }); + addChatMessage('Preview saved to chat.', 'system'); + } + } catch (e) { + addChatMessage('Failed to save preview.', 'system'); + } + }; + const applyCode = async () => { const code = promptInput.trim(); if (!code) { @@ -1669,6 +1663,16 @@ Tip: I automatically detect and install npm packages from your code imports (lik allow="clipboard-write" sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-modals" /> + {/* Save preview button */} + {currentChatId && ( + + )} {/* Refresh button */} + + + + +
MOST POPULAR
+ +
+ + Pro +
+
+ $20 + per month +
+ For builders who want more +
+ +
    +
  • Unlimited chats
  • +
  • Advanced models
  • +
+ +
+
- {/* Usage Limits Information */} @@ -81,64 +135,10 @@ export default function PricingPage() { -

- ZapDev uses a usage-based pricing model to ensure you only pay for what you use: -

-
-
-

What Counts as Usage

-
    -
  • • AI chat messages and code generations
  • -
  • • Sandbox creation and execution time
  • -
  • • Autonomous agent workflows
  • -
  • • File operations and storage
  • -
-
-
-

When You Hit Limits

-
    -
  • • You'll be automatically redirected here
  • -
  • • Upgrade instantly to continue working
  • -
  • • No interruption to your development flow
  • -
  • • All your work is safely preserved
  • -
-
-
-
-
- - {/* FAQ Section */} - - - Frequently Asked Questions - - -
-
-

What happens if I exceed my usage limits?

-

- When you reach your plan's usage limits, you'll be automatically redirected to this pricing page - where you can instantly upgrade to continue your work without losing any progress. -

-
-
-

Can I change plans anytime?

-

- Yes! You can upgrade or downgrade your plan at any time. Changes take effect immediately, - and billing is prorated automatically. -

-
-
-

Is my code and data secure?

-

- Absolutely. Each sandbox is isolated, your code is encrypted, and we follow industry-standard - security practices to keep your projects safe. -

-
-
+

ZapDev uses a usage-based model to ensure you only pay for what you use.

); -} \ No newline at end of file +} diff --git a/app/test-pricing/page.tsx b/app/test-pricing/page.tsx index 3c92103b..f4e9630e 100644 --- a/app/test-pricing/page.tsx +++ b/app/test-pricing/page.tsx @@ -20,58 +20,32 @@ import { CreditCard, TestTube, DollarSign } from "lucide-react"; const testPlans: PricingPlan[] = [ { - id: "test-free", - name: "Test Free Plan", - description: "Testing free tier functionality", + id: "free", + name: "Free", + description: "Testing free tier", price: 0, currency: "usd", interval: "month", features: [ - "Free tier testing", - "Basic Stripe integration", - "No payment required", + "5 chats", + "Basic models", ], - buttonText: "Test Free Plan", + buttonText: "Get Started", buttonVariant: "outline", }, { - id: "test-basic", - name: "Test Basic", - description: "Basic paid plan for testing", - price: 10, + id: "pro", + name: "Pro", + description: "Testing Pro plan", + price: 20, currency: "usd", interval: "month", - priceId: - process.env.NEXT_PUBLIC_STRIPE_TEST_BASIC_PRICE_ID || "price_test_basic", + priceId: process.env.NEXT_PUBLIC_STRIPE_PRO_PRICE_ID || "price_test_pro", features: [ - "Test basic payments", - "Stripe checkout flow", - "Subscription management", - "Test webhooks", + "Unlimited chats", + "Advanced models", ], - buttonText: "Test Basic Plan", - buttonVariant: "default", - }, - { - id: "test-premium", - name: "Test Premium", - description: "Premium plan for comprehensive testing", - price: 25, - currency: "usd", - interval: "month", - priceId: - process.env.NEXT_PUBLIC_STRIPE_TEST_PREMIUM_PRICE_ID || - "price_test_premium", - features: [ - "All basic features", - "Advanced payment testing", - "Multiple payment methods", - "Subscription updates", - "Proration testing", - ], - popular: true, - buttonText: "Test Premium Plan", - buttonVariant: "default", + buttonText: "Upgrade to Pro", }, ]; @@ -85,7 +59,6 @@ export default function TestPricingPage() { return (
- {/* Header */}
@@ -95,12 +68,10 @@ export default function TestPricingPage() {

Test the Stripe payment integration with various pricing scenarios - and configurations. This page demonstrates the full payment flow - including subscriptions, one-time payments, and custom amounts. + and configurations.

- {/* Test Plans */}

Test Subscription Plans @@ -110,9 +81,7 @@ export default function TestPricingPage() { - {/* Custom Testing Section */}
- {/* Custom Payment Test */} @@ -165,7 +134,7 @@ export default function TestPricingPage() { - Test Custom{" "} - {testMode === "subscription" ? "Subscription" : "Payment"} -{" "} - {customAmount} {customCurrency.toUpperCase()} + Test Custom {testMode === "subscription" ? "Subscription" : "Payment"} - {customAmount} {customCurrency.toUpperCase()} - {/* Quick Test Buttons */} @@ -196,7 +162,7 @@ export default function TestPricingPage() { - Test $9.99/month Subscription + Test $19.99/month Subscription Test $49.99 One-time Payment - - - Test Free Trial (Disabled) -
- {/* Test Information */} Testing Information @@ -268,19 +217,10 @@ export default function TestPricingPage() {

Test Credit Cards

    -
  • - 4242424242424242 - Visa (Success) -
  • -
  • - 4000000000000002 - Visa (Declined) -
  • -
  • - 4000000000009995 - Visa (Insufficient funds) -
  • -
  • - 4000002500003155 - Visa (Requires - authentication) -
  • +
  • 4242424242424242 - Visa (Success)
  • +
  • 4000000000000002 - Visa (Declined)
  • +
  • 4000000000009995 - Visa (Insufficient funds)
  • +
  • 4000002500003155 - Visa (Requires authentication)
@@ -293,13 +233,6 @@ export default function TestPricingPage() {

-
-

- Note: This page is for testing Stripe - integration. No real payments will be processed. Make sure your - environment variables are set correctly with Stripe test keys. -

-
diff --git a/components/ConvexChat.tsx b/components/ConvexChat.tsx index 2da3b2b1..1d25d020 100644 --- a/components/ConvexChat.tsx +++ b/components/ConvexChat.tsx @@ -208,10 +208,26 @@ export default function ConvexChat({ onChatSelect, onMessageAdd }: ConvexChatPro }; const handleUpgrade = async (plan: string) => { - // Integration with Stripe will be handled here - console.log('Upgrading to plan:', plan); - setShowPricingModal(false); - // TODO: Integrate with Stripe checkout + try { + const res = await fetch('/api/autumn/checkout', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ priceId: process.env.NEXT_PUBLIC_STRIPE_PRO_PRICE_ID }) + }); + const data = await res.json(); + setShowPricingModal(false); + if (data.url) { + window.location.href = data.url; + return; + } + if (data.provider === 'stripe' && data.sessionId) { + const { getStripe } = await import('@/lib/stripe-client'); + const stripe = await getStripe(); + if (stripe) await stripe.redirectToCheckout({ sessionId: data.sessionId }); + } + } catch (e) { + console.error('Upgrade failed', e); + } }; return ( diff --git a/components/EnhancedSettingsModal.tsx b/components/EnhancedSettingsModal.tsx index c6f1773b..98a147bb 100644 --- a/components/EnhancedSettingsModal.tsx +++ b/components/EnhancedSettingsModal.tsx @@ -55,47 +55,25 @@ export default function EnhancedSettingsModal({ isOpen, onClose }: EnhancedSetti }); const plans = [ + { + name: 'Free', + description: 'Perfect for getting started.', + monthlyPrice: 0, + annualPrice: 0, + features: [ + '5 chats', + 'Basic models' + ] + }, { name: 'Pro', - description: 'Designed for fast-moving teams building together in real time.', + description: 'Unlimited chats and advanced models.', monthlyPrice: 20, annualPrice: 20, popular: true, features: [ 'Unlimited chats', - 'Unlimited projects', - 'Private projects', - 'User roles & permissions', - 'Custom domains', - 'Priority support' - ] - }, - { - name: 'Business', - description: 'Advanced controls and power features for growing departments', - monthlyPrice: 50, - annualPrice: 40, - features: [ - 'All features in Pro, plus:', - '100 monthly credits', - 'SSO', - 'Personal Projects', - 'Opt out of data training', - 'Design templates', - 'Custom design systems' - ] - }, - { - name: 'Enterprise', - description: 'Built for large orgs needing flexibility, scale, and governance.', - isEnterprise: true, - features: [ - 'Everything in Business, plus:', - 'Dedicated support', - 'Onboarding services', - 'Custom integrations', - 'Group-based access control', - 'Custom design systems' + 'Advanced models' ] } ]; diff --git a/components/MasterDashboard.tsx b/components/MasterDashboard.tsx index 89568fd8..bb00ec7c 100644 --- a/components/MasterDashboard.tsx +++ b/components/MasterDashboard.tsx @@ -208,13 +208,13 @@ export default function MasterDashboard({ ? 'border-blue-600 text-blue-600 bg-blue-50' : 'border-transparent text-gray-600' )} - title={!isAvailable ? `${item.label} requires ${userSubscription === 'free' ? 'Pro' : 'Enterprise'} subscription` : item.description} + title={!isAvailable ? `${item.label} requires Pro subscription` : item.description} > {item.label} {!isAvailable && ( - {userSubscription === 'free' ? 'Pro' : 'Enterprise'} + {'Pro'} )} diff --git a/components/PricingModal.tsx b/components/PricingModal.tsx index 88e68547..a13c9cb0 100644 --- a/components/PricingModal.tsx +++ b/components/PricingModal.tsx @@ -1,6 +1,5 @@ 'use client'; -import { useState } from 'react'; import { Button } from '@/components/ui/button'; import { motion, AnimatePresence } from 'framer-motion'; @@ -11,59 +10,30 @@ interface PricingModalProps { } export default function PricingModal({ isOpen, onClose, onUpgrade }: PricingModalProps) { - const [selectedBilling, setSelectedBilling] = useState<'monthly' | 'annual'>('monthly'); + if (!isOpen) return null; const plans = [ { name: 'Free', - description: 'Perfect for trying out our AI-powered development platform.', - monthlyPrice: 0, - annualPrice: 0, + description: 'Perfect for getting started.', + price: 0, features: [ - '✅ 10 deployed sites', - '✅ Website analytics', - '✅ Unlimited codebase downloads', - '✅ AI database generation', - '✅ Drizzle Studio access', - '5 chats limit', - 'Basic AI models', - 'Community support' + '5 chats', + 'Basic models' ] }, { name: 'Pro', - description: 'Designed for fast-moving teams building together in real time.', - monthlyPrice: 20, - annualPrice: 16, + description: 'Unlimited chats and advanced models.', + price: 20, + popular: true, features: [ - '✅ Everything in Free, plus:', - '✅ Custom domains', 'Unlimited chats', - 'Advanced AI models', - 'Priority support', - 'Export conversations', - 'Custom integrations', - 'Team collaboration' - ] - }, - { - name: 'Enterprise', - description: 'Built for large orgs needing flexibility, scale, and governance.', - isEnterprise: true, - features: [ - 'Everything in Pro, plus:', - 'Dedicated support', - 'Custom deployment', - 'Advanced compliance', - 'SSO integration', - 'SLA guarantees', - 'Custom AI training' + 'Advanced models' ] } ]; - if (!isOpen) return null; - return ( e.stopPropagation()} > - {/* Header */} -
-
-
-

Upgrade Your Plan

-

You've reached your free plan limit of 5 chats

-
- -
- - {/* Billing Toggle */} -
- Monthly - - Annual - {selectedBilling === 'annual' && ( - Save 20% - )} +
+
+

Upgrade Your Plan

+

You've reached the free plan limit of 5 chats.

+
- {/* Pricing Cards */}
-
+
{plans.map((plan) => (

{plan.name}

{plan.description}

- - {plan.isEnterprise ? ( -
- Flexible billing -
- ) : ( -
- - ${selectedBilling === 'monthly' ? plan.monthlyPrice : plan.annualPrice} - - per month - {selectedBilling === 'annual' && ( -
- Billed annually -
- )} -
- )} - +
+ ${plan.price} + per month +
@@ -177,9 +100,7 @@ export default function PricingModal({ isOpen, onClose, onUpgrade }: PricingModa - - {feature} - + {feature}
))}
@@ -188,14 +109,11 @@ export default function PricingModal({ isOpen, onClose, onUpgrade }: PricingModa
- {/* Footer */}
-

- All plans include a 7-day free trial. Cancel anytime. -

+

Cancel anytime.

); -} \ No newline at end of file +} diff --git a/components/stripe/SubscriptionPlans.tsx b/components/stripe/SubscriptionPlans.tsx index 4af0244c..61f5749a 100644 --- a/components/stripe/SubscriptionPlans.tsx +++ b/components/stripe/SubscriptionPlans.tsx @@ -1,6 +1,6 @@ 'use client'; -import { Check, Zap, Crown, Rocket } from 'lucide-react'; +import { Check, Zap, Crown } from 'lucide-react'; import CheckoutButton from './CheckoutButton'; export interface PricingPlan { @@ -32,10 +32,9 @@ const defaultPlans: PricingPlan[] = [ currency: 'usd', interval: 'month', features: [ - '5 AI-powered projects', - 'Basic templates', - 'Community support', - 'Standard sandbox time', + '5 chats', + '1 sandbox', + 'Basic models' ], icon: Zap, buttonText: 'Get Started', @@ -48,41 +47,15 @@ const defaultPlans: PricingPlan[] = [ price: 20, currency: 'usd', interval: 'month', - priceId: process.env.NEXT_PUBLIC_STRIPE_PRO_PRICE_ID || 'price_pro_monthly', // Use env var for Stripe price ID + priceId: process.env.NEXT_PUBLIC_STRIPE_PRO_PRICE_ID || 'price_pro_monthly', features: [ 'Unlimited chats', - 'Unlimited AI-powered projects', - 'Premium templates', - 'Priority support', - 'Extended sandbox time', - 'Advanced AI models', - 'Export to GitHub', + 'Advanced models' ], popular: true, icon: Crown, buttonText: 'Upgrade to Pro', - }, - { - id: 'team', - name: 'Team', - description: 'For teams and organizations', - price: 50, - currency: 'usd', - interval: 'month', - priceId: 'price_team_monthly', // Replace with actual Stripe price ID - features: [ - 'Everything in Pro', - 'Team collaboration', - 'Shared projects', - 'Advanced analytics', - 'Custom integrations', - 'Dedicated support', - 'SSO authentication', - ], - icon: Rocket, - buttonText: 'Upgrade to Team', - buttonVariant: 'orange', - }, + } ]; export default function SubscriptionPlans({ @@ -98,7 +71,7 @@ export default function SubscriptionPlans({ }; return ( -
+
{plans.map((plan) => { const Icon = plan.icon || Zap; @@ -177,7 +150,7 @@ export default function SubscriptionPlans({ ) : ( ); -} \ No newline at end of file +} diff --git a/lib/groq-sequential-thinking.ts b/lib/groq-sequential-thinking.ts index e929c94e..09993731 100644 --- a/lib/groq-sequential-thinking.ts +++ b/lib/groq-sequential-thinking.ts @@ -1,14 +1,17 @@ /** - * Groq Sequential Thinking Engine - * - * Implements advanced sequential thinking capabilities specifically for Groq models, - * leveraging Groq's reasoning API and MCP integration for enhanced reasoning experiences. + * Sequential Thinking Engine (Kimi K2 default) + * + * Replaces Groq-specific implementation with a provider-agnostic approach + * defaulting to moonshotai/kimi-k2-instruct-0905 from app.config. */ -import { createGroq } from '@ai-sdk/groq'; import { streamText } from 'ai'; +import { createAnthropic } from '@ai-sdk/anthropic'; +import { createOpenAI } from '@ai-sdk/openai'; +import { createGoogleGenerativeAI } from '@ai-sdk/google'; import { getSequentialThinkingService } from '@/lib/mcp/sequential-thinking-service'; import { getReasoningSearchService } from '@/lib/search/reasoning-search'; +import { appConfig } from '@/config/app.config'; export interface ThoughtStep { stepNumber: number; @@ -62,22 +65,26 @@ export interface ReasoningProgress { message?: string; } +function getProvider(modelId: string) { + const isAnthropic = modelId.startsWith('anthropic/'); + const isGoogle = modelId.startsWith('google/'); + const openai = createOpenAI({ apiKey: process.env.OPENAI_API_KEY }); + const anthropic = createAnthropic({ apiKey: process.env.ANTHROPIC_API_KEY, baseURL: process.env.ANTHROPIC_BASE_URL || 'https://api.anthropic.com/v1' }); + const google = createGoogleGenerativeAI({ apiKey: process.env.GEMINI_API_KEY }); + const actual = isAnthropic ? modelId.replace('anthropic/', '') : (isGoogle ? modelId.replace('google/', '') : modelId); + const provider = isAnthropic ? anthropic : (isGoogle ? google : openai); + return { provider, actualModel: actual }; +} + export class GroqSequentialThinking { - private groqClient; private mcpService; private searchService; constructor() { - this.groqClient = createGroq({ - apiKey: process.env.GROQ_API_KEY, - }); this.mcpService = getSequentialThinkingService(); this.searchService = getReasoningSearchService(); } - /** - * Process a query using sequential thinking with UltraThink mode - */ async processUltraThink( request: UltraThinkRequest, progressCallback?: (progress: ReasoningProgress) => void @@ -85,62 +92,42 @@ export class GroqSequentialThinking { const startTime = Date.now(); const reasoningPaths: ReasoningPath[] = []; const searchSources: any[] = []; - + try { - // Initialize MCP Sequential Thinking await this.mcpService.initialize(); - - // Start primary reasoning path + const primaryPath = await this.createReasoningPath(request, progressCallback); reasoningPaths.push(primaryPath); - - // Enable branching for complex problems + if (request.enableBranching && this.shouldCreateBranches(request.query)) { const branchPaths = await this.createAlternativePaths(request, primaryPath, progressCallback); reasoningPaths.push(...branchPaths); } - - // Select best path and generate final answer + const bestPath = this.selectBestReasoningPath(reasoningPaths); const finalAnswer = await this.generateFinalAnswer(request, bestPath, progressCallback); - - // Collect all search sources used + reasoningPaths.forEach(path => { path.steps.forEach(step => { - if (step.searchResults) { - searchSources.push(...step.searchResults); - } + if (step.searchResults) searchSources.push(...step.searchResults); }); }); - - const processingTime = Date.now() - startTime; - const thoughtCount = reasoningPaths.reduce((total, path) => total + path.steps.length, 0); - - progressCallback?.({ - type: 'reasoning_complete', - progress: 100, - message: `Completed reasoning with ${thoughtCount} thoughts across ${reasoningPaths.length} path(s)` - }); - + return { reasoningPaths, finalAnswer, confidence: bestPath.confidence, searchSources: this.deduplicateSearchSources(searchSources), - processingTime, - thoughtCount, + processingTime: Date.now() - startTime, + thoughtCount: reasoningPaths.reduce((t, p) => t + p.steps.length, 0), ultraThinkMode: true }; - } catch (error) { console.error('UltraThink processing error:', error); throw new Error(`Sequential thinking failed: ${error instanceof Error ? error.message : 'Unknown error'}`); } } - /** - * Create a reasoning path using MCP Sequential Thinking - */ private async createReasoningPath( request: UltraThinkRequest, progressCallback?: (progress: ReasoningProgress) => void, @@ -151,7 +138,7 @@ export class GroqSequentialThinking { let currentStep = 1; let totalEstimatedSteps = request.maxThoughts || 5; let nextThoughtNeeded = true; - + const path: ReasoningPath = { pathId, steps, @@ -161,23 +148,17 @@ export class GroqSequentialThinking { isComplete: false, branchedFrom }; - - progressCallback?.({ - type: 'thought_start', - pathId, - progress: 0, - message: `Starting reasoning path${branchedFrom ? ' (alternative approach)' : ''}` - }); - + + progressCallback?.({ type: 'thought_start', pathId, progress: 0, message: `Starting reasoning path${branchedFrom ? ' (alternative approach)' : ''}` }); + while (nextThoughtNeeded && currentStep <= totalEstimatedSteps) { - // Generate thought using Groq with reasoning format const thoughtResponse = await this.generateThought( request, steps, currentStep, totalEstimatedSteps ); - + const thought: ThoughtStep = { stepNumber: currentStep, thought: thoughtResponse.thought, @@ -188,21 +169,13 @@ export class GroqSequentialThinking { searchResults: [], alternatives: [] }; - - progressCallback?.({ - type: 'thought_complete', - step: thought, - pathId, - progress: (currentStep / totalEstimatedSteps) * 100, - message: `Completed thought ${currentStep}/${totalEstimatedSteps}` - }); - - // Enhance thought with search if enabled + + progressCallback?.({ type: 'thought_complete', step: thought, pathId, progress: (currentStep / totalEstimatedSteps) * 100, message: `Completed thought ${currentStep}/${totalEstimatedSteps}` }); + if (request.enableSearch && this.shouldSearch(thought.thought)) { await this.enhanceThoughtWithSearch(thought, request.query, progressCallback); } - - // Check if we need more thoughts using MCP + const mcpResponse = await this.mcpService.processThought({ thought: thought.thought, thoughtNumber: currentStep, @@ -213,46 +186,38 @@ export class GroqSequentialThinking { searchEnabled: request.enableSearch } }); - - // Update thought with MCP insights + if (mcpResponse.alternatives) { thought.alternatives = mcpResponse.alternatives; } - + steps.push(thought); - - // Update path progress + nextThoughtNeeded = mcpResponse.nextThoughtNeeded; totalEstimatedSteps = mcpResponse.adjustedTotalThoughts || totalEstimatedSteps; currentStep++; - - // Update path confidence (running average) + path.confidence = steps.reduce((avg, step) => avg + step.confidence, 0) / steps.length; } - + path.currentStep = currentStep - 1; path.totalEstimatedSteps = totalEstimatedSteps; path.isComplete = !nextThoughtNeeded || currentStep > totalEstimatedSteps; - + return path; } - - /** - * Generate individual thought using Groq's reasoning capabilities - */ + private async generateThought( request: UltraThinkRequest, previousSteps: ThoughtStep[], stepNumber: number, totalSteps: number ): Promise<{thought: string, confidence: number, reasoning: string}> { - const model = request.model || 'llama-3.3-70b-versatile'; - - // Build context from previous thoughts - const previousThoughts = previousSteps.map(step => - `Step ${step.stepNumber}: ${step.thought}` - ).join('\n'); - + const modelId = request.model || appConfig.ai.defaultModel; + const { provider, actualModel } = getProvider(modelId); + + const previousThoughts = previousSteps.map(step => `Step ${step.stepNumber}: ${step.thought}`).join('\n'); + const systemPrompt = `You are an advanced reasoning system performing sequential thinking. This is step ${stepNumber} of approximately ${totalSteps} steps. Your task is to think deeply about the user's query and provide ONE specific thought or analysis step that builds on previous thinking. @@ -278,206 +243,120 @@ THOUGHT: [Your specific thought or analysis for this step] CONFIDENCE: [0-100 score for this thought]`; const result = await streamText({ - model: this.groqClient(model), + model: provider(actualModel), messages: [ { role: 'system', content: systemPrompt }, { role: 'user', content: `Please provide step ${stepNumber} of your reasoning about: "${request.query}"` } ], temperature: 0.7, - maxTokens: 1000, - // Use Groq's reasoning format to get tags - experimental_providerMetadata: { - groq: { - reasoning_format: 'raw' - } - } + maxTokens: 1000 }); - - // Extract the full response + let fullResponse = ''; for await (const textPart of result.textStream) { fullResponse += textPart; } - - // Parse the response to extract thought, reasoning, and confidence + const thinkMatch = fullResponse.match(/([\s\S]*?)<\/think>/); const thoughtMatch = fullResponse.match(/THOUGHT:\s*([\s\S]*?)(?=CONFIDENCE:|$)/); const confidenceMatch = fullResponse.match(/CONFIDENCE:\s*(\d+)/); - + return { thought: thoughtMatch?.[1]?.trim() || fullResponse.split('THOUGHT:')[1]?.split('CONFIDENCE:')[0]?.trim() || fullResponse.trim(), reasoning: thinkMatch?.[1]?.trim() || 'No explicit reasoning provided', confidence: confidenceMatch ? parseInt(confidenceMatch[1]) : 75 }; } - - /** - * Enhance thought with search results - */ + private async enhanceThoughtWithSearch( thought: ThoughtStep, originalQuery: string, progressCallback?: (progress: ReasoningProgress) => void ): Promise { - // Generate search queries based on the thought const searchQueries = this.generateSearchQueries(thought.thought, originalQuery); thought.searchQueries = searchQueries; - + for (const query of searchQueries) { - progressCallback?.({ - type: 'search_query', - searchQuery: query, - message: `Searching for: ${query}` - }); - + progressCallback?.({ type: 'search_query', searchQuery: query, message: `Searching for: ${query}` }); try { - const searchResults = await this.searchService.searchForReasoning(query, { - maxResults: 3, - relevanceThreshold: 0.7 - }); - + const searchResults = await this.searchService.searchForReasoning(query, { maxResults: 3, relevanceThreshold: 0.7 }); if (searchResults && searchResults.length > 0) { thought.searchResults?.push(...searchResults); - progressCallback?.({ - type: 'search_results', - searchResults: searchResults, - message: `Found ${searchResults.length} relevant sources` - }); + progressCallback?.({ type: 'search_results', searchResults, message: `Found ${searchResults.length} relevant sources` }); } } catch (error) { console.warn(`Search failed for query "${query}":`, error); } } } - - /** - * Generate search queries based on thought content - */ + private generateSearchQueries(thought: string, originalQuery: string): string[] { const queries: string[] = []; - - // Extract key terms and concepts from the thought const words = thought.toLowerCase().split(/\W+/).filter(word => word.length > 3); - const importantWords = words.filter(word => - !['that', 'this', 'with', 'from', 'they', 'have', 'been', 'will', 'would', 'could', 'should'].includes(word) - ); - - // Create focused search queries + const importantWords = words.filter(word => !['that', 'this', 'with', 'from', 'they', 'have', 'been', 'will', 'would', 'could', 'should'].includes(word)); + if (importantWords.length >= 2) { queries.push(`${originalQuery} ${importantWords.slice(0, 3).join(' ')}`); } - - // Look for specific concepts or technical terms - const technicalTerms = words.filter(word => - word.length > 5 && ( - word.includes('tion') || - word.includes('ment') || - word.includes('ness') || - word.includes('able') || - word.match(/^[A-Z][a-z]+[A-Z]/) - ) - ); - + + const technicalTerms = words.filter(word => word.length > 5 && (word.includes('tion') || word.includes('ment') || word.includes('ness') || word.includes('able') || word.match(/^[A-Z][a-z]+[A-Z]/))); if (technicalTerms.length > 0) { queries.push(`${technicalTerms[0]} ${originalQuery.split(' ').slice(0, 2).join(' ')}`); } - - return queries.slice(0, 2); // Limit to 2 search queries per thought + + return queries.slice(0, 2); } - - /** - * Determine if we should search for this thought - */ + private shouldSearch(thought: string): boolean { - const searchIndicators = [ - 'how', 'what', 'why', 'when', 'where', - 'example', 'best practice', 'documentation', - 'research', 'study', 'evidence', 'data', - 'recent', 'current', 'latest', 'new' - ]; - + const searchIndicators = ['how', 'what', 'why', 'when', 'where','example', 'best practice', 'documentation','research', 'study', 'evidence', 'data','recent', 'current', 'latest', 'new']; const lowerThought = thought.toLowerCase(); return searchIndicators.some(indicator => lowerThought.includes(indicator)); } - - /** - * Determine if we should create alternative reasoning branches - */ + private shouldCreateBranches(query: string): boolean { - const branchIndicators = [ - 'complex', 'multiple', 'various', 'different', - 'alternatives', 'options', 'approaches', 'strategies', - 'compare', 'versus', 'vs', 'or' - ]; - + const branchIndicators = ['complex', 'multiple', 'various', 'different','alternatives', 'options', 'approaches', 'strategies','compare', 'versus', 'vs', 'or']; const lowerQuery = query.toLowerCase(); - return branchIndicators.some(indicator => lowerQuery.includes(indicator)) || - query.split(' ').length > 10; // Complex queries likely benefit from branching + return branchIndicators.some(indicator => lowerQuery.includes(indicator)) || query.split(' ').length > 10; } - - /** - * Create alternative reasoning paths - */ + private async createAlternativePaths( request: UltraThinkRequest, primaryPath: ReasoningPath, progressCallback?: (progress: ReasoningProgress) => void ): Promise { const alternatives: ReasoningPath[] = []; - - // Create one alternative approach - progressCallback?.({ - type: 'branch_created', - message: 'Creating alternative reasoning approach...' - }); - - const alternativeRequest = { - ...request, - maxThoughts: Math.min(request.maxThoughts || 5, 4), // Shorter for alternatives - }; - + progressCallback?.({ type: 'branch_created', message: 'Creating alternative reasoning approach...' }); + + const alternativeRequest = { ...request, maxThoughts: Math.min(request.maxThoughts || 5, 4) }; + try { - const altPath = await this.createReasoningPath( - alternativeRequest, - progressCallback, - primaryPath.pathId - ); + const altPath = await this.createReasoningPath(alternativeRequest, progressCallback, primaryPath.pathId); alternatives.push(altPath); } catch (error) { console.warn('Failed to create alternative reasoning path:', error); } - + return alternatives; } - - /** - * Select the best reasoning path based on confidence and completeness - */ + private selectBestReasoningPath(paths: ReasoningPath[]): ReasoningPath { return paths.reduce((best, current) => { const currentScore = current.confidence * (current.isComplete ? 1.2 : 1.0); const bestScore = best.confidence * (best.isComplete ? 1.2 : 1.0); - return currentScore > bestScore ? current : best; }); } - - /** - * Generate final answer based on the best reasoning path - */ + private async generateFinalAnswer( request: UltraThinkRequest, bestPath: ReasoningPath, progressCallback?: (progress: ReasoningProgress) => void ): Promise { - progressCallback?.({ - type: 'thought_start', - message: 'Generating final answer...' - }); - - const model = request.model || 'llama-3.3-70b-versatile'; - - // Compile all thoughts and search results + progressCallback?.({ type: 'thought_start', message: 'Generating final answer...' }); + + const modelId = request.model || appConfig.ai.defaultModel; + const { provider, actualModel } = getProvider(modelId); + const reasoningContext = bestPath.steps.map(step => { let stepContent = `Step ${step.stepNumber}: ${step.thought}`; if (step.searchResults && step.searchResults.length > 0) { @@ -485,7 +364,7 @@ CONFIDENCE: [0-100 score for this thought]`; } return stepContent; }).join('\n\n'); - + const systemPrompt = `You are providing a final, comprehensive answer based on sequential reasoning analysis. REASONING PROCESS: @@ -503,7 +382,7 @@ Query: "${request.query}" Provide your final answer based on the reasoning analysis above.`; const result = await streamText({ - model: this.groqClient(model), + model: provider(actualModel), messages: [ { role: 'system', content: systemPrompt }, { role: 'user', content: 'Please provide your final answer based on the reasoning analysis.' } @@ -511,18 +390,15 @@ Provide your final answer based on the reasoning analysis above.`; temperature: 0.6, maxTokens: 2000 }); - + let finalAnswer = ''; for await (const textPart of result.textStream) { finalAnswer += textPart; } - + return finalAnswer; } - - /** - * Remove duplicate search sources - */ + private deduplicateSearchSources(sources: any[]): any[] { const seen = new Set(); return sources.filter(source => { @@ -534,7 +410,6 @@ Provide your final answer based on the reasoning analysis above.`; } } -// Export singleton instance let groqSequentialThinkingInstance: GroqSequentialThinking | null = null; export function getGroqSequentialThinking(): GroqSequentialThinking { @@ -544,13 +419,10 @@ export function getGroqSequentialThinking(): GroqSequentialThinking { return groqSequentialThinkingInstance; } -/** - * Convenience function for UltraThink processing - */ export async function processUltraThink( request: UltraThinkRequest, progressCallback?: (progress: ReasoningProgress) => void ): Promise { const engine = getGroqSequentialThinking(); return engine.processUltraThink(request, progressCallback); -} \ No newline at end of file +} From 922f86cfaa4fd2f4452b43794fe683534c1d7d13 Mon Sep 17 00:00:00 2001 From: otdoges Date: Mon, 8 Sep 2025 00:32:16 +0000 Subject: [PATCH 2/2] Capy jam: Remove Enterprise references from UI; add Save preview action; refine pricing test plans --- .capy_commit_msg | 1 + components/MasterDashboard.tsx | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) create mode 100644 .capy_commit_msg diff --git a/.capy_commit_msg b/.capy_commit_msg new file mode 100644 index 00000000..a0a980c7 --- /dev/null +++ b/.capy_commit_msg @@ -0,0 +1 @@ +Capy jam: Unify pricing to Free and Pro (0/mo); wire Autumn checkout/portal; consolidate Autumn wrapper; reduce Convex writes on home; default sequential thinking to Kimi K2; update env example diff --git a/components/MasterDashboard.tsx b/components/MasterDashboard.tsx index bb00ec7c..47641899 100644 --- a/components/MasterDashboard.tsx +++ b/components/MasterDashboard.tsx @@ -401,7 +401,7 @@ export default function MasterDashboard({ > - +
@@ -409,7 +409,7 @@ export default function MasterDashboard({
)} - {currentView === 'monitoring' && userSubscription === 'enterprise' && ( + {currentView === 'monitoring' && userSubscription !== 'free' && (

@@ -428,10 +428,10 @@ export default function MasterDashboard({

- {userSubscription === 'free' ? 'Pro Subscription Required' : 'Enterprise Subscription Required'} + {'Pro Subscription Required'}

- This feature requires a {userSubscription === 'free' ? 'Pro' : 'Enterprise'} subscription to access. + This feature requires a Pro subscription to access.