diff --git a/package.json b/package.json index 8f88ba7..d61549e 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "contexta-fe", + "name": "contexta_fe", "private": true, "version": "0.0.0", "type": "module", diff --git a/src/App.tsx b/src/App.tsx index 26e666e..3334a6a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -10,6 +10,8 @@ import './App.css' const LandingPage = lazy(() => import('@/pages/LandingPage').then(m => ({ default: m.LandingPage }))) const LoginPage = lazy(() => import('@/pages/AuthPages').then(m => ({ default: m.LoginPage }))) const SignupPage = lazy(() => import('@/pages/AuthPages').then(m => ({ default: m.SignupPage }))) +const ForgotPasswordPage = lazy(() => import('@/pages/ForgotPasswordPage').then(m => ({ default: m.ForgotPasswordPage }))) +const ResetPasswordPage = lazy(() => import('@/pages/ResetPasswordPage').then(m => ({ default: m.ResetPasswordPage }))) const DashboardPage = lazy(() => import('@/pages/DashboardPage').then(m => ({ default: m.DashboardPage }))) const ChatbotBuilderPage = lazy(() => import('@/pages/ChatbotBuilderPage').then(m => ({ default: m.ChatbotBuilderPage }))) const MarketplacePage = lazy(() => import('@/pages/MarketplacePage').then(m => ({ default: m.MarketplacePage }))) @@ -17,6 +19,9 @@ const ChatbotDetailPage = lazy(() => import('@/pages/MarketplacePage').then(m => const PricingPage = lazy(() => import('@/pages/PricingPage').then(m => ({ default: m.PricingPage }))) const SettingsPage = lazy(() => import('@/pages/SettingsPage').then(m => ({ default: m.SettingsPage }))) const AnalyticsPage = lazy(() => import('@/pages/AnalyticsPage').then(m => ({ default: m.AnalyticsPage }))) +const PublicChatPage = lazy(() => import('@/pages/PublicChatPage').then(m => ({ default: m.PublicChatPage }))) +const InboxPage = lazy(() => import('@/pages/InboxPage').then(m => ({ default: m.InboxPage }))) +const LeadsPage = lazy(() => import('@/pages/LeadsPage').then(m => ({ default: m.LeadsPage }))) const PageLoader = () => (
@@ -55,9 +60,14 @@ export const App: React.FC = () => ( } /> } /> + {/* Public chat - no auth, no layout */} + } /> + {/* Auth */} } /> } /> + } /> + } /> {/* Protected */} } /> @@ -65,6 +75,8 @@ export const App: React.FC = () => ( } /> } /> } /> + } /> + } /> } /> } /> diff --git a/src/components/ChatInterface.tsx b/src/components/ChatInterface.tsx index 72c1418..ca9e7da 100644 --- a/src/components/ChatInterface.tsx +++ b/src/components/ChatInterface.tsx @@ -1,8 +1,8 @@ import React, { useState, useEffect, useRef } from 'react' import { cn } from '@/lib/utils' -import { chatAPI } from '@/services/api' -import type { ChatMessage, SourceDocument } from '@/types' -import { Send, Bot, User, FileText, ChevronDown, ChevronUp } from 'lucide-react' +import { chatAPI, leadsAPI } from '@/services/api' +import type { ChatMessage } from '@/types' +import { Send, Bot, FileText, ChevronDown, ChevronUp } from 'lucide-react' interface ChatInterfaceProps { chatbotId: string @@ -12,11 +12,24 @@ interface ChatInterfaceProps { logoUrl?: string isPreview?: boolean sessionId?: string + showBranding?: boolean + leadCaptureEnabled?: boolean + leadCaptureFields?: string[] + leadCaptureTrigger?: string + handoffEnabled?: boolean + handoffMessage?: string + chatbotIdForLeads?: string + conversationId?: string } export const ChatInterface: React.FC = ({ - chatbotId, chatbotName, welcomeMessage, primaryColor, logoUrl, isPreview = false, sessionId: externalSessionId - }) => { + chatbotId, chatbotName, welcomeMessage, primaryColor, logoUrl, + isPreview = false, sessionId: externalSessionId, + showBranding = false, leadCaptureEnabled = false, + leadCaptureFields = ['email'], leadCaptureTrigger = 'after_first_message', + handoffEnabled = false, handoffMessage: _handoffMessage, + chatbotIdForLeads, conversationId, +}) => { const [messages, setMessages] = useState([ { id: '0', role: 'assistant', content: welcomeMessage } ]) @@ -33,6 +46,12 @@ export const ChatInterface: React.FC = ({ return newId }) + const [feedbackSent, setFeedbackSent] = useState>(new Set()) + const [showLeadForm, setShowLeadForm] = useState(false) + const [leadSubmitted, setLeadSubmitted] = useState(false) + const [leadFormData, setLeadFormData] = useState({ email: '', name: '', phone: '', company: '' }) + const [activeConversationId, setActiveConversationId] = useState(conversationId || null) + const [expandedSources, setExpandedSources] = useState>(new Set()) const bottomRef = useRef(null) const inputRef = useRef(null) @@ -68,11 +87,27 @@ export const ChatInterface: React.FC = ({ sources: response.sources, } setMessages(prev => [...prev, assistantMsg]) - } catch (err: any) { + + // Track conversation ID if returned + if ((response as { conversation_id?: string }).conversation_id) { + setActiveConversationId((response as { conversation_id?: string }).conversation_id!) + } + + // Check if lead capture needed (after first message) + if (response.needs_lead_capture && !leadSubmitted) { + setShowLeadForm(true) + } + + // Handle handoff + if (response.handoff) { + // handoffMessage will be shown via the assistantMsg content (backend sends it) + } + } catch (err) { + const e = err as { response?: { data?: { detail?: string } } } const errMsg: ChatMessage = { id: crypto.randomUUID(), role: 'assistant', - content: err.response?.data?.detail || 'Sorry, I encountered an error. Please try again.', + content: e.response?.data?.detail || 'Sorry, I encountered an error. Please try again.', } setMessages(prev => [...prev, errMsg]) } finally { @@ -81,6 +116,31 @@ export const ChatInterface: React.FC = ({ } } + const handleFeedback = async (msgId: string, feedback: 'positive' | 'negative') => { + if (feedbackSent.has(msgId)) return + try { + await chatAPI.feedback(chatbotId, msgId, feedback) + setFeedbackSent(prev => new Set(prev).add(msgId)) + } catch { + // silently fail + } + } + + const handleLeadSubmit = async (e: React.FormEvent) => { + e.preventDefault() + try { + await leadsAPI.submit(chatbotId, { + ...leadFormData, + conversation_id: activeConversationId || undefined, + }) + } catch { + // silently fail + } finally { + setLeadSubmitted(true) + setShowLeadForm(false) + } + } + const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault() @@ -91,7 +151,7 @@ export const ChatInterface: React.FC = ({ const toggleSources = (msgId: string) => { setExpandedSources(prev => { const n = new Set(prev) - n.has(msgId) ? n.delete(msgId) : n.add(msgId) + if (n.has(msgId)) { n.delete(msgId) } else { n.add(msgId) } return n }) } @@ -126,74 +186,114 @@ export const ChatInterface: React.FC = ({
{/* Header */}
{logoUrl ? ( {chatbotName} ) : ( -
+
)} -
-

{chatbotName}

- {isPreview && Preview mode} +
+

{chatbotName}

+
+ + {isPreview ? 'Preview mode' : 'Online'} +
{/* Messages */} -
- {messages.map((msg) => ( -
+
+ {messages.map((msg, msgIdx) => ( +
{msg.role === 'assistant' && } -
-

{msg.content}

+
+
+

{msg.content}

- {/* Sources */} - {msg.sources && msg.sources.length > 0 && ( -
- - {expandedSources.has(msg.id) && ( -
- {msg.sources.map((src, i) => ( -
-

{src.document_name}

-

{src.chunk_text}

-
- ))} -
+ {/* Sources */} + {msg.sources && msg.sources.length > 0 && ( +
+ + {expandedSources.has(msg.id) && ( +
+ {msg.sources.map((src, i) => ( +
+

{src.document_name}

+

{src.chunk_text}

+
+ ))} +
+ )} +
+ )} +
+ + {msg.role === 'assistant' && msg.id !== '0' && ( +
+
+ title="Helpful" + > + πŸ‘ + + +
)}
))} {loading && ( -
+
-
-
-
-
-
+
+
+
+
+
@@ -201,29 +301,97 @@ export const ChatInterface: React.FC = ({
+ {showLeadForm && !leadSubmitted && ( +
+

+ Quick question before we continue +

+

Share your details and we'll follow up if needed.

+
+ {(leadCaptureFields || ['email']).includes('email') && ( + setLeadFormData(prev => ({ ...prev, email: e.target.value }))} + className="w-full border border-blue-200 bg-white rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-400/30 placeholder-blue-300" + required + /> + )} + {(leadCaptureFields || []).includes('name') && ( + setLeadFormData(prev => ({ ...prev, name: e.target.value }))} + className="w-full border border-blue-200 bg-white rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-400/30 placeholder-blue-300" + /> + )} + {(leadCaptureFields || []).includes('phone') && ( + setLeadFormData(prev => ({ ...prev, phone: e.target.value }))} + className="w-full border border-blue-200 bg-white rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-400/30 placeholder-blue-300" + /> + )} + {(leadCaptureFields || []).includes('company') && ( + setLeadFormData(prev => ({ ...prev, company: e.target.value }))} + className="w-full border border-blue-200 bg-white rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-400/30 placeholder-blue-300" + /> + )} +
+ + +
+
+
+ )} + {/* Input */} -
+
-