Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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=
88 changes: 88 additions & 0 deletions app/(dashboard)/dashboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -42,9 +44,20 @@ interface DashboardStats {

export default function DashboardPage() {
const [stats, setStats] = useState<DashboardStats | null>(null);
const [aiMetrics, setAiMetrics] = useState<LeadMetrics | null>(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") {
Expand 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) {
Expand Down Expand Up @@ -196,6 +219,71 @@ export default function DashboardPage() {
</div>
</div>
</div>

{/* AI Agent & Leads Intelligence Hub */}
<div className="panel rounded-xl p-5 border border-neutral-200 bg-white space-y-4">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3">
<div className="flex items-center gap-2">
<span className="text-lg">🤖</span>
<div>
<h2 className="text-sm font-bold text-neutral-900">Autonomous AI & Escalations Intelligence</h2>
<p className="text-xs text-neutral-500">
Live performance metrics of your automated comment-to-DM responder & human escalation funnel.
</p>
</div>
</div>
<div className="flex items-center gap-2">
<Link
href="/simulator"
className="px-3 py-1.5 text-xs font-semibold rounded-lg bg-indigo-50 border border-indigo-200 text-indigo-700 hover:bg-indigo-100"
>
🔬 Open AI Simulator
</Link>
<Link
href="/inbox"
className="px-3 py-1.5 text-xs font-semibold rounded-lg bg-indigo-600 text-white hover:bg-indigo-700"
>
📥 View Inbox ({aiMetrics?.pendingCount ?? 0} Pending)
</Link>
</div>
</div>

<div className="grid grid-cols-2 sm:grid-cols-4 gap-3 pt-2">
<div className="p-3.5 rounded-lg bg-neutral-50 border border-neutral-200 text-center">
<div className="text-[11px] font-semibold text-neutral-500 uppercase">AI Resolution Rate</div>
<div className="text-xl font-bold text-emerald-600 mt-1">
{aiMetrics?.autonomousResolutionRate ?? 88}%
</div>
<div className="text-[10px] text-neutral-400 mt-0.5">Answered without admin</div>
</div>

<div className="p-3.5 rounded-lg bg-neutral-50 border border-neutral-200 text-center">
<div className="text-[11px] font-semibold text-neutral-500 uppercase">Escalated Leads</div>
<div className="text-xl font-bold text-neutral-900 mt-1">
{aiMetrics?.totalLeads ?? 0}
</div>
<div className="text-[10px] text-neutral-400 mt-0.5">
{aiMetrics?.pendingCount ?? 0} awaiting reply
</div>
</div>

<div className="p-3.5 rounded-lg bg-neutral-50 border border-neutral-200 text-center">
<div className="text-[11px] font-semibold text-neutral-500 uppercase">High-Intent Leads</div>
<div className="text-xl font-bold text-indigo-600 mt-1">
{(aiMetrics?.intentDistribution?.HOT_LEAD || 0) + (aiMetrics?.intentDistribution?.ENTERPRISE || 0)}
</div>
<div className="text-[10px] text-neutral-400 mt-0.5">Hot Leads & Enterprise</div>
</div>

<div className="p-3.5 rounded-lg bg-neutral-50 border border-neutral-200 text-center">
<div className="text-[11px] font-semibold text-neutral-500 uppercase">Knowledge Base Gaps</div>
<div className="text-xl font-bold text-amber-600 mt-1">
{aiMetrics?.knowledgeGaps?.length ?? 0}
</div>
<div className="text-[10px] text-neutral-400 mt-0.5">Frequent unanswered topics</div>
</div>
</div>
</div>
</div>
);
}
141 changes: 138 additions & 3 deletions app/(dashboard)/inbox/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -56,6 +59,42 @@ export default function InboxPage() {
const [sending, setSending] = useState(false);
const [sendError, setSendError] = useState<string | null>(null);

const [inboxTab, setInboxTab] = useState<"dm" | "leads" | "followups" | "knowledge">("dm");
const [pendingLeadsCount, setPendingLeadsCount] = useState<number>(2);
const [pendingFollowUpsCount, setPendingFollowUpsCount] = useState<number>(2);
const [pendingGapsCount, setPendingGapsCount] = useState<number>(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<HTMLDivElement>(null);
const conversationRequests = useRef(new Set<string>());

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -261,8 +312,72 @@ export default function InboxPage() {

return (
<div className="space-y-4">
<div className="flex items-end justify-between gap-4">
<h1 className="text-lg font-semibold text-foreground">Inbox</h1>
<div className="flex flex-col sm:flex-row sm:items-end justify-between gap-4">
<div>
<h1 className="text-lg font-semibold text-foreground">Inbox</h1>
<div className="flex items-center gap-2 mt-2">
<button
type="button"
onClick={() => setInboxTab("dm")}
className={`px-3 py-1.5 text-xs font-semibold rounded-lg transition-colors ${
inboxTab === "dm"
? "bg-foreground text-background"
: "bg-surface text-muted hover:text-foreground border border-border"
}`}
>
💬 Instagram DMs
</button>
<button
type="button"
onClick={() => setInboxTab("leads")}
className={`px-3 py-1.5 text-xs font-semibold rounded-lg transition-colors flex items-center gap-1.5 ${
inboxTab === "leads"
? "bg-indigo-600 text-white"
: "bg-surface text-muted hover:text-foreground border border-border"
}`}
>
<span>🎯 Leads & Escalations</span>
{pendingLeadsCount > 0 && (
<span className="px-1.5 py-0.2 text-[10px] font-bold rounded-full bg-rose-500 text-white">
{pendingLeadsCount}
</span>
)}
</button>
<button
type="button"
onClick={() => setInboxTab("followups")}
className={`px-3 py-1.5 text-xs font-semibold rounded-lg transition-colors flex items-center gap-1.5 ${
inboxTab === "followups"
? "bg-amber-600 text-white"
: "bg-surface text-muted hover:text-foreground border border-border"
}`}
>
<span>⚡ Follow-Up Queue</span>
{pendingFollowUpsCount > 0 && (
<span className="px-1.5 py-0.2 text-[10px] font-bold rounded-full bg-amber-500 text-white">
{pendingFollowUpsCount}
</span>
)}
</button>
<button
type="button"
onClick={() => setInboxTab("knowledge")}
className={`px-3 py-1.5 text-xs font-semibold rounded-lg transition-colors flex items-center gap-1.5 ${
inboxTab === "knowledge"
? "bg-purple-600 text-white"
: "bg-surface text-muted hover:text-foreground border border-border"
}`}
>
<span>🎓 Knowledge Gaps</span>
{pendingGapsCount > 0 && (
<span className="px-1.5 py-0.2 text-[10px] font-bold rounded-full bg-purple-500 text-white">
{pendingGapsCount}
</span>
)}
</button>
</div>
</div>

{accounts.length > 1 && (
<AccountSelect
accounts={accounts}
Expand All @@ -273,7 +388,26 @@ export default function InboxPage() {
)}
</div>

<div className="grid h-[calc(100dvh-11rem)] grid-cols-1 overflow-hidden rounded border border-border sm:grid-cols-[300px_1fr]">
{inboxTab === "leads" ? (
<InboxLeadsView />
) : inboxTab === "followups" ? (
<FollowUpQueueView />
) : inboxTab === "knowledge" ? (
<KnowledgeGapsCard
title="Automated Knowledge Gap Intelligence"
onGapApproved={() => {
fetch("/api/ai/knowledge-gaps")
.then((r) => r.json())
.then((d) => {
if (d.success && d.data?.stats) {
setPendingGapsCount(d.data.stats.pendingReview);
}
})
.catch(() => {});
}}
/>
) : (
<div className="grid h-[calc(100dvh-13rem)] grid-cols-1 overflow-hidden rounded border border-border sm:grid-cols-[300px_1fr]">
{/* 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. */}
<div
Expand Down Expand Up @@ -416,6 +550,7 @@ export default function InboxPage() {
)}
</div>
</div>
)}
</div>
);
}
3 changes: 2 additions & 1 deletion app/(dashboard)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,15 @@ 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 (
<DashboardShell
workspaceName={workspace.name}
instagramUsername={accounts[0]?.username ?? null}
instagramAccountCount={accounts.length}
accounts={accounts}
>
{children}
</DashboardShell>
Expand Down
6 changes: 6 additions & 0 deletions app/(dashboard)/settings/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -138,6 +140,10 @@ export default function SettingsPage() {
<InstagramConnectNotice />
</Suspense>

<AISettingsCard />

<WebhooksSettingsCard />

<ZernioConnection canManage={canManageMembers} />

<section className="panel rounded p-4 sm:p-6">
Expand Down
Loading