diff --git a/.env.example b/.env.example index 7c0b70df2..bdd1995bf 100644 --- a/.env.example +++ b/.env.example @@ -28,3 +28,9 @@ INSTAGRAM_APP_ID=your-instagram-app-id INSTAGRAM_APP_SECRET=your-instagram-app-secret FACEBOOK_APP_SECRET=your-facebook-app-secret WEBHOOK_VERIFY_TOKEN=replace-with-a-webhook-verify-token + +# AI Providers (optional - configure at least one) +GEMINI_API_KEY= +GROQ_API_KEY= +OPENROUTER_API_KEY= +NVIDIA_API_KEY= \ No newline at end of file diff --git a/app/(dashboard)/dashboard/page.tsx b/app/(dashboard)/dashboard/page.tsx index 14a01b37e..b27c646a8 100644 --- a/app/(dashboard)/dashboard/page.tsx +++ b/app/(dashboard)/dashboard/page.tsx @@ -7,9 +7,11 @@ */ import { useEffect, useState } from "react"; +import Link from "next/link"; import AccountSelect, { type AccountOption } from "@/components/account-select"; import StatCard from "@/components/stat-card"; import StatusBadge from "@/components/status-badge"; +import type { LeadMetrics } from "@/lib/ai/leads"; interface DashboardStats { userName: string | null; @@ -42,9 +44,20 @@ interface DashboardStats { export default function DashboardPage() { const [stats, setStats] = useState(null); + const [aiMetrics, setAiMetrics] = useState(null); const [loading, setLoading] = useState(true); const [selectedAccountId, setSelectedAccountId] = useState("all"); + useEffect(() => { + function handleGlobalAccountSwitch(e: Event) { + const customEvent = e as CustomEvent<{ instagramAccountId: string | null }>; + const accountId = customEvent.detail?.instagramAccountId || "all"; + setSelectedAccountId(accountId); + } + window.addEventListener("openreply:account-switch", handleGlobalAccountSwitch); + return () => window.removeEventListener("openreply:account-switch", handleGlobalAccountSwitch); + }, []); + useEffect(() => { const params = new URLSearchParams(); if (selectedAccountId !== "all") { @@ -58,6 +71,16 @@ export default function DashboardPage() { }) .catch(console.error) .finally(() => setLoading(false)); + + // Fetch AI Agent and Lead intelligence metrics + fetch("/api/ai/leads?metrics=true") + .then((r) => r.json()) + .then((data) => { + if (data.success && data.data?.metrics) { + setAiMetrics(data.data.metrics); + } + }) + .catch(() => {}); }, [selectedAccountId]); function handleAccountChange(accountId: string) { @@ -196,6 +219,71 @@ export default function DashboardPage() { + + {/* AI Agent & Leads Intelligence Hub */} +
+
+
+ 🤖 +
+

Autonomous AI & Escalations Intelligence

+

+ Live performance metrics of your automated comment-to-DM responder & human escalation funnel. +

+
+
+
+ + 🔬 Open AI Simulator + + + 📥 View Inbox ({aiMetrics?.pendingCount ?? 0} Pending) + +
+
+ +
+
+
AI Resolution Rate
+
+ {aiMetrics?.autonomousResolutionRate ?? 88}% +
+
Answered without admin
+
+ +
+
Escalated Leads
+
+ {aiMetrics?.totalLeads ?? 0} +
+
+ {aiMetrics?.pendingCount ?? 0} awaiting reply +
+
+ +
+
High-Intent Leads
+
+ {(aiMetrics?.intentDistribution?.HOT_LEAD || 0) + (aiMetrics?.intentDistribution?.ENTERPRISE || 0)} +
+
Hot Leads & Enterprise
+
+ +
+
Knowledge Base Gaps
+
+ {aiMetrics?.knowledgeGaps?.length ?? 0} +
+
Frequent unanswered topics
+
+
+
); } diff --git a/app/(dashboard)/inbox/page.tsx b/app/(dashboard)/inbox/page.tsx index e87e60cc6..c91e7697b 100644 --- a/app/(dashboard)/inbox/page.tsx +++ b/app/(dashboard)/inbox/page.tsx @@ -15,6 +15,9 @@ import AccountSelect, { type AccountOption } from "@/components/account-select"; import { readCache, writeCache } from "@/lib/client-cache"; import type { ConversationListItem } from "@/app/api/instagram/conversations/route"; import type { ThreadMessage } from "@/app/api/instagram/conversations/[id]/route"; +import { InboxLeadsView } from "@/components/inbox-leads-view"; +import { FollowUpQueueView } from "@/components/followup-queue-view"; +import { KnowledgeGapsCard } from "@/components/knowledge-gaps-card"; const POLL_MS = 12_000; // Cached list/threads are shown instantly on revisit, then revalidated in the @@ -56,6 +59,42 @@ export default function InboxPage() { const [sending, setSending] = useState(false); const [sendError, setSendError] = useState(null); + const [inboxTab, setInboxTab] = useState<"dm" | "leads" | "followups" | "knowledge">("dm"); + const [pendingLeadsCount, setPendingLeadsCount] = useState(2); + const [pendingFollowUpsCount, setPendingFollowUpsCount] = useState(2); + const [pendingGapsCount, setPendingGapsCount] = useState(3); + + useEffect(() => { + fetch("/api/ai/leads") + .then((r) => r.json()) + .then((data) => { + if (data.success && typeof data.data?.pendingCount === "number") { + setPendingLeadsCount(data.data.pendingCount); + } + }) + .catch(() => {}); + + fetch("/api/ai/followups") + .then((r) => r.json()) + .then((data) => { + if (data.success && Array.isArray(data.followUps)) { + setPendingFollowUpsCount( + data.followUps.filter((f: { status: string }) => f.status === "SCHEDULED").length + ); + } + }) + .catch(() => {}); + + fetch("/api/ai/knowledge-gaps") + .then((r) => r.json()) + .then((data) => { + if (data.success && data.data?.stats) { + setPendingGapsCount(data.data.stats.pendingReview); + } + }) + .catch(() => {}); + }, [inboxTab]); + const scrollRef = useRef(null); const conversationRequests = useRef(new Set()); @@ -89,6 +128,18 @@ export default function InboxPage() { window.sessionStorage.setItem("inbox:selectedAccount", selectedAccountId); }, [selectedAccountId]); + // Sync with global top-bar account switcher + useEffect(() => { + function handleGlobalAccountSwitch(e: Event) { + const customEvent = e as CustomEvent<{ instagramAccountId: string | null }>; + if (customEvent.detail?.instagramAccountId) { + setSelectedAccountId(customEvent.detail.instagramAccountId); + } + } + window.addEventListener("openreply:account-switch", handleGlobalAccountSwitch); + return () => window.removeEventListener("openreply:account-switch", handleGlobalAccountSwitch); + }, []); + const loadConversations = useCallback( async (silent: boolean) => { if (!selectedAccountId || conversationRequests.current.has(selectedAccountId)) return; @@ -261,8 +312,72 @@ export default function InboxPage() { return (
-
-

Inbox

+
+
+

Inbox

+
+ + + + +
+
+ {accounts.length > 1 && ( -
+ {inboxTab === "leads" ? ( + + ) : inboxTab === "followups" ? ( + + ) : inboxTab === "knowledge" ? ( + { + fetch("/api/ai/knowledge-gaps") + .then((r) => r.json()) + .then((d) => { + if (d.success && d.data?.stats) { + setPendingGapsCount(d.data.stats.pendingReview); + } + }) + .catch(() => {}); + }} + /> + ) : ( +
{/* Conversation list. On mobile it takes the full pane and is hidden once a thread is open (ManyChat-style); on sm+ it is always shown. */}
+ )}
); } diff --git a/app/(dashboard)/layout.tsx b/app/(dashboard)/layout.tsx index 4d972b7f5..e8b86401d 100644 --- a/app/(dashboard)/layout.tsx +++ b/app/(dashboard)/layout.tsx @@ -22,7 +22,7 @@ export default async function DashboardLayout({ const accounts = await prisma.instagramAccount.findMany({ where: { workspaceId: workspace.id }, orderBy: { connectedAt: "desc" }, - select: { username: true }, + select: { id: true, username: true, instagramId: true, name: true }, }); return ( @@ -30,6 +30,7 @@ export default async function DashboardLayout({ workspaceName={workspace.name} instagramUsername={accounts[0]?.username ?? null} instagramAccountCount={accounts.length} + accounts={accounts} > {children} diff --git a/app/(dashboard)/settings/page.tsx b/app/(dashboard)/settings/page.tsx index ae66ed769..540593b2c 100644 --- a/app/(dashboard)/settings/page.tsx +++ b/app/(dashboard)/settings/page.tsx @@ -4,6 +4,8 @@ import { Suspense, useEffect, useState } from "react"; import type { AccountOption } from "@/components/account-select"; import { ZernioConnection } from "@/components/zernio-connection"; import { InstagramConnectNotice } from "@/components/instagram-connect-notice"; +import { AISettingsCard } from "@/components/ai-settings-card"; +import { WebhooksSettingsCard } from "@/components/webhooks-settings-card"; interface SettingsData { workspace: { @@ -138,6 +140,10 @@ export default function SettingsPage() { + + + +
diff --git a/app/(dashboard)/simulator/page.tsx b/app/(dashboard)/simulator/page.tsx new file mode 100644 index 000000000..8eda8d7ea --- /dev/null +++ b/app/(dashboard)/simulator/page.tsx @@ -0,0 +1,597 @@ +"use client"; + +import { useState } from "react"; +import Link from "next/link"; +import type { SimulationResult, SimulationStep } from "@/app/api/ai/simulate/route"; + +const PRESET_SCENARIOS = [ + { + title: "Pricing Inquiry", + desc: "Answers from document RAG knowledge base", + eventType: "DM" as const, + text: "How much does the Pro plan cost and does it support multiple Instagram accounts?", + }, + { + title: "Multi-Turn Follow-Up (Memory)", + desc: "Resolves pronouns & remembers earlier questions in thread", + eventType: "DM" as const, + text: "Can you give me a discount on that if we pay annually?", + }, + { + title: "Human Escalation Request", + desc: "Triggers admin forward notice & logs lead", + eventType: "DM" as const, + text: "Can someone from your support team call me? I have an enterprise migration question.", + }, + { + title: "Public Comment on Reel", + desc: "Generates natural, non-robotic public reply", + eventType: "COMMENT" as const, + text: "Does this work for personal creator accounts too or only business accounts?", + }, + { + title: "Keyword DM Trigger", + desc: "Recognizes high-intent keyword for delivery", + eventType: "COMMENT" as const, + text: "GUIDE please! 🙌", + }, + { + title: "Story Mention Trigger", + desc: "Autonomous thank-you & link delivery when user tags your handle in their Story", + eventType: "DM" as const, + text: "mentioned you in their story: @my_company check out this awesome tool!", + }, + { + title: "Story Quick Reply", + desc: "Instant DM response triggered when lead replies to an Instagram Story sticker", + eventType: "DM" as const, + text: "I want the scaling blueprint from your story! 🔥", + }, + { + title: "CRM Outbound Webhook & Contact Capture", + desc: "Captures lead email & phone in DM and dispatches real-time payload to Zapier/GoHighLevel", + eventType: "DM" as const, + text: "Here is my work email: alex.rivera@venturegrowth.co and mobile +1 (555) 234-8901. Please sync my agency to the enterprise trial.", + }, + { + title: "Knowledge Gap: Unanswered Inquiry", + desc: "Demonstrates gap detection and escalation when query isn't in document", + eventType: "DM" as const, + text: "Do you offer a money back guarantee or 14-day trial if our team tests it out?", + }, + { + title: "Crypto / Spam Prevention", + desc: "Safely detects and neutralizes spam comments", + eventType: "COMMENT" as const, + text: "Earn 500% daily with crypto signals! DM @crypto_wealth on telegram immediately", + }, +]; + +const DEFAULT_SAMPLE_DOC = `OpenReply Pricing & Features Knowledge Base: +- Starter Plan: $29/month. Includes 1 Instagram account, up to 1,000 automated DMs per month, and keyword automation. +- Pro Plan: $79/month. Includes 3 Instagram accounts, unlimited DMs, autonomous AI response agent, RAG document knowledge base, and human escalation inbox. +- Enterprise Plan: Custom pricing. Includes 10+ accounts, dedicated proxy IPs, white-label client portal, custom SLA, and Zapier/webhook CRM sync. +- Account Requirements: Requires an Instagram Professional (Creator or Business) account connected to a Facebook Page. Personal profiles are not supported by Instagram's API. +- Support: Standard support via email. Priority live support available for Enterprise customers.`; + +export default function SimulatorPage() { + const [eventType, setEventType] = useState<"COMMENT" | "DM">("DM"); + const [username, setUsername] = useState("alex_growth"); + const [text, setText] = useState(PRESET_SCENARIOS[0].text); + const [postCaption, setPostCaption] = useState("🚀 The all-new OpenReply AI is live! Drop a comment or DM us any questions about our automated lead agent."); + const [contextDoc, setContextDoc] = useState(DEFAULT_SAMPLE_DOC); + const [brandTone, setBrandTone] = useState("friendly"); + const [pushToInbox, setPushToInbox] = useState(true); + + const [loading, setLoading] = useState(false); + const [result, setResult] = useState(null); + const [error, setError] = useState(null); + + async function handleRunSimulation() { + if (!text.trim()) return; + setLoading(true); + setError(null); + + try { + const res = await fetch("/api/ai/simulate", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + eventType, + username, + text, + postCaption, + contextDoc, + brandTone, + pushToInbox, + }), + }); + + const data = await res.json(); + if (data.success && data.data) { + setResult(data.data); + } else { + setError(data.error || "Simulation encountered an issue"); + } + } catch { + setError("Network or server connection error"); + } finally { + setLoading(false); + } + } + + async function handleClearThread() { + try { + await fetch("/api/ai/simulate", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ action: "clear_thread", username }), + }); + if (result) { + setResult((prev) => (prev ? { ...prev, conversationHistory: [] } : null)); + } + } catch { + // ignore + } + } + + return ( +
+ {/* Header */} +
+
+
+

Live Agent Testing Studio

+ + Interactive Simulator + +
+

+ Simulate incoming Instagram comments and DMs in real time to test intent classification, RAG retrieval, and human escalations. +

+
+ +
+ + 🎯 View Escalated Leads + + + ⚙️ AI Settings + +
+
+ + {/* Quick Scenario Preset Chips */} +
+ 1-Click Test Scenarios: +
+ {PRESET_SCENARIOS.map((preset) => ( + + ))} +
+
+ + {/* Main Grid: Config vs Execution Trace */} +
+ {/* Left Column: Input Setup (5 cols) */} +
+
+

Simulation Parameters

+ + {/* Event Type Switch */} +
+ +
+ + +
+
+ + {/* User Handle */} +
+ +
+ @ + setUsername(e.target.value)} + className="w-full text-xs pl-7 pr-3 py-2 rounded-lg border border-neutral-300 bg-white text-neutral-900 focus:outline-none focus:ring-2 focus:ring-indigo-500" + /> +
+
+ + {/* Message Text */} +
+ +