mirror of
http://88.130.71.182:3000/BlitTech/contexta_fe.git
synced 2026-06-13 10:58:40 +00:00
Initial commit
This commit is contained in:
299
src/pages/DashboardPage.tsx
Normal file
299
src/pages/DashboardPage.tsx
Normal file
@@ -0,0 +1,299 @@
|
||||
import React, { useState } from 'react'
|
||||
import { Link, useNavigate } from 'react-router-dom'
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { chatbotsAPI } from '@/services/api'
|
||||
import { useAuthStore } from '@/store/authStore'
|
||||
import { Button, Card, Badge, StatusDot, EmptyState, Modal, Spinner } from '@/components/ui'
|
||||
import { formatDate, getFileIcon, cn } from '@/lib/utils'
|
||||
import type { Chatbot } from '@/types'
|
||||
import {
|
||||
Bot, Plus, MoreHorizontal, Globe, Lock, Trash2,
|
||||
Settings, Upload, Eye, ExternalLink, Download, BarChart2
|
||||
} from 'lucide-react'
|
||||
|
||||
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 { data: chatbots = [], isLoading } = useQuery({
|
||||
queryKey: ['chatbots'],
|
||||
queryFn: chatbotsAPI.list,
|
||||
})
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: chatbotsAPI.delete,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['chatbots'] })
|
||||
setDeleteId(null)
|
||||
setToast('Chatbot deleted')
|
||||
},
|
||||
})
|
||||
|
||||
const publishMutation = useMutation({
|
||||
mutationFn: chatbotsAPI.publish,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['chatbots'] })
|
||||
setToast('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')
|
||||
},
|
||||
})
|
||||
|
||||
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>
|
||||
<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>
|
||||
</div>
|
||||
<Link to="/pricing">
|
||||
<Button size="sm">Upgrade</Button>
|
||||
</Link>
|
||||
</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>
|
||||
</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>
|
||||
|
||||
{/* 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>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
const ChatbotCard: React.FC<{
|
||||
chatbot: Chatbot
|
||||
onEdit: () => void
|
||||
onPreview: () => void
|
||||
onPublish: () => void
|
||||
onUnpublish: () => void
|
||||
onDelete: () => void
|
||||
onAnalytics: () => void
|
||||
}> = ({ chatbot, onEdit, onPreview, onPublish, onUnpublish, onDelete, onAnalytics }) => {
|
||||
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">
|
||||
{chatbot.is_published ? 'Published' : 'Preview'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<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">
|
||||
<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>
|
||||
</>
|
||||
)}
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
||||
function getGreeting() {
|
||||
const h = new Date().getHours()
|
||||
if (h < 12) return 'morning'
|
||||
if (h < 17) return 'afternoon'
|
||||
return 'evening'
|
||||
}
|
||||
Reference in New Issue
Block a user