diff --git a/src/App.tsx b/src/App.tsx index 8bd23f9..cc698dc 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,16 +1,28 @@ -import React from 'react' +import React, { lazy, Suspense } from 'react' import { Routes, Route, Navigate } from 'react-router-dom' import { useAuthStore } from '@/store/authStore' import { AppLayout } from '@/components/Layout' -import { LandingPage } from '@/pages/LandingPage' -import { LoginPage, SignupPage } from '@/pages/AuthPages' -import { DashboardPage } from '@/pages/DashboardPage' -import { ChatbotBuilderPage } from '@/pages/ChatbotBuilderPage' -import { MarketplacePage, ChatbotDetailPage } from '@/pages/MarketplacePage' -import { PricingPage } from '@/pages/PricingPage' -import { SettingsPage } from '@/pages/SettingsPage' +import { PublicLayout } from '@/components/PublicLayout' +import { Spinner } from '@/components/ui' import './App.css' +// IMP-02 FIX: Route code splitting with lazy imports +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 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 }))) +const ChatbotDetailPage = lazy(() => import('@/pages/MarketplacePage').then(m => ({ default: m.ChatbotDetailPage }))) +const PricingPage = lazy(() => import('@/pages/PricingPage').then(m => ({ default: m.PricingPage }))) +const SettingsPage = lazy(() => import('@/pages/SettingsPage').then(m => ({ default: m.SettingsPage }))) + +const PageLoader = () => ( +
+ +
+) + const PrivateRoute: React.FC<{ children: React.ReactNode }> = ({ children }) => { const { isAuthenticated } = useAuthStore() if (!isAuthenticated) return @@ -23,27 +35,42 @@ const PublicOnlyRoute: React.FC<{ children: React.ReactNode }> = ({ children }) return <>{children} } +// BUG-07/08 FIX: Smart wrapper that uses AppLayout for authenticated users +// and PublicLayout for unauthenticated users, solving the "lost sidebar" issue +const SmartPublicRoute: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const { isAuthenticated } = useAuthStore() + if (isAuthenticated) { + return {children} + } + // R-07 FIX: PublicLayout adds navigation header for unauthenticated users + return {children} +} + export const App: React.FC = () => ( - - {/* Public */} - } /> - } /> - } /> - } /> + }> + + {/* Public - Landing has its own nav */} + } /> - {/* Auth */} - } /> - } /> + {/* Public pages - wrapped in SmartPublicRoute for proper nav */} + } /> + } /> + } /> - {/* Protected */} - } /> - } /> - } /> - } /> - } /> - } /> + {/* Auth */} + } /> + } /> - {/* Fallback */} - } /> - + {/* Protected */} + } /> + } /> + } /> + } /> + } /> + } /> + + {/* Fallback */} + } /> + + ) \ No newline at end of file diff --git a/src/components/ChatInterface.tsx b/src/components/ChatInterface.tsx index 32cea43..e8a5ef2 100644 --- a/src/components/ChatInterface.tsx +++ b/src/components/ChatInterface.tsx @@ -10,20 +10,33 @@ interface ChatInterfaceProps { welcomeMessage: string primaryColor: string isPreview?: boolean + // BUG-09 FIX: Accept optional sessionId to persist conversations across navigation + sessionId?: string } export const ChatInterface: React.FC = ({ - chatbotId, chatbotName, welcomeMessage, primaryColor, isPreview = false -}) => { + chatbotId, chatbotName, welcomeMessage, primaryColor, isPreview = false, sessionId: externalSessionId + }) => { const [messages, setMessages] = useState([ { id: '0', role: 'assistant', content: welcomeMessage } ]) const [input, setInput] = useState('') const [loading, setLoading] = useState(false) - const [sessionId] = useState(() => crypto.randomUUID()) + + // BUG-09 FIX: Use provided sessionId or persist in sessionStorage + const [sessionId] = useState(() => { + if (externalSessionId) return externalSessionId + const storageKey = `chat-session-${chatbotId}` + const stored = sessionStorage.getItem(storageKey) + if (stored) return stored + const newId = crypto.randomUUID() + sessionStorage.setItem(storageKey, newId) + return newId + }) + const [expandedSources, setExpandedSources] = useState>(new Set()) const bottomRef = useRef(null) - const inputRef = useRef(null) + const inputRef = useRef(null) useEffect(() => { bottomRef.current?.scrollIntoView({ behavior: 'smooth' }) @@ -69,6 +82,14 @@ export const ChatInterface: React.FC = ({ } } + // IMP-08: Shift+Enter for newline, Enter to send + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault() + send() + } + } + const toggleSources = (msgId: string) => { setExpandedSources(prev => { const n = new Set(prev) @@ -78,120 +99,112 @@ export const ChatInterface: React.FC = ({ } return ( -
- {/* Header */} -
-
- +
+ {/* Header */} +
+
+ +
+
+

{chatbotName}

+ {isPreview && Preview mode} +
-
-

{chatbotName}

-

Online

-
- {isPreview && ( - - Preview - - )} -
- {/* Messages */} -
- {messages.map((msg) => ( -
- {/* Avatar */} -
- {msg.role === 'assistant' ? : } -
- -
-
- {msg.content} -
- - {/* Sources */} - {msg.role === 'assistant' && msg.sources && msg.sources.length > 0 && ( -
- - {expandedSources.has(msg.id) && ( -
- {msg.sources.map((s, i) => ( -
-

📄 {s.document_name}

-

{s.chunk_text}

-
- ))} + {/* Messages */} +
+ {messages.map((msg) => ( +
+ {msg.role === 'assistant' && ( +
+
+ )} +
+

{msg.content}

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

{src.document_name}

+

{src.chunk_text}

+

Relevance: {(src.score * 100).toFixed(0)}%

+
+ ))} +
+ )} +
)}
- )} -
-
- ))} - - {loading && ( -
-
- -
-
-
- - - + {msg.role === 'user' && ( +
+ +
+ )}
-
-
- )} -
-
+ ))} - {/* Input */} -
-
- setInput(e.target.value)} - onKeyDown={(e) => e.key === 'Enter' && !e.shiftKey && send()} - placeholder="Type a message..." - className="flex-1 px-3 py-2 border border-gray-200 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-primary-500" - /> - + {/* Typing indicator */} + {loading && ( +
+
+ +
+
+
+
+
+
+
+
+
+ )} + +
+
+ + {/* Input */} +
+
+