"use client" import { useState, useEffect, useCallback, useRef } from "react" import { toast } from "sonner" import { History as HistoryIcon } from "lucide-react" import { ChatSidebar } from "./chat/ChatSidebar" import { ChatHeader } from "./chat/ChatHeader" import { AmaniMessageList } from "./chat/AmaniMessageList" import { ChatInput } from "./chat/ChatInput" import type { Message, ChatSession, StreamMetadata, SharePlatform, ShareFormatResponse, ShareSheetState } from "./chat/types" // Extend Window interface for OAuth callback declare global { interface Window { handleOAuthCallback?: (platform: SharePlatform, code: string, state: string) => Promise } } export function Chat() { const [messages, setMessages] = useState([]) const [input, setInput] = useState("") const [isLoading, setIsLoading] = useState(false) const [currentSessionId, setCurrentSessionId] = useState(null) const [useHybrid, setUseHybrid] = useState(false) const [chatHistory, setChatHistory] = useState([]) const [showHistory, setShowHistory] = useState(false) const [showSources, setShowSources] = useState(false) const [isResearchMode, setIsResearchMode] = useState(false) const [researchResults, setResearchResults] = useState>({}) const [shareSheet, setShareSheet] = useState(null) const [shareCache, setShareCache] = useState< Record>> >({}) const [autocompleteSuggestions, setAutocompleteSuggestions] = useState([]) const [showAutocomplete, setShowAutocomplete] = useState(false) const [selectedFiles, setSelectedFiles] = useState([]) const [uploadingFiles, setUploadingFiles] = useState(false) const [editingMessageId, setEditingMessageId] = useState(null) const [editingContent, setEditingContent] = useState("") const [regeneratingMessageId, setRegeneratingMessageId] = useState(null) const [isCreatingSession, setIsCreatingSession] = useState(false) const [platformTokens, setPlatformTokens] = useState>({ twitter: null, linkedin: null, facebook: null, whatsapp: null, telegram: null, email: null, threads: null, bluesky: null, tiktok: null }) const autocompleteTimeoutRef = useRef(null) const messagesContainerRef = useRef(null) const messagesEndRef = useRef(null) const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000" const ENABLE_AUTOCOMPLETE = process.env.NEXT_PUBLIC_ENABLE_AUTOCOMPLETE !== "false" // Helper function to get auth headers const getAuthHeaders = useCallback((): Record => { const token = localStorage.getItem("session_token") return token ? { "X-Session-Token": token } : {} }, []) // Token management functions const getStoredToken = useCallback((platform: SharePlatform): string | null => { try { const tokens = JSON.parse(localStorage.getItem("platform_tokens") || "{}") return tokens[platform] || null } catch { return null } }, []) const storeToken = useCallback((platform: SharePlatform, token: string) => { try { const tokens = JSON.parse(localStorage.getItem("platform_tokens") || "{}") tokens[platform] = token localStorage.setItem("platform_tokens", JSON.stringify(tokens)) setPlatformTokens(prev => ({ ...prev, [platform]: token })) } catch (error) { console.error("Failed to store token:", error) } }, []) const clearToken = useCallback((platform: SharePlatform) => { try { const tokens = JSON.parse(localStorage.getItem("platform_tokens") || "{}") delete tokens[platform] localStorage.setItem("platform_tokens", JSON.stringify(tokens)) setPlatformTokens(prev => ({ ...prev, [platform]: null })) } catch (error) { console.error("Failed to clear token:", error) } }, []) const loadStoredTokens = useCallback(() => { try { const tokens = JSON.parse(localStorage.getItem("platform_tokens") || "{}") setPlatformTokens({ twitter: tokens.twitter || null, linkedin: tokens.linkedin || null, facebook: tokens.facebook || null, whatsapp: null, // WhatsApp doesn't use OAuth tokens telegram: null, // Telegram doesn't use OAuth tokens email: null, // Email doesn't use OAuth tokens threads: tokens.threads || null, bluesky: tokens.bluesky || null, tiktok: null // TikTok might not support this flow yet or tokens.tiktok }) } catch (error) { console.error("Failed to load stored tokens:", error) } }, []) const handleAuthCallback = useCallback(async (platform: SharePlatform, code: string, state: string) => { try { const response = await fetch(`${API_BASE_URL}/share/auth/callback`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ platform, code, state }) }) if (!response.ok) { throw new Error("Authentication callback failed") } const data = await response.json() if (data.access_token) { storeToken(platform, data.access_token) toast.success(`Successfully authenticated with ${platform}`) return true } } catch (error) { console.error("Auth callback failed:", error) toast.error(`Authentication failed for ${platform}`) } return false }, [API_BASE_URL, storeToken]) // Load stored tokens on component mount useEffect(() => { loadStoredTokens() // Make handleAuthCallback available globally for OAuth redirects window.handleOAuthCallback = handleAuthCallback // Check for OAuth callback parameters in URL const urlParams = new URLSearchParams(window.location.search) const code = urlParams.get('code') const state = urlParams.get('state') const platform = urlParams.get('platform') if (code && state && platform) { // Handle OAuth callback handleAuthCallback(platform as SharePlatform, code, state).then(() => { // Clean up URL params window.history.replaceState({}, document.title, window.location.pathname) }) } return () => { delete window.handleOAuthCallback } }, [loadStoredTokens, handleAuthCallback]) // OAuth flow functions const initiateAuth = async (platform: SharePlatform) => { try { setShareSheet(prev => prev ? { ...prev, shareLinkLoading: true, shareError: null } : prev) const response = await fetch(`${API_BASE_URL}/share/auth`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ platform }) }) if (!response.ok) { const error = await response.json() throw new Error(error.detail || "Failed to initiate authentication") } const data = await response.json() if (data.status === "needs_auth" && data.auth_url) { // Open OAuth URL in popup or new window const authWindow = window.open( data.auth_url, `auth-${platform}`, "width=600,height=700,scrollbars=yes,resizable=yes" ) if (authWindow) { // Listen for auth completion const checkClosed = setInterval(() => { if (authWindow.closed) { clearInterval(checkClosed) // Check if auth was successful by trying to get user info checkAuthStatus(platform) } }, 1000) } } else if (data.status === "authenticated") { // Already authenticated if (data.user_info?.access_token) { storeToken(platform, data.user_info.access_token) } toast.success(`Already authenticated with ${platform}`) } setShareSheet(prev => prev ? { ...prev, shareLinkLoading: false } : prev) } catch (error) { console.error("Failed to initiate auth:", error) setShareSheet(prev => prev ? { ...prev, shareLinkLoading: false, shareError: error instanceof Error ? error.message : "Authentication failed" } : prev) } } const checkAuthStatus = async (platform: SharePlatform) => { try { // Try to get user info to check if authenticated const response = await fetch(`${API_BASE_URL}/share/auth/status`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ platform }) }) if (response.ok) { const data = await response.json() if (data.status === "authenticated" && data.user_info?.access_token) { storeToken(platform, data.user_info.access_token) toast.success(`Successfully authenticated with ${platform}`) return true } else { // Clear token if not authenticated clearToken(platform) } } else { clearToken(platform) } } catch (error) { console.error("Failed to check auth status:", error) clearToken(platform) } return false } const scrollMessagesToBottom = useCallback( (behavior: ScrollBehavior = "smooth") => { requestAnimationFrame(() => { if (messagesContainerRef.current) { messagesContainerRef.current.scrollTo({ top: messagesContainerRef.current.scrollHeight, behavior }) } messagesEndRef.current?.scrollIntoView({ behavior, block: "end" }) }) }, [] ) useEffect(() => { scrollMessagesToBottom(messages.length < 3 ? "instant" : "smooth") }, [messages, isLoading, scrollMessagesToBottom]) const findPreviousUserPrompt = useCallback( (messageId: string) => { const index = messages.findIndex((msg) => msg.id === messageId) if (index === -1) return undefined for (let i = index - 1; i >= 0; i--) { if (messages[i].role === "user") { return messages[i].content } } return undefined }, [messages] ) const loadChatHistory = useCallback(async () => { try { const headers: Record = { "Content-Type": "application/json", ...getAuthHeaders() } // Use cache API route instead of direct backend call const response = await fetch(`/api/cache/sessions`, { headers }) if (response.ok) { const sessions = await response.json() setChatHistory(sessions) // Log cache hit/miss for debugging const cacheStatus = response.headers.get("X-Cache") if (cacheStatus) { console.log(`Sessions cache: ${cacheStatus}`) } } } catch (error) { console.error("Failed to load chat history:", error) } }, [getAuthHeaders]) const createNewSession = useCallback(async (firstMessage?: string) => { let title = "New Chat" if (firstMessage) { const content = firstMessage.trim() if (content.length <= 50) { title = content } else { title = content.substring(0, 50).split(' ').slice(0, -1).join(' ') + "..." } } setIsCreatingSession(true) try { const headers: Record = { "Content-Type": "application/json", ...getAuthHeaders() } const response = await fetch(`${API_BASE_URL}/api/v1/chat/sessions`, { method: "POST", headers, body: JSON.stringify({ title }) }) if (response.ok) { const session = await response.json() setCurrentSessionId(session.id) setMessages([]) // Invalidate sessions cache since we created a new one try { const invalidateHeaders: Record = { ...getAuthHeaders() } await fetch(`/api/cache/sessions`, { method: "DELETE", headers: invalidateHeaders }) } catch (err) { console.error("Failed to invalidate sessions cache:", err) } loadChatHistory() return session.id } } catch (error) { console.error("Failed to create session:", error) } finally { setIsCreatingSession(false) } return null }, [getAuthHeaders, API_BASE_URL, loadChatHistory]) // Load chat history on component mount useEffect(() => { loadChatHistory() }, [loadChatHistory]) // Autocomplete functionality const fetchAutocomplete = useCallback(async (query: string) => { if (!ENABLE_AUTOCOMPLETE || !query.trim() || query.length < 2) { setAutocompleteSuggestions([]) setShowAutocomplete(false) return } try { const response = await fetch( `${API_BASE_URL}/api/autocomplete?q=${encodeURIComponent(query)}&max_results=5&location=Kenya&language=en` ) if (response.ok) { const data = await response.json() const suggestions = data.suggestions?.map((s: { text: string }) => s.text) || [] setAutocompleteSuggestions(suggestions) setShowAutocomplete(suggestions.length > 0) } } catch (error) { console.error("Failed to fetch autocomplete:", error) setAutocompleteSuggestions([]) setShowAutocomplete(false) } }, [API_BASE_URL, ENABLE_AUTOCOMPLETE]) // Debounced autocomplete useEffect(() => { if (!ENABLE_AUTOCOMPLETE) { setAutocompleteSuggestions([]) setShowAutocomplete(false) return } if (autocompleteTimeoutRef.current) { clearTimeout(autocompleteTimeoutRef.current) } if (input.trim().length >= 2) { autocompleteTimeoutRef.current = setTimeout(() => { fetchAutocomplete(input) }, 300) } else { setAutocompleteSuggestions([]) setShowAutocomplete(false) } return () => { if (autocompleteTimeoutRef.current) { clearTimeout(autocompleteTimeoutRef.current) } } }, [input, fetchAutocomplete, ENABLE_AUTOCOMPLETE]) const loadSession = async (sessionId: string) => { try { const headers: Record = { "Content-Type": "application/json", ...getAuthHeaders() } // Use cache API route instead of direct backend call const response = await fetch(`/api/cache/sessions/${sessionId}/messages`, { headers }) if (response.ok) { const sessionMessages = await response.json() setMessages(sessionMessages) setCurrentSessionId(sessionId) setShowHistory(false) // Log cache hit/miss for debugging const cacheStatus = response.headers.get("X-Cache") if (cacheStatus) { console.log(`Session ${sessionId} messages cache: ${cacheStatus}`) } } } catch (error) { console.error("Failed to load session:", error) } } const uploadFiles = useCallback(async (sessionId: string, files: File[]): Promise => { const attachmentIds: string[] = [] setUploadingFiles(true) try { for (const file of files) { const formData = new FormData() formData.append("file", file) const headers: Record = { ...getAuthHeaders() } const response = await fetch(`${API_BASE_URL}/api/v1/chat/sessions/${sessionId}/attachments`, { method: "POST", headers, body: formData, }) if (response.ok) { const data = await response.json() attachmentIds.push(data.attachment.id) } else { const error = await response.json() throw new Error(error.detail || "Failed to upload file") } } } finally { setUploadingFiles(false) } return attachmentIds }, [getAuthHeaders, API_BASE_URL]) const sendMessage = useCallback(async (content: string) => { if (!content.trim() && selectedFiles.length === 0) return if (isLoading || isCreatingSession || uploadingFiles) return setIsLoading(true) try { let sessionId = currentSessionId if (!sessionId) { sessionId = await createNewSession(content.trim() || "File upload") if (!sessionId) { setIsLoading(false) return } } // Upload files first if any let attachmentIds: string[] = [] let attachmentMetadata: Message["attachments"] = [] if (selectedFiles.length > 0) { try { attachmentIds = await uploadFiles(sessionId, selectedFiles) // Fetch attachment details to display immediately try { const headers: Record = { "Content-Type": "application/json", ...getAuthHeaders() } const attachmentPromises = attachmentIds.map(id => fetch(`${API_BASE_URL}/api/v1/chat/sessions/${sessionId}/attachments/${id}`, { headers }) .then(res => res.ok ? res.json() : null) .catch(() => null) ) const attachmentResults = await Promise.all(attachmentPromises) attachmentMetadata = attachmentResults .filter(att => att !== null) .map(att => ({ id: att.id, filename: att.filename, file_type: att.file_type, file_size: att.file_size, uploaded_at: att.uploaded_at, processed: att.processed || false, cloudinary_url: att.cloudinary_url })) } catch (error) { console.error("Failed to fetch attachment metadata:", error) } setSelectedFiles([]) // Clear selected files after upload } catch (error) { toast.error(error instanceof Error ? error.message : "Failed to upload files") setIsLoading(false) return } } const userMessage: Message = { id: Date.now().toString(), session_id: sessionId, role: "user", content: content.trim() || `Uploaded ${selectedFiles.length} file(s)`, created_at: new Date().toISOString(), saved: false, attachments: attachmentMetadata.length > 0 ? attachmentMetadata : undefined } setMessages(prev => [...prev, userMessage]) setInput("") setIsLoading(true) try { let response; if (isResearchMode) { const headers: Record = { "Content-Type": "application/x-www-form-urlencoded", ...getAuthHeaders() } response = await fetch(`${API_BASE_URL}/research/analyze-legal-query`, { method: "POST", headers, body: new URLSearchParams({ query: content.trim() }) }) } else if (useHybrid) { const headers: Record = { "Content-Type": "application/json", ...getAuthHeaders() } response = await fetch(`${API_BASE_URL}/stream/query`, { method: "POST", headers, body: JSON.stringify({ query: content.trim(), top_k: 5, include_sources: true, stream: true }) }) } else { const headers: Record = { "Content-Type": "application/json", ...getAuthHeaders() } response = await fetch(`${API_BASE_URL}/api/v1/chat/sessions/${sessionId}/messages`, { method: "POST", headers, body: JSON.stringify({ content: content.trim(), role: "user", stream: true, attachment_ids: attachmentIds.length > 0 ? attachmentIds : undefined }) }) } if (response.ok) { if (isResearchMode) { const data = await response.json() const analysis = data.analysis || {} const queryInterpretation = analysis.query_interpretation || 'Analyzing your legal question...' const applicableLaws = Array.isArray(analysis.applicable_laws) ? analysis.applicable_laws.join('\n- ') : (analysis.applicable_laws || 'Researching relevant Kenyan laws...') const legalAnalysis = analysis.legal_analysis || 'Conducting detailed legal analysis...' const practicalGuidance = analysis.practical_guidance || {} const guidanceSteps = Array.isArray(practicalGuidance.steps) ? practicalGuidance.steps : (typeof practicalGuidance === 'string' ? [practicalGuidance] : ['Review the analysis above']) const guidanceSummary = practicalGuidance.summary || '' const additionalConsiderations = Array.isArray(analysis.additional_considerations) ? analysis.additional_considerations : (typeof analysis.additional_considerations === 'string' ? [analysis.additional_considerations] : ['Review all applicable laws']) const researchProcess = analysis.research_process || {} const researchContent = ` ## 📋 Legal Research Analysis **Your Question:** ${data.original_query || content.trim()} --- ### 🔍 Query Understanding ${queryInterpretation} --- ### 📖 Applicable Laws & Provisions ${Array.isArray(analysis.applicable_laws) ? analysis.applicable_laws.map((law: string, idx: number) => `${idx + 1}. ${law}`).join('\n') : `- ${applicableLaws}`} --- ### ⚖️ Legal Analysis ${legalAnalysis} --- ### 🛠️ Practical Guidance ${guidanceSummary ? `**Summary:** ${guidanceSummary}\n\n` : ''}**Action Steps:** ${guidanceSteps.map((step: string, idx: number) => `${idx + 1}. ${step}`).join('\n')} --- ### ⚠️ Important Considerations ${additionalConsiderations.map((consideration: string) => `- ${consideration}`).join('\n')} --- ${researchProcess.steps_completed || researchProcess.sources_consulted ? `### 📊 Research Process - **Steps Completed:** ${researchProcess.steps_completed || 0} - **Actions Taken:** ${researchProcess.actions_taken || 0} - **Sources Consulted:** ${researchProcess.sources_consulted || 0} ${researchProcess.tools_used && researchProcess.tools_used.length > 0 ? `- **Tools Used:** ${researchProcess.tools_used.join(', ')}` : ''} ---\n` : ''}### 📝 Disclaimer *This analysis is for informational purposes only and does not constitute legal advice. Please consult with a qualified legal professional for advice specific to your situation.* `.trim() const assistantMessage: Message = { id: Date.now().toString(), session_id: sessionId, role: "assistant", content: researchContent, created_at: new Date().toISOString(), model_used: data.model_used || "gemini-research", saved: true } setMessages(prev => [...prev, assistantMessage]) setResearchResults(prev => ({ ...prev, [assistantMessage.id]: data })) } else { const assistantMessageId = Date.now().toString() const assistantMessage: Message = { id: assistantMessageId, session_id: sessionId, role: "assistant", content: "", created_at: new Date().toISOString(), model_used: "streaming", saved: false } setMessages(prev => [...prev, assistantMessage]) const reader = response.body?.getReader() const decoder = new TextDecoder() let accumulatedContent = "" const metadata: StreamMetadata = {} if (reader) { try { while (true) { const { done, value } = await reader.read() if (done) break const chunk = decoder.decode(value, { stream: true }) const lines = chunk.split('\n') for (const line of lines) { if (line.startsWith('data: ')) { const data = line.slice(6) if (data === '[DONE]') continue if (data.startsWith('[DONE]')) { try { const parsed = JSON.parse(data.slice(7)) metadata.sources = parsed.sources || [] metadata.token_count = parsed.retrieved_chunks metadata.model_used = parsed.model_used || (parsed.hybrid_used ? 'hybrid' : 'standard') setMessages(prev => prev.map(msg => msg.id === assistantMessageId ? { ...msg, content: accumulatedContent, token_count: metadata.token_count, model_used: metadata.model_used, sources: metadata.sources, saved: true } : msg )) break } catch { // Fall through } } if (useHybrid && !data.startsWith('{') && !data.startsWith('[')) { accumulatedContent += data setMessages(prev => prev.map(msg => msg.id === assistantMessageId ? { ...msg, content: accumulatedContent } : msg )) continue } try { const parsed = JSON.parse(data) if (parsed.type === 'sources') { metadata.sources = parsed.sources metadata.token_count = parsed.retrieved_chunks metadata.model_used = parsed.model_used } else if (parsed.type === 'content') { accumulatedContent += parsed.content setMessages(prev => prev.map(msg => msg.id === assistantMessageId ? { ...msg, content: accumulatedContent } : msg )) } else if (parsed.type === 'done') { setMessages(prev => prev.map(msg => msg.id === assistantMessageId ? { ...msg, content: parsed.full_answer || accumulatedContent, token_count: metadata.token_count, model_used: metadata.model_used, sources: metadata.sources, structured_response: parsed.structured_data, saved: true } : msg )) break } else if (parsed.type === 'error') { console.error('Streaming error:', parsed.error) setMessages(prev => prev.map(msg => msg.id === userMessage.id ? { ...msg, failed: true, originalQuery: content.trim() } : msg )) setMessages(prev => prev.map(msg => msg.id === assistantMessageId ? { ...msg, content: `Error: ${parsed.error}`, model_used: 'error', saved: true, failed: true, originalQuery: content.trim() } : msg )) break } } catch { continue } } } } } finally { reader.releaseLock() } } } // Invalidate session messages cache since new message was added try { const invalidateHeaders: Record = { ...getAuthHeaders() } await Promise.all([ fetch(`/api/cache/sessions/${sessionId}/messages`, { method: "DELETE", headers: invalidateHeaders }), // Also invalidate sessions list to capture any auto-generated titles fetch(`/api/cache/sessions`, { method: "DELETE", headers: invalidateHeaders }) ]) } catch (err) { console.error("Failed to invalidate cache:", err) } loadChatHistory() } else { setMessages(prev => prev.map(msg => msg.id === userMessage.id ? { ...msg, failed: true, originalQuery: content.trim() } : msg )) const errorMessage: Message = { id: (Date.now() + 1).toString(), session_id: sessionId, role: "assistant", content: isResearchMode ? "Sorry, I encountered an error processing your legal research request." : "Sorry, I encountered an error processing your request.", created_at: new Date().toISOString(), failed: true, originalQuery: content.trim() } setMessages(prev => [...prev, errorMessage]) } } catch (error) { console.error("Failed to send message:", error) setMessages(prev => prev.map(msg => msg.id === userMessage.id ? { ...msg, failed: true, originalQuery: content.trim() } : msg )) const errorMessage: Message = { id: (Date.now() + 1).toString(), session_id: sessionId, role: "assistant", content: isResearchMode ? "Sorry, I couldn't connect to the research service. Please try again." : "Sorry, I couldn't connect to the server. Please try again.", created_at: new Date().toISOString(), failed: true, originalQuery: content.trim() } setMessages(prev => [...prev, errorMessage]) } finally { setIsLoading(false) } } catch (error) { console.error("Failed to process message:", error) setIsLoading(false) } }, [currentSessionId, selectedFiles, isResearchMode, useHybrid, getAuthHeaders, API_BASE_URL, loadChatHistory, createNewSession, uploadFiles, isLoading, isCreatingSession, uploadingFiles]) const submitFeedback = async (messageId: string, feedbackType: "like" | "dislike") => { try { const headers: Record = { "Content-Type": "application/json", ...getAuthHeaders() } const response = await fetch(`${API_BASE_URL}/api/v1/chat/feedback`, { method: "POST", headers, body: JSON.stringify({ message_id: messageId, feedback_type: feedbackType === "like" ? "positive" : "negative" }) }) if (response.ok) { setMessages(prev => prev.map(msg => msg.id === messageId ? { ...msg, feedback_type: feedbackType } : msg )) } } catch (error) { console.error("Failed to submit feedback:", error) } } const copyToClipboard = async (text: string) => { try { await navigator.clipboard.writeText(text) toast.success("Message copied to clipboard!") } catch (error) { console.error("Failed to copy to clipboard:", error) toast.error("Failed to copy to clipboard") } } const copyFailedQuery = async (message: Message) => { const query = message.originalQuery || message.content try { await navigator.clipboard.writeText(query) toast.success("Query copied to clipboard!") } catch (error) { console.error("Failed to copy query:", error) toast.error("Failed to copy query") } } const editFailedQuery = (message: Message) => { const query = message.originalQuery || message.content setInput(query) setTimeout(() => { const inputElement = document.querySelector('textarea') as HTMLTextAreaElement inputElement?.focus() }, 100) } const resendFailedQuery = async (message: Message) => { const query = message.originalQuery || (message.role === "user" ? message.content : "") if (!query.trim()) return setMessages(prev => prev.filter(msg => msg.id !== message.id && !(msg.failed && (msg.originalQuery === query || (msg.role === "user" && msg.content === query))) )) await sendMessage(query) } const startEditingMessage = (message: Message) => { if (message.role !== "user") return setEditingMessageId(message.id) setEditingContent(message.content) } const cancelEditing = () => { setEditingMessageId(null) setEditingContent("") } const saveEditedMessage = async (messageId: string) => { if (!editingContent.trim()) return const messageIndex = messages.findIndex(m => m.id === messageId) if (messageIndex === -1) return setMessages(prev => prev.slice(0, messageIndex)) setEditingMessageId(null) setEditingContent("") await sendMessage(editingContent) } const regenerateMessage = async (messageId: string) => { const messageIndex = messages.findIndex(m => m.id === messageId) if (messageIndex === -1 || messages[messageIndex].role !== "assistant") return let userMessageIndex = -1 let userMessageContent = "" for (let i = messageIndex - 1; i >= 0; i--) { if (messages[i].role === "user") { userMessageIndex = i userMessageContent = messages[i].content break } } if (userMessageIndex === -1) return setMessages(prev => prev.slice(0, userMessageIndex + 1)) setRegeneratingMessageId(messageId) await sendMessage(userMessageContent) setRegeneratingMessageId(null) } const deleteSession = async (sessionId: string) => { if (!confirm("Are you sure you want to delete this chat session? This action cannot be undone.")) { return } try { const headers: Record = { "Content-Type": "application/json", ...getAuthHeaders() } const response = await fetch(`${API_BASE_URL}/chat/sessions/${sessionId}`, { method: "DELETE", headers }) if (response.ok) { setChatHistory(prev => prev.filter(session => session.id !== sessionId)) if (currentSessionId === sessionId) { setCurrentSessionId(null) setMessages([]) } // Invalidate both sessions list and specific session messages cache try { const invalidateHeaders: Record = { ...getAuthHeaders() } await Promise.all([ fetch(`/api/cache/sessions`, { method: "DELETE", headers: invalidateHeaders }), fetch(`/api/cache/sessions/${sessionId}/messages`, { method: "DELETE", headers: invalidateHeaders }) ]) } catch (err) { console.error("Failed to invalidate cache:", err) } } } catch (error) { console.error("Failed to delete session:", error) } } const renameSession = async (sessionId: string, newTitle: string) => { try { const headers: Record = { "Content-Type": "application/json", ...getAuthHeaders() } const response = await fetch(`${API_BASE_URL}/chat/sessions/${sessionId}`, { method: "PATCH", headers, body: JSON.stringify({ title: newTitle }) }) if (response.ok) { // Update local state setChatHistory(prev => prev.map(s => s.id === sessionId ? { ...s, title: newTitle } : s )) // Invalidate sessions cache try { const invalidateHeaders: Record = { ...getAuthHeaders() } await fetch(`/api/cache/sessions`, { method: "DELETE", headers: invalidateHeaders }) } catch (err) { console.error("Failed to invalidate sessions cache:", err) } toast.success("Chat renamed") } } catch (error) { console.error("Failed to rename session:", error) toast.error("Failed to rename chat") } } const shareChat = async (options: { silent?: boolean } = {}) => { if (!currentSessionId) return null try { const headers: Record = { "Content-Type": "application/json", ...getAuthHeaders() } const response = await fetch( `${API_BASE_URL}/api/v1/chat/share?session_id=${encodeURIComponent(currentSessionId)}`, { method: "POST", headers } ) if (response.ok) { const data = await response.json() const shareUrl = `${window.location.origin}${data.share_link}` if (!options.silent) { await navigator.clipboard.writeText(shareUrl) toast.success("Share link copied to clipboard!") } return shareUrl } else { if (!options.silent) { toast.error("Failed to generate share link") } } } catch (error) { console.error("Failed to generate share link:", error) if (!options.silent) { toast.error("Failed to generate share link") } } return null } const generatePDF = async (messageId: string) => { const researchData = researchResults[messageId] if (!researchData) { toast.error("Research data not found") return } try { const response = await fetch(`${API_BASE_URL}/research/generate-pdf-report`, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ analysis_results: JSON.stringify(researchData), report_title: "Legal Research Report" }) }) if (response.ok) { const blob = await response.blob() const url = window.URL.createObjectURL(blob) const a = document.createElement('a') a.href = url a.download = 'legal_research_report.pdf' document.body.appendChild(a) a.click() window.URL.revokeObjectURL(url) document.body.removeChild(a) toast.success("PDF report downloaded!") } else { toast.error("Failed to generate PDF report") } } catch (error) { console.error("Error generating PDF:", error) toast.error("Error generating PDF report") } } const generateWord = async (messageId: string) => { const researchData = researchResults[messageId] if (!researchData) { toast.error("Research data not found") return } try { const response = await fetch(`${API_BASE_URL}/research/generate-word-report`, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ analysis_results: JSON.stringify(researchData), report_title: "Legal Research Report" }) }) if (response.ok) { const blob = await response.blob() const url = window.URL.createObjectURL(blob) const a = document.createElement('a') a.href = url a.download = 'legal_research_report.docx' document.body.appendChild(a) a.click() window.URL.revokeObjectURL(url) document.body.removeChild(a) toast.success("Word document downloaded!") } else { toast.error("Failed to generate Word document") } } catch (error) { console.error("Error generating Word document:", error) toast.error("Error generating Word document") } } const sanitizeForSharing = (content: string) => { if (!content) return "" return content.replace(/<[^>]+>/g, "").trim() } const ensureSharePreview = async (message: Message, platform: SharePlatform) => { const cached = shareCache[message.id]?.[platform] if (cached) return cached try { setShareSheet((prev) => prev?.messageId === message.id ? { ...prev, isLoading: true, shareError: null, success: null, shareLink: null } : prev ) const response = await fetch(`${API_BASE_URL}/share/format`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ answer: sanitizeForSharing(message.content), sources: message.sources || [], platform, query: findPreviousUserPrompt(message.id), include_hashtags: true }) }) if (!response.ok) { const detail = await response.text() throw new Error(detail || "Unable to format response") } const data: ShareFormatResponse = await response.json() setShareCache((prev) => ({ ...prev, [message.id]: { ...(prev[message.id] || {}), [platform]: data } })) setShareSheet((prev) => prev?.messageId === message.id ? { ...prev, preview: data, isLoading: false } : prev ) return data } catch (error) { console.error("Failed to format post:", error) setShareSheet((prev) => prev?.messageId === message.id ? { ...prev, isLoading: false, shareError: error instanceof Error ? error.message : "Unable to format this response right now." } : prev ) return null } } const openShareSheet = (message: Message, platform: SharePlatform = "twitter") => { if (message.role !== "assistant") return if (shareSheet?.messageId === message.id && shareSheet.platform === platform) { setShareSheet(null) return } const cached = shareCache[message.id]?.[platform] setShareSheet({ messageId: message.id, platform, preview: cached, isLoading: !cached, shareLink: null, shareError: null, success: null }) if (!cached) { ensureSharePreview(message, platform) } } const changeSharePlatform = (message: Message, platform: SharePlatform) => { if (!shareSheet || shareSheet.messageId !== message.id) { openShareSheet(message, platform) return } const cached = shareCache[message.id]?.[platform] setShareSheet({ ...shareSheet, platform, preview: cached, isLoading: !cached, shareLink: null, shareError: null, success: null }) if (!cached) { ensureSharePreview(message, platform) } } const copyShareContent = async () => { if (!shareSheet?.preview) return const preview = shareSheet.preview const text = Array.isArray(preview.content) ? preview.content.join("\n\n") : preview.content try { await navigator.clipboard.writeText(text) toast.success("Formatted answer copied!") } catch (error) { console.error("Failed to copy share content:", error) toast.error("Unable to copy content") } } const openShareIntent = async (message: Message) => { if (!shareSheet) return const preview = shareSheet.preview ?? (await ensureSharePreview(message, shareSheet.platform)) if (!preview) return try { setShareSheet((prev) => prev ? { ...prev, shareLinkLoading: true, shareError: null, success: null } : prev ) const sessionLink = await shareChat({ silent: true }) const response = await fetch(`${API_BASE_URL}/share/generate-link`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ platform: shareSheet.platform, content: preview.content, url: sessionLink || undefined }) }) if (!response.ok) { const detail = await response.text() throw new Error(detail || "Unable to open share dialog") } const data = await response.json() setShareSheet((prev) => prev ? { ...prev, shareLink: data.share_url, shareLinkLoading: false, success: `Share dialog ready for ${shareSheet.platform}` } : prev ) window.open(data.share_url, "_blank", "noopener,noreferrer") } catch (error) { console.error("Failed to open share link:", error) setShareSheet((prev) => prev ? { ...prev, shareLinkLoading: false, shareError: error instanceof Error ? error.message : "Unable to open the platform share dialog." } : prev ) } } const generateShareImage = async (message: Message) => { if (!shareSheet) return const preview = shareSheet.preview ?? (await ensureSharePreview(message, shareSheet.platform)) if (!preview) return try { setShareSheet((prev) => prev ? { ...prev, generatingImage: true, shareError: null, success: null } : prev ) const text = Array.isArray(preview.content) ? preview.content.join("\n\n") : preview.content const title = `AmaniQuery Insight` const response = await fetch(`${API_BASE_URL}/share/generate-image-from-post`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ post_content: text, query: title, color_scheme: "professional" }) }) if (!response.ok) { const detail = await response.text() throw new Error(detail || "Unable to generate image") } const data = await response.json() // Download the image const imageResponse = await fetch(`data:image/png;base64,${data.image_base64}`) const blob = await imageResponse.blob() const url = URL.createObjectURL(blob) const a = document.createElement('a') a.href = url a.download = `amaniquery-${message.id}.png` document.body.appendChild(a) a.click() document.body.removeChild(a) URL.revokeObjectURL(url) setShareSheet((prev) => prev ? { ...prev, generatingImage: false, success: "Image downloaded successfully!", shareError: null } : prev ) } catch (error) { console.error("Failed to generate image:", error) setShareSheet((prev) => prev ? { ...prev, generatingImage: false, shareError: error instanceof Error ? error.message : "Unable to generate image." } : prev ) } } const postDirectly = async (message: Message) => { if (!shareSheet) return const preview = shareSheet.preview ?? (await ensureSharePreview(message, shareSheet.platform)) if (!preview) return // Check if user is authenticated const accessToken = getStoredToken(shareSheet.platform) if (!accessToken) { setShareSheet(prev => prev ? { ...prev, shareError: `Please authenticate with ${shareSheet.platform} first. Click the authenticate button below.` } : prev) return } try { setShareSheet((prev) => prev ? { ...prev, posting: true, shareError: null, success: null } : prev ) const response = await fetch(`${API_BASE_URL}/share/post`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ platform: shareSheet.platform, content: preview.content, message_id: message.id, access_token: accessToken }) }) if (!response.ok) { const detail = await response.text() throw new Error(detail || "Unable to post") } const data = await response.json() console.log('Post response:', data) setShareSheet((prev) => prev ? { ...prev, posting: false, success: data.message || `Posted to ${shareSheet.platform} successfully`, shareError: null } : prev ) toast.success(data.message || `Posted to ${shareSheet.platform} successfully`) } catch (error) { console.error("Failed to post to platform:", error) setShareSheet((prev) => prev ? { ...prev, posting: false, shareError: error instanceof Error ? error.message : "Unable to post automatically. Try the share dialog instead." } : prev ) } } return (
createNewSession()} onCloseHistory={() => setShowHistory(false)} />
setShowHistory(!showHistory)} onShare={() => shareChat()} HistoryIcon={HistoryIcon} />
setShareSheet(null)} onChangeSharePlatform={changeSharePlatform} onCopyShareContent={copyShareContent} onOpenShareIntent={openShareIntent} onPostDirectly={postDirectly} onGenerateShareImage={generateShareImage} onAuthenticatePlatform={initiateAuth} platformTokens={platformTokens} onCopyFailedQuery={copyFailedQuery} onEditFailedQuery={editFailedQuery} onResendFailedQuery={resendFailedQuery} />
{ setIsResearchMode(!isResearchMode) setUseHybrid(false) }} onToggleHybrid={() => { setUseHybrid(!useHybrid) setIsResearchMode(false) }} />
) }