import { useEffect } from 'react' import { SignedIn, SignedOut, useAuth } from '@clerk/clerk-react' import { useRef, useState, useCallback } from 'react' import { get, setAuthToken, setTokenRefresh } from './api/client.js' import { sendChatMessage, sendSlashCommand } from './api/chat.js' import { useConversations } from './hooks/useConversations.js' import { useMessages } from './hooks/useMessages.js' import { useDocuments } from './hooks/useDocuments.js' import { useImageUpload } from './hooks/useImageUpload.js' import { useFullscreenImage } from './hooks/useFullscreenImage.js' import { useClickOutside } from './hooks/useClickOutside.js' import { DEFAULT_TOOLS } from './utils/constants.js' import SlashCommandsDropdown, { parseSlashCommand } from './components/chat/SlashCommandsDropdown.jsx' import Header from './components/layout/Header.jsx' import Sidebar from './components/layout/Sidebar.jsx' import MessageList from './components/chat/MessageList.jsx' import FullscreenImage from './components/chat/FullscreenImage.jsx' import ToolMenu from './components/documents/ToolMenu.jsx' import ErrorBoundary from './components/common/ErrorBoundary.jsx' import SignInPage from './components/auth/SignInPage.jsx' // Visually hidden style for file inputs — keeps them in the DOM for mobile browsers const hiddenInputStyle = { position: 'fixed', top: '-99999px', left: '-99999px', opacity: 0, pointerEvents: 'none', width: '1px', height: '1px', } function AppContent() { // Hooks const { conversations, setConversations, activeId, setActiveId, refreshConversations, createNewChat, deleteChat, updateTitle, } = useConversations() const { messages, setMessages, isThinking, setIsThinking, statusText, setStatusText, sendMessage, } = useMessages({ activeId, refreshConversations }) const { documents, selectedDocuments, useRag, setUseRag, uploading, uploadSuccess, uploadError, uploadDocument, deleteDocument, toggleDocument, clearSelection, } = useDocuments(activeId) const { uploadedImage, imageAnalysis, imageUrl, analyzingImage, clearImage, clearImageState, handleImageUpload, handlePaste, } = useImageUpload() const { fullscreenImage, openFullscreen, closeFullscreen } = useFullscreenImage() // Local UI state const [sidebarOpen, setSidebarOpen] = useState(false) const [sidebarCollapsed, setSidebarCollapsed] = useState(false) const [inputValue, setInputValue] = useState('') const [availableTools, setAvailableTools] = useState(DEFAULT_TOOLS) const [selectedTools, setSelectedTools] = useState([]) const [showTools, setShowTools] = useState(false) const [showSlashCommands, setShowSlashCommands] = useState(false) const toolsRef = useRef(null) const imageInputRef = useRef(null) const fileInputRef = useRef(null) const textareaRef = useRef(null) // Clear image state on conversation switch useEffect(() => { clearImageState() }, [activeId, clearImageState]) // Load available tools on mount useEffect(() => { get('/api/tools') .then((data) => setAvailableTools(data.tools || DEFAULT_TOOLS)) .catch(() => {}) }, []) // Click outside to close tools menu useClickOutside(toolsRef, () => setShowTools(false), [fileInputRef, imageInputRef]) const handleSlashCommand = useCallback(async (cmd, topicOverride) => { if (!activeId) return const topic = topicOverride ?? (inputValue.slice(cmd.id.length + 1).trim()) const userMsg = { id: `user-${Date.now()}`, role: 'user', content: `/${cmd.id} ${topic}` } setMessages((prev) => [...prev, userMsg]) setInputValue('') setIsThinking(true) setStatusText(cmd.id === 'summarize' ? 'Summarizing conversation...' : 'Researching...') setShowTools(false) try { const data = await sendSlashCommand(cmd.id, activeId, topic) const assistantMsg = { id: `assistant-${Date.now()}`, role: 'assistant', content: data.error ? data.error : (data.answer || 'No response'), } setMessages((prev) => [...prev, assistantMsg]) } catch (error) { const assistantMsg = { id: `assistant-${Date.now()}`, role: 'assistant', content: `${error.message}`, } setMessages((prev) => [...prev, assistantMsg]) } setIsThinking(false) setStatusText('') refreshConversations() }, [activeId, inputValue, messages, refreshConversations]) const handleSend = useCallback(() => { // Check if this is a slash command const parsed = parseSlashCommand(inputValue) if (parsed && parsed.matched) { handleSlashCommand(parsed.matched) return } sendMessage({ inputValue, uploadedImage, imageAnalysis, imageUrl, selectedTools, selectedDocuments, useRag, setInputValue, clearImageState, setShowTools, }) }, [inputValue, uploadedImage, imageAnalysis, imageUrl, selectedTools, selectedDocuments, useRag, sendMessage, handleSlashCommand, clearImageState]) const handleKeyDown = useCallback((event) => { if (event.key === 'Enter' && !event.shiftKey) { event.preventDefault() handleSend() } }, [handleSend]) const handleNewChat = useCallback(() => { createNewChat() setSidebarOpen(false) }, [createNewChat]) const handleSelectConversation = useCallback((convId) => { setActiveId(convId) setSidebarOpen(false) }, [setActiveId]) return (
setSidebarCollapsed((prev) => !prev)} onClose={() => setSidebarOpen(false)} />
setSidebarOpen(true)} /> {/* Visually hidden file inputs — uses clip pattern instead of display:none for mobile browser compatibility */} { const file = e.target.files?.[0] if (file) { await uploadDocument(file) e.target.value = '' } }} style={hiddenInputStyle} tabIndex={-1} />