import React from 'react'; import { createPortal } from 'react-dom'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import { Brain, Send, Sparkles, User, Loader2, ShieldCheck, Settings, Zap, X, History, Plus, Trash2, ChevronLeft, ChevronRight, MessageSquare, ThumbsUp, ThumbsDown, Copy, RotateCcw, Edit2, Check, Save } from 'lucide-react'; import { GhostButton, ModernCardHeader } from "../ui/settings-ui"; import { Badge } from "../ui/badge"; import { cn } from "@/lib/utils"; import { AnimatePresence, motion } from 'framer-motion'; import { Switch } from "../ui/switch"; import type { WawpDashboardData } from '../../types'; import { Reasoning, ReasoningContent, ReasoningTrigger } from "../ai-elements/reasoning"; import AISettings from "./AISettings"; import { AI_ACTIONS_SCHEMA } from '../../ai-actions-schema'; const WAWPLogo = ({ className, size = 24 }: { className?: string; size?: number }) => ( ); interface Message { id: string; role: 'user' | 'assistant'; content: string; reasoning?: string; rating?: number; timestamp: Date; isThinking?: boolean; } interface ChatSession { id: number; title: string; timestamp: string; } const WELCOME_MESSAGE: Message = { id: 'welcome', role: 'assistant', content: "Hello! I am your Wawp AI Guide. I have access to your current settings, system status, and configurations. How can I help you optimize your automation today?", timestamp: new Date(), rating: 0 }; interface ActionAttrs { value: string | boolean; key: string; label: string; component: 'switch' | 'input' | string; } const ChatAction = ({ action, data, onComplete }: { action: ActionAttrs; data: WawpDashboardData; onComplete: (msg: string) => void }) => { const getRealDbKeyAndValue = () => { if (data && data.features) { const normalizedKey = String(action.key).toLowerCase().replace(/^wawp_/, '').replace(/_enabled$/, '').replace(/_popup$/, ''); for (const k of Object.keys(data.features)) { const feat = data.features[k as keyof typeof data.features]; if (feat && typeof feat === 'object') { const normalizedFeatKey = String(k).toLowerCase().replace(/^wawp_/, '').replace(/_enabled$/, ''); const normalizedOption = String(feat.option || '').toLowerCase().replace(/^wawp_/, '').replace(/_enabled$/, ''); if (normalizedKey === normalizedFeatKey || normalizedKey === normalizedOption || normalizedKey.includes(normalizedFeatKey) || normalizedFeatKey.includes(normalizedKey)) { return { dbKey: feat.option || k, dbValue: feat.enabled }; } } } } return { dbKey: action.key, dbValue: action.value }; }; const { dbKey, dbValue } = getRealDbKeyAndValue(); const getIsInitialChecked = () => { return dbValue === true || dbValue === 'true' || dbValue === 1 || dbValue === '1' || dbValue === 'yes'; }; const [checked, setChecked] = React.useState(getIsInitialChecked()); const [inputValue, setInputValue] = React.useState(String(action.value ?? '')); const [isSaving, setIsSaving] = React.useState(false); const [isLoadingValue, setIsLoadingValue] = React.useState(action.component !== 'switch'); // Fetch the actual current value from the database for input-type actions React.useEffect(() => { if (action.component === 'switch') return; const fetchRealValue = async () => { try { const response = await fetch(`${data.global.restUrl}/settings`, { headers: { 'X-WP-Nonce': data.global.wpRestNonce } }); const res = await response.json(); if (res.success && res.data) { const realVal = res.data[dbKey] ?? res.data[action.key]; if (realVal !== undefined && realVal !== null) { setInputValue(String(realVal)); } } } catch (e: unknown) { console.error("Failed to fetch real setting value", e); } finally { setIsLoadingValue(false); } }; fetchRealValue(); }, [action.component, action.key, dbKey, data.global.restUrl, data.global.wpRestNonce]); const formatPayloadValue = (isChecked: boolean) => { if (typeof dbValue === 'number') { return isChecked ? 1 : 0; } else if (typeof dbValue === 'boolean') { return isChecked; } else if (dbValue === 'yes' || dbValue === 'no') { return isChecked ? 'yes' : 'no'; } else if (dbValue === '1' || dbValue === '0') { return isChecked ? '1' : '0'; } else if (dbValue === 'true' || dbValue === 'false') { return isChecked ? 'true' : 'false'; } if (dbKey.includes('enable') || dbKey.includes('active')) { return isChecked ? 1 : 0; } return isChecked ? 'yes' : 'no'; }; const handleSaveWithNewChecked = async (newChecked: boolean) => { setIsSaving(true); const newVal = formatPayloadValue(newChecked); try { const response = await fetch(`${data.global.restUrl}/settings`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': data.global.wpRestNonce }, body: JSON.stringify({ settings: { [dbKey]: newVal } }) }); const res = await response.json(); if (res.success) { const globalData = (window as unknown as { wawpDashboardData: WawpDashboardData }).wawpDashboardData; if (globalData && globalData.features) { const features = globalData.features as unknown as Record; for (const k of Object.keys(features)) { const feat = features[k]; if (feat && typeof feat === 'object' && ((feat as Record).option === dbKey || k === dbKey)) { features[k] = { ...(feat as Record), enabled: newVal }; } } } onComplete(`✅ Successfully updated ${action.label}`); } } catch (e: unknown) { console.error("Action save failed", e); } finally { setIsSaving(false); } }; const toggleVal = (newChecked: boolean) => { setChecked(newChecked); handleSaveWithNewChecked(newChecked); }; const handleSave = async () => { setIsSaving(true); let finalValue: string | number | boolean = inputValue; if (action.component === 'switch') { finalValue = formatPayloadValue(checked); } try { const response = await fetch(`${data.global.restUrl}/settings`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': data.global.wpRestNonce }, body: JSON.stringify({ settings: { [dbKey]: finalValue } }) }); const res = await response.json(); if (res.success) { const globalData = (window as unknown as { wawpDashboardData: WawpDashboardData }).wawpDashboardData; if (globalData && globalData.features) { const features = globalData.features as unknown as Record; for (const k of Object.keys(features)) { const feat = features[k]; if (feat && typeof feat === 'object' && ((feat as Record).option === dbKey || k === dbKey)) { features[k] = { ...(feat as Record), enabled: finalValue }; } } } onComplete(`✅ Successfully updated ${action.label}`); } } catch (e: unknown) { console.error("Action save failed", e); } finally { setIsSaving(false); } }; return ( {action.label} {action.component === 'switch' && ( )} {action.component !== 'switch' && ( {action.label} {isLoadingValue ? ( Loading current value... ) : ( setInputValue(e.target.value)} placeholder={`Enter ${action.label}...`} /> )} )} {isSaving ? : } Confirm & Save Change ); }; const ChatNotificationAction = ({ attrs, data, onComplete }: { attrs: Record; data: WawpDashboardData; onComplete: (msg: string) => void }) => { const [isSaving, setIsSaving] = React.useState(false); const handleCreate = async () => { setIsSaving(true); try { const response = await fetch(`${data.global.restUrl}/notifications/create`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': data.global.wpRestNonce }, body: JSON.stringify({ rule: attrs }) }); const res = await response.json(); if (res.success) { onComplete(`🎉 Successfully created new notification rule: **${res.name}**`); } else { onComplete(`❌ Failed to create rule: ${res.message || 'Unknown error'}`); } } catch (e: unknown) { console.error("Rule creation failed", e); onComplete(`❌ Request failed during rule creation.`); } finally { setIsSaving(false); } }; return ( Suggested Notification Trigger: {attrs.trigger_key || 'N/A'} Name: {attrs.name || 'Auto Rule'} Channels: {attrs.whatsapp_enabled === '1' ? 'WhatsApp ' : ''} {attrs.email_enabled === '1' ? 'Email' : ''} {attrs.whatsapp_message && ( Message: {attrs.whatsapp_message} )} {isSaving ? : } Approve & Create Notification ); }; // Detect if text contains Arabic characters const isArabic = (text: string): boolean => /[\u0600-\u06FF]/.test(text); export default function AIGlobalAssistant({ data }: { data: WawpDashboardData }) { const [open, setOpen] = React.useState(false); const [view, setView] = React.useState<'chat' | 'settings'>('chat'); const [sidebarOpen, setSidebarOpen] = React.useState(true); const [input, setInput] = React.useState(''); const [messages, setMessages] = React.useState([]); const [sessions, setSessions] = React.useState([]); const [currentSessionId, setCurrentSessionId] = React.useState(null); const [aiSettings, setAiSettings] = React.useState | null>(null); const [isLoading, setIsLoading] = React.useState(false); const [isFetchingHistory, setIsFetchingHistory] = React.useState(false); const [isFetchingSessions, setIsFetchingSessions] = React.useState(false); const [editingId, setEditingId] = React.useState(null); const [editContent, setEditContent] = React.useState(''); const [renamingSessionId, setRenamingSessionId] = React.useState(null); const [renamingTitle, setRenamingTitle] = React.useState(''); const [currentThinkingStep, setCurrentThinkingStep] = React.useState(0); const scrollRef = React.useRef(null); const localIdCounter = React.useRef(0); const streamContentRef = React.useRef(''); const streamReasoningRef = React.useRef(''); const getAssistantIcon = (msgId?: string, content?: string) => { const isAction = msgId?.startsWith('action-') || msgId?.startsWith('notif-') || content?.includes('Successfully created') || content?.includes('Successfully updated') || content?.includes('🎉 Successfully created') || content?.includes('✅ Successfully updated'); if (isAction) { return ; } const provider = aiSettings?.activeProvider; if (!provider || provider === 'free') { return ; } return ( ); }; const thinkingSteps = [ "Analyzing your current request...", "Scanning plugin configuration for context...", "Evaluating available AI actions for your intent...", "Formulating the most efficient response..." ]; React.useEffect(() => { let interval: ReturnType | undefined; if (isLoading) { interval = setInterval(() => { setCurrentThinkingStep(prev => (prev + 1) % thinkingSteps.length); }, 2500); } return () => clearInterval(interval); }, [isLoading, thinkingSteps.length]); const fetchAiSettings = React.useCallback(async () => { try { const response = await fetch(`${data.global.restUrl}/ai-assistant/settings`, { headers: { 'X-WP-Nonce': data.global.wpRestNonce } }); const res = await response.json(); if (res.success) setAiSettings(res.data); } catch (error) { console.error("Failed to fetch AI settings:", error); } }, [data.global.restUrl, data.global.wpRestNonce]); const fetchSessions = React.useCallback(async () => { setIsFetchingSessions(true); try { const response = await fetch(`${data.global.restUrl}/ai-assistant/sessions`, { headers: { 'X-WP-Nonce': data.global.wpRestNonce } }); const res = await response.json(); if (res.success) setSessions(res.data || []); } catch (error) { console.error("Failed to fetch sessions:", error); } finally { setIsFetchingSessions(false); } }, [data.global.restUrl, data.global.wpRestNonce]); const fetchHistory = React.useCallback(async (sessionId: number) => { setIsFetchingHistory(true); try { const response = await fetch(`${data.global.restUrl}/ai-assistant/chats?session_id=${sessionId}`, { headers: { 'X-WP-Nonce': data.global.wpRestNonce } }); const res = await response.json(); if (res.success && res.data) { const history = res.data.map((m: { id: number | string; role: 'user' | 'assistant'; content: string; rating?: string | number; timestamp: string }) => { let reasoning = ''; let cleanContent = m.content.trim(); if (m.role === 'assistant') { const thoughtMatch = cleanContent.match(/([\s\S]*?)<\/thought>/); if (thoughtMatch) { reasoning = thoughtMatch[1].trim(); cleanContent = cleanContent.replace(/[\s\S]*?<\/thought>/, '').trim(); } } return { id: m.id.toString(), role: m.role, content: cleanContent, reasoning: reasoning, rating: typeof m.rating === 'number' ? m.rating : parseInt(String(m.rating || 0)), timestamp: new Date(m.timestamp) }; }); setMessages([WELCOME_MESSAGE, ...history]); } } catch (error) { console.error("Failed to fetch history:", error); } finally { setIsFetchingHistory(false); } }, [data.global.restUrl, data.global.wpRestNonce]); const startNewChat = () => { setView('chat'); setCurrentSessionId(null); setMessages([WELCOME_MESSAGE]); setEditingId(null); }; const selectSession = (sessionId: number) => { setView('chat'); setCurrentSessionId(sessionId); fetchHistory(sessionId); setEditingId(null); }; const deleteSession = async (e: React.MouseEvent, sessionId: number) => { e.stopPropagation(); if (!confirm('Are you sure you want to delete this conversation?')) return; try { await fetch(`${data.global.restUrl}/ai-assistant/sessions/${sessionId}`, { method: 'DELETE', headers: { 'X-WP-Nonce': data.global.wpRestNonce } }); setSessions(prev => prev.filter(s => s.id !== sessionId)); if (currentSessionId === sessionId) startNewChat(); } catch (error) { console.error("Failed to delete session:", error); } }; const renameSession = async (e: React.FormEvent) => { e.preventDefault(); if (!renamingSessionId || !renamingTitle.trim()) return; try { await fetch(`${data.global.restUrl}/ai-assistant/sessions`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': data.global.wpRestNonce }, body: JSON.stringify({ id: renamingSessionId, title: renamingTitle }) }); setSessions(prev => prev.map(s => s.id === renamingSessionId ? { ...s, title: renamingTitle } : s)); setRenamingSessionId(null); } catch (error) { console.error("Failed to rename session:", error); } }; const handleOpen = () => { setOpen(true); fetchSessions(); fetchAiSettings(); if (!currentSessionId && messages.length === 0) { setMessages([WELCOME_MESSAGE]); } }; React.useEffect(() => { if (scrollRef.current) { scrollRef.current.scrollIntoView({ behavior: 'smooth' }); } }, [messages, isLoading, view]); const generateAutoTitle = async (sessionId: number, userMsg: string, aiMsg: string) => { try { const prompt = `Based on this user question and AI answer, generate a very short (max 4 words) title in the SAME language used (Arabic or English). Return ONLY the title text. USER: ${userMsg} AI: ${aiMsg.substring(0, 100)}... Title:`; let generatedTitle = ''; // Try Paid Providers First const PAID_PROVIDERS = ['openai', 'anthropic', 'google', 'groq', 'deepseek']; for (const p of PAID_PROVIDERS) { const isEnabled = aiSettings?.[`${p}_enabled`]; const pKey = aiSettings?.[`${p}_key`]; const pModel = aiSettings?.[`${p}_model`]; if (isEnabled && pKey) { try { const proxyResponse = await fetch(`${data.global.restUrl}/ai-assistant/chat-proxy`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': data.global.wpRestNonce }, body: JSON.stringify({ messages: [{ role: 'user', content: prompt }], provider: p, model: pModel }) }); const proxyRes = await proxyResponse.json(); if (proxyRes.success && proxyRes.content) { generatedTitle = proxyRes.content.replace(/["']/g, '').trim(); break; } } catch { console.warn(`Title generation failed on ${p}, trying next...`); } } } // Fallback to Free if not generated via paid if (!generatedTitle) { const response = await fetch('https://text.pollinations.ai/', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ messages: [{ role: 'user', content: prompt }], model: 'openai', seed: 42 }) }); generatedTitle = (await response.text()).replace(/["']/g, '').trim(); } if (generatedTitle && generatedTitle.length > 2) { setSessions(prev => { const exists = prev.some(s => s.id === sessionId); if (exists) { return prev.map(s => s.id === sessionId ? { ...s, title: generatedTitle } : s); } else { return [{ id: sessionId, title: generatedTitle, timestamp: new Date().toISOString() }, ...prev]; } }); await fetch(`${data.global.restUrl}/ai-assistant/sessions`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': data.global.wpRestNonce }, body: JSON.stringify({ id: sessionId, title: generatedTitle }) }); } } catch (err) { console.warn("Failed to generate auto title", err); } }; const saveMessage = async (role: 'user' | 'assistant', content: string, msgId?: string, forceSessionId?: number | null) => { try { const sessionIdToUse = forceSessionId !== undefined ? forceSessionId : currentSessionId; const response = await fetch(`${data.global.restUrl}/ai-assistant/chats`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': data.global.wpRestNonce }, body: JSON.stringify({ id: msgId, role, content, session_id: sessionIdToUse }) }); const res = await response.json(); if (res.success) { if (!sessionIdToUse && res.session_id) { setCurrentSessionId(res.session_id); fetchSessions(); } return { msgId: res.id, sessionId: res.session_id }; } } catch (error) { console.error("Failed to save message:", error); } return null; }; const rateMessage = async (msgId: string, rating: number) => { try { await fetch(`${data.global.restUrl}/ai-assistant/chats/${msgId}/rate`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': data.global.wpRestNonce }, body: JSON.stringify({ rating }) }); setMessages(prev => prev.map(m => m.id === msgId ? { ...m, rating } : m)); } catch (error) { console.error("Failed to rate message:", error); } }; const deleteMessage = async (msgId: string) => { try { await fetch(`${data.global.restUrl}/ai-assistant/chats/${msgId}`, { method: 'DELETE', headers: { 'X-WP-Nonce': data.global.wpRestNonce } }); } catch (error) { console.error("Failed to delete message:", error); } }; const handleCopy = (text: string) => { navigator.clipboard.writeText(text); }; const handleRegenerate = async () => { if (isLoading || messages.length < 2) return; const lastMsg = messages[messages.length - 1]; if (lastMsg.role !== 'assistant') return; await deleteMessage(lastMsg.id); setMessages(prev => prev.slice(0, -1)); const lastUserMsg = messages.findLast(m => m.role === 'user'); if (lastUserMsg) { handleSend(lastUserMsg.content, true); } }; const handleEditSave = async () => { if (!editingId || !editContent.trim()) return; await saveMessage('user', editContent, editingId); const editIndex = messages.findIndex(m => m.id === editingId); if (editIndex === -1) return; const updatedMessages = messages.slice(0, editIndex + 1); updatedMessages[editIndex].content = editContent; const nextMsg = messages[editIndex + 1]; if (nextMsg && nextMsg.role === 'assistant') { await deleteMessage(nextMsg.id); } setMessages(updatedMessages); setEditingId(null); handleSend(editContent, true); }; const handleSend = async (customInput?: string, isRetry = false) => { const textToSend = customInput || input; if (!textToSend.trim() || isLoading) return; let activeSessionId = currentSessionId; if (!isRetry) { localIdCounter.current++; const userMessage: Message = { id: `user-${localIdCounter.current}`, role: 'user', content: textToSend, timestamp: new Date() }; setMessages(prev => [...prev, userMessage]); const res = await saveMessage('user', textToSend); if (res) { activeSessionId = res.sessionId; setMessages(prev => prev.map(m => m.id === userMessage.id ? { ...m, id: res.msgId.toString() } : m)); } if (!customInput) setInput(''); } setIsLoading(true); const apiMessages = [ ...messages.filter(m => m.id !== 'welcome').map(m => ({ role: m.role, content: m.content })), { role: 'user', content: textToSend } ]; // Build a precise, deterministic features snapshot for the AI const featuresSnapshot: Record = {}; if (data.features) { for (const k of Object.keys(data.features)) { const feat = data.features[k as keyof typeof data.features]; if (feat && typeof feat === 'object' && 'option' in feat) { const f = feat as { enabled: string | number | boolean; allowed: boolean; option: string }; featuresSnapshot[k] = { status: f.enabled ? 'Enabled' : 'Disabled', allowed: f.allowed, db_key: f.option }; } } } const systemContext = `You are "Wawp Co-Pilot", the built-in AI assistant for the Wawp WordPress plugin (Automation Web Platform). CRITICAL RULES: - Respond in the SAME language the user writes in. Arabic→Arabic, English→English. - The plugin name is always "Wawp" (capital W, lowercase awp). Never WAWP or wawp. - Always start with brief analysis then your response. - You are NOT a general AI. Every answer must relate to this plugin. - Never say "I don't know" or "I can't help". Always provide a concrete next step. CURRENT STATE: Version: ${data.global?.version || 'unknown'} Plan: ${data.connector?.plan_name || 'Free'} Active Section: ${data.global?.section || 'dashboard'} Connected: ${data.sidebarData?.isConnected ? 'Yes' : 'No'} Active Senders: ${JSON.stringify(data.sidebarData?.enabledSenders || {})} FEATURES STATUS: ${Object.entries(featuresSnapshot).map(([k, v]) => `- ${k}: ${v.status} (key: ${v.db_key}, allowed: ${v.allowed})`).join('\n')} EXACT PAGE URLS (use these ONLY — never invent URLs): - Dashboard: admin.php?page=wawp&wawp_section=dashboard - Senders (WhatsApp/Email/Firebase): admin.php?page=wawp&wawp_section=senders - Chat Widget: admin.php?page=wawp&wawp_section=chat_widget - Notifications: admin.php?page=wawp&wawp_section=notifications - Campaigns: admin.php?page=wawp&wawp_section=campaigns - OTP Login: admin.php?page=wawp&wawp_section=otp_messages&tab=tab-otp-login - Registration: admin.php?page=wawp&wawp_section=otp_messages&tab=tab-signup - Checkout OTP: admin.php?page=wawp&wawp_section=otp_messages&tab=tab-checkout - Login/Signup Pages: admin.php?page=wawp&wawp_section=otp_messages&tab=tab-login-signup-pages - Abandoned Cart: admin.php?page=wawp&wawp_section=abandoned_cart - Block Manager: admin.php?page=wawp&wawp_section=block_manager - System Info: admin.php?page=wawp&wawp_section=system_info - reCAPTCHA: admin.php?page=wawp&wawp_section=recaptcha - Email Templates: admin.php?page=wawp&wawp_section=email_templates - Activity Logs: admin.php?page=wawp&wawp_section=activity_logs ACTION SYSTEM — to offer a toggle or input to the user, output exactly: EXAMPLES: - Enable chat widget: - Change welcome message: - Enable abandoned cart popup: NOTIFICATION CREATION — to create a new notification, output: Available trigger_keys: - user_login: User logs in - user_signup: New user registers - wc_status_processing: Order processing - wc_status_completed: Order completed - wc_status_on-hold: Order on hold - wc_status_cancelled: Order cancelled - wc_status_refunded: Order refunded - wc_status_failed: Order failed - wc_status_pending: Order pending - wc_order_note_added: Order note added - abandoned_cart: Abandoned cart recovery - potential_customer: Potential customer detected Available sender_types: - user_whatsapp: Send WhatsApp to user - user_email: Send email to user - user_both: Send WhatsApp + email to user - admin_whatsapp: Send WhatsApp to admin - admin_email: Send email to admin - user_admin_whatsapp: Send WhatsApp to user + admin - user_admin_both: Send all channels to user + admin Available placeholders for messages: - {{sitename}}: Site name - {{wp-display-name}}: User display name - {{order_id}}: Order ID - {{order_total}}: Order total - {{status}}: Order status When the user asks to create a notification, suggest the best configuration and output the tag. The user can approve it directly. KEY FORMAT RULES: - Master toggles (dashboard): value "1" or "0" - Chat widget keys: value "yes" or "no" - OTP/checkout keys: value "yes" or "no" - Always use the exact db_key from FEATURES STATUS or from this schema: ${JSON.stringify(AI_ACTIONS_SCHEMA, null, 2)} Be concise, helpful, and proactive. If a relevant feature is disabled, suggest enabling it. USER: ${textToSend}`; const fullApiMessages = [ { role: 'system', content: systemContext }, ...apiMessages ]; // Build priority list: fastest providers first (groq > openai > deepseek > anthropic > google > free) const SPEED_ORDER = ['groq', 'openai', 'deepseek', 'anthropic', 'google']; const PROVIDERS: { id: string; model: string | boolean | undefined }[] = []; for (const p of SPEED_ORDER) { if (aiSettings?.[`${p}_enabled`] && aiSettings?.[`${p}_key`]) { PROVIDERS.push({ id: p, model: aiSettings[`${p}_model`] }); } } if (aiSettings?.freeModeEnabled !== false) { PROVIDERS.push({ id: 'free', model: 'openai' }); } if (PROVIDERS.length === 0) { localIdCounter.current++; setMessages(prev => [...prev, { id: `assistant-local-${localIdCounter.current}`, role: 'assistant', content: "⚠️ **No AI Brain Active.**\n\nAll AI providers are currently disabled. Please go to **AI Settings** and enable at least one brain (Free or Paid) to continue.", timestamp: new Date() }]); setIsLoading(false); return; } localIdCounter.current++; const assistantMsgId = `assistant-local-${localIdCounter.current}`; let streamSuccess = false; for (const providerInfo of PROVIDERS) { try { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 45000); // 45s timeout const response = await fetch(`${data.global.restUrl}/ai-assistant/chat-stream`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': data.global.wpRestNonce }, body: JSON.stringify({ messages: fullApiMessages, provider: providerInfo.id, model: providerInfo.model }), signal: controller.signal }); clearTimeout(timeoutId); if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); const reader = response.body?.getReader(); if (!reader) throw new Error("No reader available"); const decoder = new TextDecoder(); streamContentRef.current = ''; streamReasoningRef.current = ''; let isStarted = false; let hasError = false; 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 dataStr = line.slice(6).trim(); if (dataStr === '[DONE]') continue; try { const json = JSON.parse(dataStr); // Detect streaming errors from the server if (json.error) { console.warn(`Stream error from ${providerInfo.id}:`, json.error); hasError = true; break; } let delta = ''; let reasoningDelta = ''; // Handle different provider response formats if (['openai', 'groq', 'deepseek', 'free'].includes(providerInfo.id)) { delta = json.choices?.[0]?.delta?.content || ''; reasoningDelta = json.choices?.[0]?.delta?.reasoning_content || ''; } else if (providerInfo.id === 'anthropic') { if (json.type === 'content_block_delta') { delta = json.delta?.text || ''; } else if (json.type === 'message_delta') { // End of message } else if (json.type === 'error') { hasError = true; break; } } else if (providerInfo.id === 'google') { delta = json.candidates?.[0]?.content?.parts?.[0]?.text || ''; } if (delta || reasoningDelta) { streamContentRef.current = streamContentRef.current + delta; streamReasoningRef.current = streamReasoningRef.current + reasoningDelta; streamSuccess = true; if (!isStarted) { setMessages(prev => [...prev, { id: assistantMsgId, role: 'assistant', content: '', reasoning: '', timestamp: new Date(), rating: 0 }]); isStarted = true; setIsLoading(false); } setMessages(prev => prev.map(m => m.id === assistantMsgId ? { ...m, content: streamContentRef.current.replace(/[\s\S]*?<\/thought>/, '').trim(), reasoning: streamReasoningRef.current || (streamContentRef.current.match(/([\s\S]*?)<\/thought>/)?.[1] || m.reasoning) } : m)); } } catch { // Ignore parse errors for partial chunks } } } if (hasError) break; } // If we got an error mid-stream without any content, skip to next provider if (hasError && !streamSuccess) { console.warn(`Provider ${providerInfo.id} returned an error, trying next...`); continue; } if (streamSuccess) { // Final clean up and save const finalMsg = streamContentRef.current.replace(/[\s\S]*?<\/thought>/, '').trim(); const saveRes = await saveMessage('assistant', streamContentRef.current, undefined, activeSessionId); if (saveRes) { setMessages(prev => prev.map(m => m.id === assistantMsgId ? { ...m, id: saveRes.msgId.toString() } : m)); if (messages.length <= 3) generateAutoTitle(saveRes.sessionId, textToSend, finalMsg); } break; } } catch (err) { console.warn(`Provider ${providerInfo.id} failed, trying next...`, err); } } if (!streamSuccess) { localIdCounter.current++; setMessages(prev => [...prev, { id: `assistant-local-${localIdCounter.current}`, role: 'assistant', content: "⚠️ **Connection Issue**\n\nI couldn't reach any AI provider. Please check:\n- Your API keys in **AI Settings**\n- Your server's internet connectivity\n- That at least one AI provider is enabled", timestamp: new Date(), rating: 0 }]); } setIsLoading(false); }; return ( <> {createPortal( {open && ( setOpen(false)} className="fixed inset-0 bg-black/40 z-[9999999] backdrop-blur-sm" /> {/* Sidebar */} Chat History {isFetchingSessions && sessions.length === 0 ? ( ) : sessions.length === 0 ? ( No previous chats. ) : ( sessions.map(session => ( selectSession(session.id)} className={cn( "group p-3 rounded-xl cursor-pointer transition-all flex items-start gap-3", currentSessionId === session.id ? "bg-white border border-emerald-100 shadow-sm" : "hover:bg-white/50 border border-transparent" )} > {renamingSessionId === session.id ? ( setRenamingTitle(e.target.value)} onBlur={() => setRenamingSessionId(null)} className="w-full text-xs p-1 border border-emerald-100 rounded focus:ring-0 focus:border-emerald-500 bg-white" /> ) : ( {session.title} { e.stopPropagation(); setRenamingSessionId(session.id); setRenamingTitle(session.title); }} className="opacity-0 group-hover:opacity-100 p-1 text-slate-300 hover:text-emerald-600" > )} {new Date(session.timestamp).toLocaleDateString()} deleteSession(e, session.id)} className="opacity-0 group-hover:opacity-100 p-1 hover:text-red-500 text-slate-300" > )) )} {/* Sidebar Bottom Actions */} setView('settings')} className={cn( "w-full justify-start gap-2 text-xs font-bold uppercase tracking-widest transition-all", view === 'settings' ? "text-emerald-600 bg-white shadow-sm ring-1 ring-emerald-100" : "text-slate-400 hover:text-emerald-600" )} > AI Settings {/* Main Content */} setSidebarOpen(!sidebarOpen)} className="w-6 h-12 bg-white border border-slate-100 rounded-full flex items-center justify-center text-slate-300 hover:text-emerald-500 transition-colors" > {sidebarOpen ? (data.global.isRtl ? : ) : (data.global.isRtl ? : )} {view === 'settings' ? ( { setView('chat'); fetchAiSettings(); }} /> ) : ( <> s.id === currentSessionId)?.title || "Wawp Co-Pilot" : "Wawp Co-Pilot"} description="Live Context-Aware Assistant" icon={() => } rightAction={ setOpen(false)} className="text-slate-300 hover:text-slate-600 rounded-full"> } /> {isFetchingHistory && ( )} {messages.map((msg, index) => ( {msg.role === 'assistant' ? ( getAssistantIcon(msg.id, msg.content) ) : ( )} {editingId === msg.id ? ( setEditContent(e.target.value)} className="w-full bg-white/10 border border-white/20 rounded-xl p-3 text-sm text-white focus:ring-0 shadow-none resize-none" autoFocus /> setEditingId(null)} className="p-1.5 hover:bg-white/10 rounded-lg"> ) : ( {(() => { interface ContentPart { type: 'text' | 'action' | 'notification'; content?: string; attrs?: Record; } const parts: ContentPart[] = []; let lastIndex = 0; const regexAction = //g; const regexNotif = //g; interface TagMatch { start: number; end: number; type: 'action' | 'notification'; attrStr: string; } const matches: TagMatch[] = []; let match; while ((match = regexAction.exec(msg.content)) !== null) { matches.push({ start: match.index, end: regexAction.lastIndex, type: 'action', attrStr: match[1] }); } while ((match = regexNotif.exec(msg.content)) !== null) { matches.push({ start: match.index, end: regexNotif.lastIndex, type: 'notification', attrStr: match[1] }); } matches.sort((a, b) => a.start - b.start); for (const m of matches) { const textBefore = msg.content.substring(lastIndex, m.start); if (textBefore.trim()) { parts.push({ type: 'text', content: textBefore }); } const attrs: Record = {}; const attrMatches = m.attrStr.matchAll(/(\w+)="([^"]*)"/g); for (const am of attrMatches) { attrs[am[1]] = am[2]; } parts.push({ type: m.type, attrs }); lastIndex = m.end; } const remainingText = msg.content.substring(lastIndex); if (remainingText.trim() || parts.length === 0) { parts.push({ type: 'text', content: remainingText }); } return parts.map((part, i) => ( {part.type === 'text' ? ( ) => , th: (props: React.ComponentPropsWithoutRef<'th'>) => , td: (props: React.ComponentPropsWithoutRef<'td'>) => , a: (props: React.ComponentPropsWithoutRef<'a'>) => , code: ({ inline, children, ...props }: { inline?: boolean; children?: React.ReactNode } & React.ComponentPropsWithoutRef<'code'>) => inline ? {children} : {children} }} > {part.content} ) : part.type === 'action' ? ( { localIdCounter.current++; setMessages(prev => [...prev, { id: `action-${localIdCounter.current}`, role: 'assistant', content: m, timestamp: new Date() }]); }} /> ) : ( { localIdCounter.current++; setMessages(prev => [...prev, { id: `notif-${localIdCounter.current}`, role: 'assistant', content: m, timestamp: new Date() }]); }} /> )} )); })()} )} {msg.reasoning && ( {msg.reasoning} )} {msg.timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} {msg.role === 'assistant' ? ( <> rateMessage(msg.id, msg.rating === 1 ? 0 : 1)} className={cn("p-1 transition-colors", msg.rating === 1 ? "text-emerald-500" : "text-slate-300 hover:text-emerald-500")}> rateMessage(msg.id, msg.rating === -1 ? 0 : -1)} className={cn("p-1 transition-colors", msg.rating === -1 ? "text-red-500" : "text-slate-300 hover:text-red-500")}> handleCopy(msg.content)} className="p-1 text-slate-300 hover:text-emerald-600"> {index === messages.length - 1 && ( )} > ) : ( <> handleCopy(msg.content)} className="p-1 text-slate-300 hover:text-emerald-600"> {index === messages.length - 2 && ( { setEditingId(msg.id); setEditContent(msg.content); }} className="p-1 text-slate-300 hover:text-emerald-600"> )} > )} ))} {isLoading && ( {getAssistantIcon()} {thinkingSteps[currentThinkingStep]} )} {/* Input Area */} {messages.length <= 1 && ( {[ { label: 'My Setup', icon: Settings }, { label: 'Check Issues', icon: ShieldCheck }, { label: 'Meta API Help', icon: Zap } ].map(btn => ( handleSend(btn.label)} className="bg-slate-50/50 border border-slate-100 text-slate-500 rounded-full px-4 hover:border-emerald-200 hover:text-emerald-600 transition-all text-[11px]" > {btn.label} ))} )} Linked setInput(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSend(); } }} placeholder="Ask Wawp anything..." className="flex-1 bg-transparent border-none focus:ring-0 text-sm py-3 px-1 resize-none min-h-[44px] max-h-32 text-slate-600 placeholder:text-slate-300 shadow-none" /> handleSend()} disabled={isLoading || !input.trim()} className={cn( "w-9 h-9 rounded-xl flex items-center justify-center transition-all shrink-0", input.trim() && !isLoading ? "bg-emerald-600 text-white hover:bg-emerald-700 shadow-lg shadow-emerald-600/10" : "bg-slate-100 text-slate-200" )} > {isLoading ? : } Secure Context Ready > )} )} , document.body )} > ); }
{attrs.whatsapp_message}
{children}