fixed bugs

This commit is contained in:
belviskhoremk
2026-02-22 23:25:10 +00:00
parent 53279e8fe1
commit f5d1bfb49d
10 changed files with 1073 additions and 834 deletions

View File

@@ -1,4 +1,4 @@
import React, { useState } from 'react'
import React, { useState, useCallback } from 'react'
import { Link, useNavigate } from 'react-router-dom'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { chatbotsAPI } from '@/services/api'
@@ -11,12 +11,31 @@ import {
Settings, Upload, Eye, ExternalLink, Download, BarChart2
} from 'lucide-react'
// BUG-05 FIX: Toast queue system using array + auto-dismiss
interface ToastItem {
id: string
message: string
}
export const DashboardPage: React.FC = () => {
const { user } = useAuthStore()
const navigate = useNavigate()
const queryClient = useQueryClient()
const [deleteId, setDeleteId] = useState<string | null>(null)
const [toast, setToast] = useState<string>('')
const [toasts, setToasts] = useState<ToastItem[]>([])
// BUG-05 FIX: Queue-based toast - no overwrites
const showToast = useCallback((message: string) => {
const id = crypto.randomUUID()
setToasts(prev => [...prev, { id, message }])
setTimeout(() => {
setToasts(prev => prev.filter(t => t.id !== id))
}, 3000)
}, [])
const removeToast = useCallback((id: string) => {
setToasts(prev => prev.filter(t => t.id !== id))
}, [])
const { data: chatbots = [], isLoading } = useQuery({
queryKey: ['chatbots'],
@@ -28,7 +47,7 @@ export const DashboardPage: React.FC = () => {
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['chatbots'] })
setDeleteId(null)
setToast('Chatbot deleted')
showToast('Chatbot deleted')
},
})
@@ -36,148 +55,152 @@ export const DashboardPage: React.FC = () => {
mutationFn: chatbotsAPI.publish,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['chatbots'] })
setToast('Chatbot published to marketplace!')
showToast('Chatbot published to marketplace!')
},
onError: (err: any) => setToast(err.response?.data?.detail || 'Failed to publish'),
})
const unpublishMutation = useMutation({
mutationFn: chatbotsAPI.unpublish,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['chatbots'] })
setToast('Chatbot unpublished')
showToast('Chatbot unpublished')
},
})
// IMP-11: Confirmation before publish/unpublish
const [confirmAction, setConfirmAction] = useState<{ type: 'publish' | 'unpublish'; id: string } | null>(null)
const handleConfirmAction = () => {
if (!confirmAction) return
if (confirmAction.type === 'publish') {
publishMutation.mutate(confirmAction.id)
} else {
unpublishMutation.mutate(confirmAction.id)
}
setConfirmAction(null)
}
return (
<div className="p-6 max-w-6xl mx-auto">
{/* Header */}
<div className="flex items-center justify-between mb-8">
<div>
<h1 className="text-2xl font-bold text-gray-900">
Good {getGreeting()}, {user?.company_name || 'there'} 👋
</h1>
<p className="text-gray-500 text-sm mt-1">Manage your AI chatbots</p>
</div>
<Button onClick={() => navigate('/chatbots/new')} size="lg">
<Plus className="w-4 h-4" />
New Chatbot
</Button>
</div>
{/* Stats */}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
{[
{ label: 'Total Chatbots', value: chatbots.length, icon: '🤖' },
{ label: 'Published', value: chatbots.filter(c => c.is_published).length, icon: '🌐' },
{ label: 'Documents', value: chatbots.reduce((sum, c) => sum + c.document_count, 0), icon: '📄' },
{ label: 'Conversations', value: chatbots.reduce((sum, c) => sum + c.conversation_count, 0), icon: '💬' },
].map(({ label, value, icon }) => (
<Card key={label} className="p-4">
<div className="flex items-center gap-3">
<span className="text-2xl">{icon}</span>
<div>
<p className="text-2xl font-bold text-gray-900">{value}</p>
<p className="text-xs text-gray-500">{label}</p>
</div>
</div>
</Card>
))}
</div>
{/* Plan notice */}
{user?.plan === 'free' && (
<div className="mb-6 p-4 bg-gradient-to-r from-primary-50 to-purple-50 border border-primary-200 rounded-xl flex items-center justify-between">
<div className="p-4 sm:p-6">
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between mb-6 gap-3">
<div>
<p className="text-sm font-semibold text-primary-900">You're on the Free plan</p>
<p className="text-xs text-primary-700 mt-0.5">
Upgrade to publish chatbots to the marketplace and unlock premium AI models
</p>
<h1 className="text-xl sm:text-2xl font-bold text-gray-900">Dashboard</h1>
<p className="text-sm text-gray-500 mt-0.5">Manage your AI chatbots</p>
</div>
<Link to="/pricing">
<Button size="sm">Upgrade</Button>
</Link>
<Button onClick={() => navigate('/chatbots/new')}>
<Plus className="w-4 h-4" />
New Chatbot
</Button>
</div>
)}
{/* Chatbots Grid */}
{isLoading ? (
<div className="flex items-center justify-center py-20">
<Spinner className="text-primary-600" />
</div>
) : chatbots.length === 0 ? (
<EmptyState
icon={<Bot className="w-8 h-8" />}
title="No chatbots yet"
description="Create your first AI chatbot powered by your documents. It's free to build and test."
action={
<Button onClick={() => navigate('/chatbots/new')} size="lg">
<Plus className="w-4 h-4" />
Create your first chatbot
</Button>
}
/>
) : (
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-4">
{chatbots.map((chatbot) => (
<ChatbotCard
key={chatbot.id}
chatbot={chatbot}
onEdit={() => navigate(`/chatbots/${chatbot.id}/edit`)}
onPreview={() => navigate(`/chatbots/${chatbot.id}/preview`)}
onPublish={() => publishMutation.mutate(chatbot.id)}
onUnpublish={() => unpublishMutation.mutate(chatbot.id)}
onDelete={() => setDeleteId(chatbot.id)}
onAnalytics={() => navigate(`/chatbots/${chatbot.id}/analytics`)}
/>
))}
{/* Add new card */}
<Card
className="border-dashed hover:border-primary-400 hover:bg-primary-50 flex items-center justify-center min-h-[200px] cursor-pointer transition-colors"
onClick={() => navigate('/chatbots/new')}
>
<div className="text-center">
<div className="w-12 h-12 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-3">
<Plus className="w-6 h-6 text-gray-400" />
</div>
<p className="text-sm font-medium text-gray-500">New Chatbot</p>
{isLoading ? (
<div className="flex items-center justify-center py-20">
<Spinner className="text-primary-600" />
</div>
</Card>
</div>
)}
) : chatbots.length === 0 ? (
<EmptyState
icon={<Bot className="w-8 h-8" />}
title="No chatbots yet"
description="Create your first AI chatbot powered by your documents. It's free to build and test."
action={
<Button onClick={() => navigate('/chatbots/new')} size="lg">
<Plus className="w-4 h-4" />
Create your first chatbot
</Button>
}
/>
) : (
// R-02 FIX: Better responsive grid breakpoints
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{chatbots.map((chatbot) => (
<ChatbotCard
key={chatbot.id}
chatbot={chatbot}
onEdit={() => navigate(`/chatbots/${chatbot.id}/edit`)}
onPreview={() => navigate(`/chatbots/${chatbot.id}/preview`)}
onPublish={() => setConfirmAction({ type: 'publish', id: chatbot.id })}
onUnpublish={() => setConfirmAction({ type: 'unpublish', id: chatbot.id })}
onDelete={() => setDeleteId(chatbot.id)}
onAnalytics={() => navigate(`/chatbots/${chatbot.id}/analytics`)}
/>
))}
{/* Add new card */}
<Card
className="border-dashed hover:border-primary-400 hover:bg-primary-50 flex items-center justify-center min-h-[200px] cursor-pointer transition-colors"
onClick={() => navigate('/chatbots/new')}
>
<div className="text-center">
<div className="w-12 h-12 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-3">
<Plus className="w-6 h-6 text-gray-400" />
</div>
<p className="text-sm font-medium text-gray-500">New Chatbot</p>
</div>
</Card>
</div>
)}
{/* Delete confirmation */}
<Modal
isOpen={!!deleteId}
onClose={() => setDeleteId(null)}
title="Delete Chatbot"
>
<p className="text-gray-600 mb-6">
Are you sure you want to delete this chatbot? All documents and conversation history will be permanently removed.
</p>
<div className="flex gap-3">
<Button variant="outline" onClick={() => setDeleteId(null)} className="flex-1">
Cancel
</Button>
<Button
variant="danger"
onClick={() => deleteId && deleteMutation.mutate(deleteId)}
loading={deleteMutation.isPending}
className="flex-1"
>
Delete
</Button>
</div>
</Modal>
{/* Delete confirmation */}
<Modal
isOpen={!!deleteId}
onClose={() => setDeleteId(null)}
title="Delete Chatbot"
>
<p className="text-gray-600 mb-6">
Are you sure you want to delete this chatbot? All documents and conversation history will be permanently removed.
</p>
<div className="flex gap-3">
<Button variant="outline" onClick={() => setDeleteId(null)} className="flex-1">
Cancel
</Button>
<Button
variant="danger"
onClick={() => deleteId && deleteMutation.mutate(deleteId)}
loading={deleteMutation.isPending}
className="flex-1"
>
Delete
</Button>
</div>
</Modal>
{/* Toast */}
{toast && (
<div className="fixed bottom-4 right-4 bg-gray-900 text-white px-4 py-2 rounded-lg text-sm shadow-lg z-50">
{toast}
<button onClick={() => setToast('')} className="ml-3 opacity-60 hover:opacity-100">×</button>
{/* IMP-11: Publish/Unpublish confirmation */}
<Modal
isOpen={!!confirmAction}
onClose={() => setConfirmAction(null)}
title={confirmAction?.type === 'publish' ? 'Publish Chatbot' : 'Unpublish Chatbot'}
>
<p className="text-gray-600 mb-6">
{confirmAction?.type === 'publish'
? 'This will make your chatbot publicly visible on the marketplace. Are you sure?'
: 'This will remove your chatbot from the marketplace. Users will no longer be able to access it.'}
</p>
<div className="flex gap-3">
<Button variant="outline" onClick={() => setConfirmAction(null)} className="flex-1">
Cancel
</Button>
<Button
onClick={handleConfirmAction}
loading={publishMutation.isPending || unpublishMutation.isPending}
className="flex-1"
>
{confirmAction?.type === 'publish' ? 'Publish' : 'Unpublish'}
</Button>
</div>
</Modal>
{/* BUG-05 FIX: Toast queue - renders all active toasts */}
<div className="fixed bottom-4 right-4 z-50 space-y-2">
{toasts.map((toast) => (
<div
key={toast.id}
className="bg-gray-900 text-white px-4 py-2 rounded-lg text-sm shadow-lg animate-fade-in-up flex items-center gap-2"
>
{toast.message}
<button onClick={() => removeToast(toast.id)} className="opacity-60 hover:opacity-100">&times;</button>
</div>
))}
</div>
)}
</div>
</div>
)
}
@@ -194,106 +217,100 @@ const ChatbotCard: React.FC<{
const [menuOpen, setMenuOpen] = useState(false)
return (
<Card className="p-5">
<div className="flex items-start justify-between mb-3">
<div className="flex items-center gap-3">
<div
className="w-10 h-10 rounded-xl flex items-center justify-center text-white text-lg"
style={{ background: chatbot.primary_color }}
>
<Bot className="w-5 h-5" />
</div>
<div>
<h3 className="font-semibold text-gray-900 text-sm">{chatbot.name}</h3>
<div className="flex items-center gap-1.5 mt-0.5">
<StatusDot status={chatbot.is_published ? 'published' : 'preview'} />
<span className="text-xs text-gray-500">
<Card className="p-5">
<div className="flex items-start justify-between mb-3">
<div className="flex items-center gap-3">
<div
className="w-10 h-10 rounded-xl flex items-center justify-center text-white text-lg"
style={{ background: chatbot.primary_color }}
>
<Bot className="w-5 h-5" />
</div>
<div>
<h3 className="font-semibold text-gray-900 text-sm">{chatbot.name}</h3>
<div className="flex items-center gap-1.5 mt-0.5">
<StatusDot status={chatbot.is_published ? 'published' : 'preview'} />
<span className="text-xs text-gray-500">
{chatbot.is_published ? 'Published' : 'Preview'}
</span>
</div>
</div>
</div>
{/* R-02 FIX: Menu dropdown with viewport-aware positioning */}
<div className="relative">
<button
onClick={() => setMenuOpen(!menuOpen)}
className="p-1.5 hover:bg-gray-100 rounded-lg text-gray-400"
>
<MoreHorizontal className="w-4 h-4" />
</button>
{menuOpen && (
<>
<div className="fixed inset-0 z-10" onClick={() => setMenuOpen(false)} />
<div className="absolute right-0 mt-1 w-44 bg-white border border-gray-200 rounded-xl shadow-lg z-20 overflow-hidden text-sm max-h-64 overflow-y-auto">
<button onClick={() => { onEdit(); setMenuOpen(false) }} className="flex items-center gap-2 w-full px-3 py-2 hover:bg-gray-50 text-left">
<Settings className="w-3.5 h-3.5" /> Edit Settings
</button>
<button onClick={() => { onPreview(); setMenuOpen(false) }} className="flex items-center gap-2 w-full px-3 py-2 hover:bg-gray-50 text-left">
<Eye className="w-3.5 h-3.5" /> Preview
</button>
<button onClick={() => { onAnalytics(); setMenuOpen(false) }} className="flex items-center gap-2 w-full px-3 py-2 hover:bg-gray-50 text-left">
<BarChart2 className="w-3.5 h-3.5" /> Analytics
</button>
<div className="h-px bg-gray-100" />
{chatbot.is_published ? (
<button onClick={() => { onUnpublish(); setMenuOpen(false) }} className="flex items-center gap-2 w-full px-3 py-2 hover:bg-orange-50 text-orange-600 text-left">
<Lock className="w-3.5 h-3.5" /> Unpublish
</button>
) : (
<button onClick={() => { onPublish(); setMenuOpen(false) }} className="flex items-center gap-2 w-full px-3 py-2 hover:bg-green-50 text-green-600 text-left">
<Globe className="w-3.5 h-3.5" /> Publish
</button>
)}
<div className="h-px bg-gray-100" />
<button onClick={() => { onDelete(); setMenuOpen(false) }} className="flex items-center gap-2 w-full px-3 py-2 hover:bg-red-50 text-red-600 text-left">
<Trash2 className="w-3.5 h-3.5" /> Delete
</button>
</div>
</>
)}
</div>
</div>
<div className="relative">
<button
onClick={() => setMenuOpen(!menuOpen)}
className="p-1.5 hover:bg-gray-100 rounded-lg text-gray-400"
{chatbot.description && (
<p className="text-xs text-gray-500 mb-3 line-clamp-2">{chatbot.description}</p>
)}
{/* Stats */}
<div className="flex flex-wrap gap-3 mb-4 text-xs text-gray-500">
<span>📄 {chatbot.document_count} docs</span>
<span>💬 {chatbot.conversation_count} chats</span>
{chatbot.category && <span>🏷 {chatbot.category}</span>}
</div>
{/* Actions */}
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={onPreview}
className="flex-1"
>
<MoreHorizontal className="w-4 h-4" />
</button>
{menuOpen && (
<>
<div className="fixed inset-0 z-10" onClick={() => setMenuOpen(false)} />
<div className="absolute right-0 mt-1 w-44 bg-white border border-gray-200 rounded-xl shadow-lg z-20 overflow-hidden text-sm">
<button onClick={() => { onEdit(); setMenuOpen(false) }} className="flex items-center gap-2 w-full px-3 py-2 hover:bg-gray-50 text-left">
<Settings className="w-3.5 h-3.5" /> Edit Settings
</button>
<button onClick={() => { onPreview(); setMenuOpen(false) }} className="flex items-center gap-2 w-full px-3 py-2 hover:bg-gray-50 text-left">
<Eye className="w-3.5 h-3.5" /> Preview
</button>
<button onClick={() => { onAnalytics(); setMenuOpen(false) }} className="flex items-center gap-2 w-full px-3 py-2 hover:bg-gray-50 text-left">
<BarChart2 className="w-3.5 h-3.5" /> Analytics
</button>
<div className="h-px bg-gray-100" />
<button onClick={() => { onDelete(); setMenuOpen(false) }} className="flex items-center gap-2 w-full px-3 py-2 hover:bg-red-50 text-red-600 text-left">
<Trash2 className="w-3.5 h-3.5" /> Delete
</button>
</div>
</>
<Eye className="w-3.5 h-3.5" />
Preview
</Button>
{chatbot.is_published ? (
<Button variant="outline" size="sm" onClick={onUnpublish} className="flex-1">
<Lock className="w-3.5 h-3.5" />
Unpublish
</Button>
) : (
<Button size="sm" onClick={onPublish} className="flex-1">
<Globe className="w-3.5 h-3.5" />
Publish
</Button>
)}
</div>
</div>
{chatbot.description && (
<p className="text-xs text-gray-500 mb-3 line-clamp-2">{chatbot.description}</p>
)}
{/* Stats */}
<div className="flex gap-3 mb-4 text-xs text-gray-500">
<span>📄 {chatbot.document_count} docs</span>
<span>💬 {chatbot.conversation_count} chats</span>
{chatbot.category && <span>🏷 {chatbot.category}</span>}
</div>
{/* Actions */}
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={onPreview}
className="flex-1"
>
<Eye className="w-3.5 h-3.5" />
Preview
</Button>
{chatbot.is_published ? (
<Button
variant="ghost"
size="sm"
onClick={onUnpublish}
className="flex-1 text-orange-600 hover:bg-orange-50"
>
<Lock className="w-3.5 h-3.5" />
Unpublish
</Button>
) : (
<Button
variant="primary"
size="sm"
onClick={onPublish}
className="flex-1"
>
<Globe className="w-3.5 h-3.5" />
Publish
</Button>
)}
</div>
</Card>
</Card>
)
}
function getGreeting() {
const h = new Date().getHours()
if (h < 12) return 'morning'
if (h < 17) return 'afternoon'
return 'evening'
}
}

View File

@@ -1,10 +1,11 @@
import React, { useState } from 'react'
import { useQuery } from '@tanstack/react-query'
import { useNavigate } from 'react-router-dom'
import { marketplaceAPI } from '@/services/api'
import { useNavigate, useParams, Link } from 'react-router-dom'
import { marketplaceAPI, chatAPI } from '@/services/api'
import { Card, Spinner, EmptyState, Button } from '@/components/ui'
import { CATEGORIES, INDUSTRIES } from '@/lib/utils'
import { Search, Bot, Star, MessageSquare } from 'lucide-react'
import { ChatInterface } from '@/components/ChatInterface'
import { CATEGORIES, INDUSTRIES, useDebounce } from '@/lib/utils'
import { Search, Bot, Star, MessageSquare, ArrowLeft } from 'lucide-react'
import type { ChatbotPublic } from '@/types'
export const MarketplacePage: React.FC = () => {
@@ -14,224 +15,216 @@ export const MarketplacePage: React.FC = () => {
const [industry, setIndustry] = useState('')
const [page, setPage] = useState(1)
// IMP-07 FIX: Debounce search input by 300ms
const debouncedSearch = useDebounce(search, 300)
const { data, isLoading } = useQuery({
queryKey: ['marketplace', search, category, industry, page],
queryFn: () => marketplaceAPI.list({ search, category, industry, page, limit: 20 }),
queryKey: ['marketplace', debouncedSearch, category, industry, page],
queryFn: () => marketplaceAPI.list({ search: debouncedSearch, category, industry, page, limit: 20 }),
})
return (
<div className="p-6 max-w-6xl mx-auto">
<div className="mb-8">
<h1 className="text-2xl font-bold text-gray-900">AI Chatbot Marketplace</h1>
<p className="text-gray-500 mt-1 text-sm">Discover and interact with AI chatbots from companies worldwide</p>
</div>
{/* Search & Filters */}
<div className="flex flex-col sm:flex-row gap-3 mb-6">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
<input
type="text"
value={search}
onChange={e => { setSearch(e.target.value); setPage(1) }}
placeholder="Search chatbots..."
className="w-full pl-9 pr-4 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary-500"
/>
<div className="p-4 sm:p-6 max-w-6xl mx-auto">
<div className="mb-6 sm:mb-8">
<h1 className="text-xl sm:text-2xl font-bold text-gray-900">AI Chatbot Marketplace</h1>
<p className="text-gray-500 mt-1 text-sm">Discover and interact with AI chatbots from companies worldwide</p>
</div>
<select
value={category}
onChange={e => { setCategory(e.target.value); setPage(1) }}
className="px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary-500 bg-white"
>
<option value="">All Categories</option>
{CATEGORIES.map(c => <option key={c} value={c}>{c}</option>)}
</select>
<select
value={industry}
onChange={e => { setIndustry(e.target.value); setPage(1) }}
className="px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary-500 bg-white"
>
<option value="">All Industries</option>
{INDUSTRIES.map(i => <option key={i} value={i}>{i}</option>)}
</select>
</div>
{isLoading ? (
<div className="flex justify-center py-20"><Spinner className="text-primary-600" /></div>
) : !data?.chatbots?.length ? (
<EmptyState
icon={<Bot className="w-8 h-8" />}
title="No chatbots found"
description="Be the first to publish your AI chatbot to the marketplace!"
action={<Button onClick={() => navigate('/chatbots/new')}>Create Chatbot</Button>}
/>
) : (
<>
<div className="mb-4 text-sm text-gray-500">{data.total} chatbot{data.total !== 1 ? 's' : ''} available</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-8">
{data.chatbots.map(chatbot => (
<ChatbotMarketplaceCard
key={chatbot.id}
chatbot={chatbot}
onClick={() => navigate(`/marketplace/${chatbot.id}`)}
/>
))}
{/* Search & Filters */}
<div className="flex flex-col sm:flex-row gap-3 mb-6">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
<input
type="text"
value={search}
onChange={e => { setSearch(e.target.value); setPage(1) }}
placeholder="Search chatbots..."
className="w-full pl-9 pr-4 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary-500"
/>
</div>
<select
value={category}
onChange={e => { setCategory(e.target.value); setPage(1) }}
className="px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary-500 bg-white"
>
<option value="">All Categories</option>
{CATEGORIES.map(c => <option key={c} value={c}>{c}</option>)}
</select>
<select
value={industry}
onChange={e => { setIndustry(e.target.value); setPage(1) }}
className="px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary-500 bg-white"
>
<option value="">All Industries</option>
{INDUSTRIES.map(i => <option key={i} value={i}>{i}</option>)}
</select>
</div>
{/* Pagination */}
{data.total > 20 && (
<div className="flex justify-center gap-2">
<Button variant="outline" size="sm" onClick={() => setPage(p => Math.max(1, p - 1))} disabled={page === 1}>
Previous
</Button>
<span className="flex items-center px-3 text-sm text-gray-600">
Page {page} of {Math.ceil(data.total / 20)}
</span>
<Button variant="outline" size="sm" onClick={() => setPage(p => p + 1)} disabled={!data.has_more}>
Next
</Button>
</div>
)}
</>
)}
</div>
{isLoading ? (
<div className="flex justify-center py-20"><Spinner className="text-primary-600" /></div>
) : !data?.chatbots?.length ? (
<EmptyState
icon={<Bot className="w-8 h-8" />}
title="No chatbots found"
description="Be the first to publish your AI chatbot to the marketplace!"
/>
) : (
<>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{data.chatbots.map((chatbot) => (
<ChatbotMarketplaceCard
key={chatbot.id}
chatbot={chatbot}
onClick={() => navigate(`/marketplace/${chatbot.id}`)}
/>
))}
</div>
{/* Pagination */}
{data.has_more && (
<div className="flex justify-center mt-8 gap-2">
<Button
variant="outline"
size="sm"
disabled={page === 1}
onClick={() => setPage(p => Math.max(1, p - 1))}
>
Previous
</Button>
<span className="px-3 py-1.5 text-sm text-gray-500">Page {page}</span>
<Button
variant="outline"
size="sm"
disabled={!data.has_more}
onClick={() => setPage(p => p + 1)}
>
Next
</Button>
</div>
)}
</>
)}
</div>
)
}
const ChatbotMarketplaceCard: React.FC<{ chatbot: ChatbotPublic; onClick: () => void }> = ({ chatbot, onClick }) => (
<Card className="p-5 cursor-pointer hover:shadow-md transition-shadow" onClick={onClick}>
<div className="flex items-start gap-3 mb-3">
<div
className="w-10 h-10 rounded-xl flex items-center justify-center text-white flex-shrink-0"
style={{ background: chatbot.primary_color }}
>
<Bot className="w-5 h-5" />
<Card
className="p-5 cursor-pointer hover:shadow-md hover:border-gray-300 transition-all"
onClick={onClick}
>
<div className="flex items-center gap-3 mb-3">
<div
className="w-10 h-10 rounded-xl flex items-center justify-center text-white"
style={{ background: chatbot.primary_color }}
>
<Bot className="w-5 h-5" />
</div>
<div className="flex-1 min-w-0">
<h3 className="font-semibold text-gray-900 text-sm truncate">{chatbot.name}</h3>
{chatbot.company_name && (
<p className="text-xs text-gray-500 truncate">by {chatbot.company_name}</p>
)}
</div>
</div>
<div className="flex-1 min-w-0">
<h3 className="font-semibold text-gray-900 text-sm truncate">{chatbot.name}</h3>
{chatbot.company_name && (
<p className="text-xs text-gray-500 truncate">by {chatbot.company_name}</p>
)}
</div>
</div>
{chatbot.description && (
<p className="text-xs text-gray-600 mb-3 line-clamp-2">{chatbot.description}</p>
)}
<div className="flex flex-wrap gap-1.5 mb-3">
{chatbot.category && (
<span className="text-xs bg-primary-50 text-primary-700 px-2 py-0.5 rounded-full">
{chatbot.category}
</span>
{chatbot.description && (
<p className="text-xs text-gray-500 mb-3 line-clamp-2">{chatbot.description}</p>
)}
{chatbot.industry && (
<span className="text-xs bg-gray-100 text-gray-600 px-2 py-0.5 rounded-full">
{chatbot.industry}
</span>
)}
</div>
<div className="flex items-center justify-between text-xs text-gray-500">
<div className="flex items-center gap-3">
{chatbot.average_rating && (
<span className="flex items-center gap-0.5">
<Star className="w-3 h-3 fill-yellow-400 text-yellow-400" />
{chatbot.average_rating.toFixed(1)}
</span>
)}
<span className="flex items-center gap-0.5">
<MessageSquare className="w-3 h-3" />
{chatbot.total_conversations}
<div className="flex items-center gap-3 text-xs text-gray-400">
{chatbot.average_rating && chatbot.average_rating > 0 && (
<span className="flex items-center gap-1">
<Star className="w-3 h-3 text-yellow-400 fill-yellow-400" />
{chatbot.average_rating.toFixed(1)}
</span>
)}
<span className="flex items-center gap-1">
<MessageSquare className="w-3 h-3" />
{chatbot.total_conversations} chats
</span>
{chatbot.category && <span className="truncate">🏷 {chatbot.category}</span>}
</div>
<span className="text-primary-600 font-medium">Chat </span>
</div>
</Card>
</Card>
)
// ─── Chatbot Detail / Chat Page ────────────────────────────────────────────────
export const ChatbotDetailPage: React.FC = () => {
const { id } = useParams<{ id: string }>()
const navigate = useNavigate()
const { data: chatbot, isLoading } = useQuery({
const { data: chatbot, isLoading, error } = useQuery({
queryKey: ['marketplace-chatbot', id],
queryFn: () => marketplaceAPI.get(id!),
enabled: !!id,
})
if (isLoading) return <div className="flex justify-center py-20"><Spinner className="text-primary-600" /></div>
if (!chatbot) return <div className="text-center py-20 text-gray-500">Chatbot not found</div>
return (
<div className="p-6 max-w-4xl mx-auto">
<div className="flex items-center gap-4 mb-6">
<button onClick={() => navigate('/marketplace')} className="p-1.5 hover:bg-gray-100 rounded-lg">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
</button>
<div>
<h1 className="text-xl font-bold text-gray-900">{chatbot.name}</h1>
{chatbot.company_name && <p className="text-sm text-gray-500">by {chatbot.company_name}</p>}
if (isLoading) {
return (
<div className="flex justify-center py-20">
<Spinner className="text-primary-600" />
</div>
</div>
)
}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Chat */}
<div className="lg:col-span-2 h-[600px]">
<ChatInterface
chatbotId={chatbot.id}
chatbotName={chatbot.name}
welcomeMessage={chatbot.welcome_message}
primaryColor={chatbot.primary_color}
if (error || !chatbot) {
return (
<div className="p-6 max-w-2xl mx-auto text-center">
<EmptyState
icon={<Bot className="w-8 h-8" />}
title="Chatbot not found"
description="This chatbot may have been unpublished or removed."
action={
<Button onClick={() => navigate('/marketplace')} variant="outline">
<ArrowLeft className="w-4 h-4" />
Back to Marketplace
</Button>
}
/>
</div>
)
}
{/* Info sidebar */}
<div className="space-y-4">
<Card className="p-4">
<h3 className="font-semibold text-gray-900 text-sm mb-3">About</h3>
{chatbot.description && <p className="text-sm text-gray-600 mb-3">{chatbot.description}</p>}
<div className="space-y-2 text-sm">
{chatbot.category && (
<div className="flex justify-between">
<span className="text-gray-500">Category</span>
<span className="font-medium">{chatbot.category}</span>
</div>
)}
{chatbot.industry && (
<div className="flex justify-between">
<span className="text-gray-500">Industry</span>
<span className="font-medium">{chatbot.industry}</span>
</div>
)}
<div className="flex justify-between">
<span className="text-gray-500">Languages</span>
<span className="font-medium uppercase">{chatbot.languages?.join(', ')}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-500">Conversations</span>
<span className="font-medium">{chatbot.total_conversations}</span>
</div>
{chatbot.average_rating && (
<div className="flex justify-between">
<span className="text-gray-500">Rating</span>
<span className="font-medium flex items-center gap-1">
<Star className="w-3.5 h-3.5 fill-yellow-400 text-yellow-400" />
{chatbot.average_rating.toFixed(1)}
</span>
</div>
)}
</div>
</Card>
return (
<div className="p-4 sm:p-6 max-w-4xl mx-auto">
{/* Back link */}
<button
onClick={() => navigate('/marketplace')}
className="flex items-center gap-1.5 text-sm text-gray-500 hover:text-gray-700 mb-4 transition-colors"
>
<ArrowLeft className="w-4 h-4" />
Back to Marketplace
</button>
{/* Chatbot info */}
<div className="flex items-center gap-4 mb-6">
<div
className="w-14 h-14 rounded-2xl flex items-center justify-center text-white"
style={{ background: chatbot.primary_color }}
>
<Bot className="w-7 h-7" />
</div>
<div>
<h1 className="text-xl sm:text-2xl font-bold text-gray-900">{chatbot.name}</h1>
{chatbot.company_name && (
<p className="text-sm text-gray-500">by {chatbot.company_name}</p>
)}
</div>
</div>
{chatbot.description && (
<p className="text-gray-600 text-sm mb-6">{chatbot.description}</p>
)}
{/* R-05 FIX: Use viewport-relative height instead of fixed h-[600px] */}
<div className="h-[calc(100vh-280px)] min-h-[400px] max-h-[700px]">
<ChatInterface
chatbotId={chatbot.id}
chatbotName={chatbot.name}
welcomeMessage={chatbot.welcome_message}
primaryColor={chatbot.primary_color}
/>
</div>
</div>
</div>
)
}
// Import needed for ChatbotDetailPage
import { useParams } from 'react-router-dom'
import { ChatInterface } from '@/components/ChatInterface'
}

View File

@@ -1,6 +1,6 @@
import React, { useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { useMutation } from '@tanstack/react-query'
import { useNavigate, Link } from 'react-router-dom'
import { useQuery } from '@tanstack/react-query'
import { billingAPI } from '@/services/api'
import { useAuthStore } from '@/store/authStore'
import { Button, Card } from '@/components/ui'
@@ -91,6 +91,15 @@ export const PricingPage: React.FC = () => {
const navigate = useNavigate()
const [loading, setLoading] = useState<string | null>(null)
// BUG-10 FIX: Fetch current subscription to show "Current Plan" badge
const { data: subscription } = useQuery({
queryKey: ['subscription'],
queryFn: billingAPI.getSubscription,
enabled: !!user, // Only fetch if logged in
})
const currentPlan = subscription?.plan || user?.plan || 'free'
const handleSubscribe = async (planId: string) => {
if (!user) { navigate('/login'); return }
if (planId === 'enterprise') {
@@ -101,13 +110,15 @@ export const PricingPage: React.FC = () => {
navigate('/dashboard')
return
}
// BUG-10 FIX: Don't allow re-subscribing to current plan
if (planId === currentPlan) return
setLoading(planId)
try {
const { checkout_url } = await billingAPI.createCheckout(
planId,
`${window.location.origin}/settings/billing?success=true`,
`${window.location.origin}/pricing`
planId,
`${window.location.origin}/settings/billing?success=true`,
`${window.location.origin}/pricing`
)
window.location.href = checkout_url
} catch (err: any) {
@@ -117,110 +128,131 @@ export const PricingPage: React.FC = () => {
}
}
return (
<div className="p-6 max-w-6xl mx-auto">
<div className="text-center mb-12">
<h1 className="text-3xl font-bold text-gray-900 mb-3">Simple, transparent pricing</h1>
<p className="text-gray-500 max-w-xl mx-auto">
Start free and build as many chatbots as you want. Upgrade when you're ready to publish and go live.
</p>
</div>
// Helper to determine CTA text
const getCtaText = (planId: string): string => {
if (!user) return planId === 'enterprise' ? 'Contact Sales' : 'Get Started'
if (planId === currentPlan) return 'Current Plan'
if (planId === 'enterprise') return 'Contact Sales'
if (planId === 'free') return 'Downgrade'
return 'Upgrade'
}
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-6">
{PLANS.map((plan) => (
<div
key={plan.id}
className={`relative rounded-2xl border p-6 flex flex-col ${
plan.highlighted
? 'border-primary-400 bg-gradient-to-b from-primary-50 to-white shadow-lg shadow-primary-100'
: 'border-gray-200 bg-white'
}`}
>
{plan.badge && (
<div className="absolute -top-3 left-1/2 -translate-x-1/2">
const isCurrentPlan = (planId: string) => user && planId === currentPlan
return (
<div className="p-6 max-w-6xl mx-auto">
<div className="text-center mb-12">
<h1 className="text-3xl font-bold text-gray-900 mb-3">Simple, transparent pricing</h1>
<p className="text-gray-500 max-w-xl mx-auto">
Start free and build as many chatbots as you want. Upgrade when you're ready to publish and go live.
</p>
</div>
{/* R-06 FIX: Better grid breakpoints - md:grid-cols-2 instead of jumping to 4 */}
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-6">
{PLANS.map((plan) => (
<div
key={plan.id}
className={`relative rounded-2xl border p-6 flex flex-col ${
plan.highlighted
? 'border-primary-400 bg-gradient-to-b from-primary-50 to-white shadow-lg shadow-primary-100'
: isCurrentPlan(plan.id)
? 'border-primary-300 bg-primary-50/30 shadow-sm'
: 'border-gray-200 bg-white'
}`}
>
{/* BUG-10 FIX: Show "Current Plan" badge */}
{isCurrentPlan(plan.id) && (
<div className="absolute -top-3 left-1/2 -translate-x-1/2">
<span className="bg-green-600 text-white text-xs font-semibold px-3 py-1 rounded-full">
Current Plan
</span>
</div>
)}
{plan.badge && !isCurrentPlan(plan.id) && (
<div className="absolute -top-3 left-1/2 -translate-x-1/2">
<span className="bg-primary-600 text-white text-xs font-semibold px-3 py-1 rounded-full">
{plan.badge}
</span>
</div>
)}
<div className="mb-6">
<div className="text-3xl mb-2">{plan.icon}</div>
<h2 className="text-xl font-bold text-gray-900">{plan.name}</h2>
<p className="text-sm text-gray-500 mt-1 min-h-[40px]">{plan.description}</p>
<div className="mt-4">
{plan.price !== null ? (
<div className="flex items-baseline gap-1">
<span className="text-3xl font-bold text-gray-900">${plan.price}</span>
<span className="text-gray-500 text-sm">/month</span>
</div>
) : (
<span className="text-2xl font-bold text-gray-900">Custom</span>
</div>
)}
</div>
</div>
<ul className="space-y-2.5 flex-1 mb-6">
{plan.features.map(({ text, included }) => (
<li key={text} className="flex items-start gap-2.5">
<div className={`w-4 h-4 rounded-full flex-shrink-0 mt-0.5 flex items-center justify-center ${
included ? 'bg-green-100 text-green-600' : 'bg-gray-100 text-gray-400'
}`}>
{included ? <Check className="w-2.5 h-2.5" /> : <span className="text-xs"></span>}
<div className="mb-6">
<div className="text-3xl mb-2">{plan.icon}</div>
<h2 className="text-xl font-bold text-gray-900">{plan.name}</h2>
<p className="text-sm text-gray-500 mt-1 min-h-[40px]">{plan.description}</p>
<div className="mt-4">
{plan.price !== null ? (
<div className="flex items-baseline gap-1">
<span className="text-4xl font-extrabold text-gray-900">${plan.price}</span>
{plan.price > 0 && <span className="text-gray-500 text-sm">/month</span>}
</div>
) : (
<div className="text-2xl font-bold text-gray-900">Custom</div>
)}
</div>
<span className={`text-sm ${included ? 'text-gray-700' : 'text-gray-400'}`}>{text}</span>
</li>
))}
</ul>
</div>
<Button
variant={plan.highlighted ? 'primary' : 'outline'}
className="w-full"
loading={loading === plan.id}
onClick={() => handleSubscribe(plan.id)}
disabled={user?.plan === plan.id}
>
{user?.plan === plan.id
? 'Current Plan'
: plan.price === null
? 'Contact Sales'
: plan.price === 0
? 'Get Started Free'
: `Subscribe $${plan.price}/mo`}
</Button>
</div>
))}
</div>
<div className="flex-1">
<ul className="space-y-3 mb-8">
{plan.features.map((feature) => (
<li key={feature.text} className="flex items-start gap-2 text-sm">
<div className={`w-4 h-4 rounded-full flex items-center justify-center flex-shrink-0 mt-0.5 ${
feature.included ? 'bg-green-100 text-green-600' : 'bg-gray-100 text-gray-400'
}`}>
<Check className="w-2.5 h-2.5" />
</div>
<span className={feature.included ? 'text-gray-700' : 'text-gray-400'}>
{feature.text}
</span>
</li>
))}
</ul>
</div>
{/* FAQ */}
<div className="mt-16 max-w-2xl mx-auto">
<h2 className="text-xl font-bold text-gray-900 mb-6 text-center">Frequently Asked Questions</h2>
<div className="space-y-4">
{[
{
q: 'What is preview mode?',
a: 'Preview mode lets you build and test your chatbot for free with unlimited conversations. Only you (and people you share the link with) can access it until you publish.'
},
{
q: 'Can I cancel anytime?',
a: 'Yes, you can cancel anytime. Your chatbots will remain in preview mode but will be removed from the marketplace.'
},
{
q: 'What is code export?',
a: 'Pro plan users can export their chatbot as a complete, production-ready package including a FastAPI backend and React TypeScript widget giving you full control to self-host.'
},
{
q: 'Do I need my own API keys?',
a: 'No! Your API keys are handled by Contexta. However, if you export the code, you\'ll need your own API keys for self-hosted deployment.'
},
].map(({ q, a }) => (
<div key={q} className="bg-white border border-gray-200 rounded-xl p-4">
<h3 className="font-semibold text-gray-900 text-sm mb-1">{q}</h3>
<p className="text-sm text-gray-500">{a}</p>
</div>
<Button
onClick={() => handleSubscribe(plan.id)}
loading={loading === plan.id}
// BUG-10 FIX: Disable CTA for current plan
disabled={isCurrentPlan(plan.id) || loading === plan.id}
variant={plan.highlighted ? 'default' : 'outline'}
className="w-full"
>
{getCtaText(plan.id)}
</Button>
</div>
))}
</div>
{/* FAQ */}
<div className="mt-16 max-w-2xl mx-auto">
<h2 className="text-xl font-bold text-gray-900 text-center mb-8">Frequently Asked Questions</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{[
{
q: 'Can I use the free tier forever?',
a: 'Yes! Build and test unlimited chatbots for free. Your chatbots will remain in preview mode but will be removed from the marketplace.'
},
{
q: 'What is code export?',
a: 'Pro plan users can export their chatbot as a complete, production-ready package including a FastAPI backend and React TypeScript widget giving you full control to self-host.'
},
{
q: 'Do I need my own API keys?',
a: 'No! Your API keys are handled by Contexta. However, if you export the code, you\'ll need your own API keys for self-hosted deployment.'
},
{
q: 'Can I cancel anytime?',
a: 'Yes, you can cancel your subscription anytime. Your chatbots will revert to preview mode at the end of your billing period.'
},
].map(({ q, a }) => (
<div key={q} className="bg-white border border-gray-200 rounded-xl p-4">
<h3 className="font-semibold text-gray-900 text-sm mb-1">{q}</h3>
<p className="text-sm text-gray-500">{a}</p>
</div>
))}
</div>
</div>
</div>
</div>
)
}
}

View File

@@ -1,6 +1,6 @@
import React, { useState } from 'react'
import { useQuery, useMutation } from '@tanstack/react-query'
import { useNavigate, Link } from 'react-router-dom'
import React, { useState, useEffect, useCallback } from 'react'
import { useQuery } from '@tanstack/react-query'
import { useNavigate, useLocation, Link } from 'react-router-dom'
import { billingAPI } from '@/services/api'
import { useAuthStore } from '@/store/authStore'
import { Button, Card, Input, Badge } from '@/components/ui'
@@ -8,48 +8,74 @@ import { getPlanColor, formatDate } from '@/lib/utils'
import { CreditCard, User, Shield, Download, ExternalLink } from 'lucide-react'
export const SettingsPage: React.FC = () => {
const { user, updateUser } = useAuthStore()
// BUG-04 FIX: Removed unused 'updateUser' from destructuring
const { user } = useAuthStore()
const navigate = useNavigate()
const [tab, setTab] = useState<'profile' | 'billing' | 'export'>('profile')
const location = useLocation()
// BUG-06 FIX: Sync tab with URL path on mount and when path changes
const getTabFromPath = (pathname: string): 'profile' | 'billing' => {
if (pathname === '/settings/billing') return 'billing'
return 'profile'
}
const [tab, setTab] = useState<'profile' | 'billing'>(getTabFromPath(location.pathname))
const [toast, setToast] = useState('')
// Keep tab in sync if URL changes externally
useEffect(() => {
setTab(getTabFromPath(location.pathname))
}, [location.pathname])
// Update URL when tab changes
const handleTabChange = useCallback((newTab: 'profile' | 'billing') => {
setTab(newTab)
const newPath = newTab === 'billing' ? '/settings/billing' : '/settings'
if (location.pathname !== newPath) {
navigate(newPath, { replace: true })
}
}, [navigate, location.pathname])
const showToast = (msg: string) => {
setToast(msg)
setTimeout(() => setToast(''), 3000)
}
return (
<div className="p-6 max-w-3xl mx-auto">
<h1 className="text-2xl font-bold text-gray-900 mb-6">Settings</h1>
<div className="p-6 max-w-3xl mx-auto">
<h1 className="text-2xl font-bold text-gray-900 mb-6">Settings</h1>
{/* Tabs */}
<div className="flex gap-1 mb-6 bg-gray-100 p-1 rounded-xl w-fit">
{[
{ id: 'profile', label: 'Profile', icon: User },
{ id: 'billing', label: 'Billing', icon: CreditCard },
].map(({ id, label, icon: Icon }) => (
<button
key={id}
onClick={() => setTab(id as any)}
className={`flex items-center gap-1.5 px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
tab === id ? 'bg-white shadow-sm text-gray-900' : 'text-gray-500 hover:text-gray-700'
}`}
>
<Icon className="w-3.5 h-3.5" />
{label}
</button>
))}
</div>
{tab === 'profile' && <ProfileSettings onToast={showToast} />}
{tab === 'billing' && <BillingSettings onToast={showToast} />}
{toast && (
<div className="fixed bottom-4 right-4 bg-gray-900 text-white px-4 py-2 rounded-lg text-sm shadow-lg z-50">
{toast}
{/* Tabs */}
<div className="flex gap-1 mb-6 bg-gray-100 p-1 rounded-xl w-fit">
{[
{ id: 'profile' as const, label: 'Profile', icon: User },
{ id: 'billing' as const, label: 'Billing', icon: CreditCard },
].map(({ id, label, icon: Icon }) => (
<button
key={id}
onClick={() => handleTabChange(id)}
className={`flex items-center gap-1.5 px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
tab === id
? 'bg-white shadow-sm text-gray-900'
: 'text-gray-500 hover:text-gray-700'
}`}
>
<Icon className="w-3.5 h-3.5" />
{label}
</button>
))}
</div>
)}
</div>
{tab === 'profile' && <ProfileSettings onToast={showToast} />}
{tab === 'billing' && <BillingSettings onToast={showToast} />}
{toast && (
<div className="fixed bottom-4 right-4 bg-gray-900 text-white px-4 py-2 rounded-lg text-sm shadow-lg z-50 animate-fade-in-up">
{toast}
<button onClick={() => setToast('')} className="ml-3 opacity-60 hover:opacity-100">&times;</button>
</div>
)}
</div>
)
}
@@ -57,22 +83,22 @@ const ProfileSettings: React.FC<{ onToast: (msg: string) => void }> = ({ onToast
const { user } = useAuthStore()
return (
<Card className="p-6 space-y-4">
<h2 className="font-semibold text-gray-900">Profile Information</h2>
<Input label="Email" value={user?.email || ''} disabled />
<Input label="Company Name" value={user?.company_name || ''} disabled hint="Contact support to change company name" />
<div>
<label className="block text-sm font-medium text-gray-700 mb-1.5">Plan</label>
<div className="flex items-center gap-2">
<Card className="p-6 space-y-4">
<h2 className="font-semibold text-gray-900">Profile Information</h2>
<Input label="Email" value={user?.email || ''} disabled />
<Input label="Company Name" value={user?.company_name || ''} disabled hint="Contact support to change company name" />
<div>
<label className="block text-sm font-medium text-gray-700 mb-1.5">Plan</label>
<div className="flex items-center gap-2">
<span className={`px-3 py-1 text-sm font-medium rounded-full capitalize ${getPlanColor(user?.plan || 'free')}`}>
{user?.plan || 'free'}
</span>
<Link to="/pricing" className="text-sm text-primary-600 hover:underline">
Manage plan
</Link>
<Link to="/pricing" className="text-sm text-primary-600 hover:underline">
Manage plan
</Link>
</div>
</div>
</div>
</Card>
</Card>
)
}
@@ -101,59 +127,59 @@ const BillingSettings: React.FC<{ onToast: (msg: string) => void }> = ({ onToast
const isPaid = subscription?.plan && subscription.plan !== 'free'
return (
<div className="space-y-4">
<Card className="p-6">
<h2 className="font-semibold text-gray-900 mb-4">Current Plan</h2>
<div className="flex items-center justify-between mb-4">
<div>
<div className="space-y-4">
<Card className="p-6">
<h2 className="font-semibold text-gray-900 mb-4">Current Plan</h2>
<div className="flex items-center justify-between mb-4">
<div>
<span className={`px-3 py-1 text-sm font-semibold rounded-full capitalize ${getPlanColor(subscription?.plan || 'free')}`}>
{subscription?.plan || 'free'}
</span>
<p className="text-xs text-gray-500 mt-1">
Status: <span className={subscription?.status === 'active' ? 'text-green-600' : 'text-red-600'}>
<p className="text-xs text-gray-500 mt-1">
Status: <span className={subscription?.status === 'active' ? 'text-green-600' : 'text-red-600'}>
{subscription?.status || 'active'}
</span>
</p>
</p>
</div>
{isPaid && subscription?.current_period_end && (
<div className="text-right">
<p className="text-xs text-gray-500">Renews on</p>
<p className="text-sm font-medium">{formatDate(subscription.current_period_end)}</p>
</div>
)}
</div>
{isPaid && subscription?.current_period_end && (
<div className="text-right">
<p className="text-xs text-gray-500">Renews on</p>
<p className="text-sm font-medium">{formatDate(subscription.current_period_end)}</p>
</div>
)}
</div>
<div className="flex gap-3">
{!isPaid ? (
<Button onClick={() => navigate('/pricing')} className="flex-1">
Upgrade Plan
</Button>
) : (
<Button variant="outline" onClick={handlePortal} loading={loading} className="flex-1">
<ExternalLink className="w-3.5 h-3.5" />
Manage Billing
</Button>
)}
</div>
</Card>
<div className="flex gap-3">
{!isPaid ? (
<Button onClick={() => navigate('/pricing')} className="flex-1">
Upgrade Plan
</Button>
) : (
<Button variant="outline" onClick={handlePortal} loading={loading} className="flex-1">
<ExternalLink className="w-3.5 h-3.5" />
Manage Billing
</Button>
)}
</div>
</Card>
{/* Plan features */}
<Card className="p-6">
<h3 className="font-semibold text-gray-900 mb-4">Plan Features</h3>
<div className="space-y-3">
{[
{ label: 'Chatbots created', value: 'Unlimited', always: true },
{ label: 'Chatbots published', value: subscription?.plan === 'pro' ? '3' : subscription?.plan === 'starter' ? '1' : '0 (upgrade to publish)', always: true },
{ label: 'Conversations/month', value: subscription?.plan === 'pro' ? '20,000' : subscription?.plan === 'starter' ? '5,000' : 'Unlimited (preview)', always: true },
{ label: 'Code export', value: subscription?.plan === 'pro' || subscription?.plan === 'enterprise' ? '✅ Included' : '❌ Pro+ only', always: true },
].map(({ label, value }) => (
<div key={label} className="flex justify-between py-2 border-b border-gray-100 last:border-0">
<span className="text-sm text-gray-600">{label}</span>
<span className="text-sm font-medium text-gray-900">{value}</span>
</div>
))}
</div>
</Card>
</div>
{/* Plan features */}
<Card className="p-6">
<h3 className="font-semibold text-gray-900 mb-4">Plan Features</h3>
<div className="space-y-3">
{[
{ label: 'Chatbots created', value: 'Unlimited' },
{ label: 'Chatbots published', value: subscription?.plan === 'pro' ? '3' : subscription?.plan === 'starter' ? '1' : '0 (upgrade to publish)' },
{ label: 'Conversations/month', value: subscription?.plan === 'pro' ? '20,000' : subscription?.plan === 'starter' ? '5,000' : 'Unlimited (preview)' },
{ label: 'Code export', value: subscription?.plan === 'pro' || subscription?.plan === 'enterprise' ? '✅ Included' : '❌ Pro+ only' },
].map(({ label, value }) => (
<div key={label} className="flex justify-between py-2 border-b border-gray-100 last:border-0">
<span className="text-sm text-gray-600">{label}</span>
<span className="text-sm font-medium text-gray-900">{value}</span>
</div>
))}
</div>
</Card>
</div>
)
}
}