From bba33b59471adc1ddd24a96a1b2247cdbc85674a Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Fri, 29 May 2026 10:21:37 +0530 Subject: [PATCH] feat(web): add WhatsApp messaging channel controls --- surfsense_web/.env.example | 1 + .../components/MessagingChannelsContent.tsx | 128 +++++++++++++++--- 2 files changed, 111 insertions(+), 18 deletions(-) diff --git a/surfsense_web/.env.example b/surfsense_web/.env.example index 5fb9d07d1..12d81ee3f 100644 --- a/surfsense_web/.env.example +++ b/surfsense_web/.env.example @@ -6,6 +6,7 @@ FASTAPI_BACKEND_INTERNAL_URL=https://your-internal-backend.example.com NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE=LOCAL or GOOGLE NEXT_PUBLIC_ETL_SERVICE=UNSTRUCTURED or LLAMACLOUD or DOCLING NEXT_PUBLIC_ZERO_CACHE_URL=http://localhost:4848 +NEXT_PUBLIC_GATEWAY_WHATSAPP_INTAKE_MODE=disabled # Contact Form Vars (optional) DATABASE_URL=postgresql://postgres:[YOUR-PASSWORD]@db.sdsf.supabase.co:5432/postgres diff --git a/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/MessagingChannelsContent.tsx b/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/MessagingChannelsContent.tsx index 0c35533c6..248abb121 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/MessagingChannelsContent.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/MessagingChannelsContent.tsx @@ -2,7 +2,7 @@ import { MessageCircle, RefreshCw, ShieldAlert } from "lucide-react"; import { useParams } from "next/navigation"; -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useEffect, useState, useTransition } from "react"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; @@ -11,6 +11,7 @@ import { BACKEND_URL } from "@/lib/env-config"; type Binding = { id: number; + platform?: string; state: string; search_space_id: number; external_display_name?: string | null; @@ -34,13 +35,20 @@ type Pairing = { expires_at: string; }; +type PairingPlatform = "telegram" | "whatsapp"; + export function MessagingChannelsContent() { const params = useParams<{ search_space_id: string }>(); const searchSpaceId = Number(params.search_space_id); const [bindings, setBindings] = useState([]); const [platforms, setPlatforms] = useState([]); const [pairing, setPairing] = useState(null); + const [pairingPlatform, setPairingPlatform] = useState(null); + const [whatsappStatus, setWhatsappStatus] = useState(null); + const [baileysPhone, setBaileysPhone] = useState(""); + const [baileysCode, setBaileysCode] = useState(null); const [loading, setLoading] = useState(true); + const [isPending, startTransition] = useTransition(); const refresh = useCallback(async () => { setLoading(true); @@ -57,16 +65,40 @@ export function MessagingChannelsContent() { void refresh(); }, [refresh]); - async function startPairing() { + async function startPairing(platform: PairingPlatform) { const res = await authenticatedFetch(`${BACKEND_URL}/api/v1/gateway/bindings/start`, { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ platform: "telegram", search_space_id: searchSpaceId }), + body: JSON.stringify({ platform, search_space_id: searchSpaceId }), }); setPairing(await res.json()); + setPairingPlatform(platform); await refresh(); } + function pairBaileys() { + startTransition(async () => { + setWhatsappStatus("Requesting WhatsApp pairing code..."); + const res = await authenticatedFetch(`${BACKEND_URL}/api/v1/gateway/whatsapp/baileys/pair`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ search_space_id: searchSpaceId, phone_number: baileysPhone }), + }); + if (!res.ok) { + setWhatsappStatus("Unable to request pairing code. Check the whatsapp-bridge service."); + return; + } + const data = await res.json(); + setBaileysCode(data.pairing_code ?? null); + setWhatsappStatus( + data.status === "connected" + ? "WhatsApp bridge is connected." + : "Enter the pairing code in WhatsApp.", + ); + await refresh(); + }); + } + async function revoke(id: number) { await authenticatedFetch(`${BACKEND_URL}/api/v1/gateway/bindings/${id}`, { method: "DELETE", @@ -82,7 +114,26 @@ export function MessagingChannelsContent() { } const telegram = platforms.find((p) => p.platform === "telegram"); + const whatsapp = platforms.find((p) => p.platform === "whatsapp"); + const whatsappMode = process.env.NEXT_PUBLIC_GATEWAY_WHATSAPP_INTAKE_MODE ?? "disabled"; const activeBindings = bindings.filter((binding) => binding.search_space_id === searchSpaceId); + const renderPairingPanel = (platform: PairingPlatform) => { + if (!pairing || pairingPlatform !== platform) return null; + + return ( +
+

Pairing code

+

{pairing.code}

+ + Open {platform === "whatsapp" ? "WhatsApp" : "Telegram"} pairing link + +

+ Expires at {new Date(pairing.expires_at).toLocaleString()}. SurfSense stores this + channel's messages for agent memory and operational debugging. +

+
+ ); + }; return (
@@ -104,36 +155,77 @@ export function MessagingChannelsContent() {
- +
- {pairing ? ( -
-

Pairing code

-

{pairing.code}

- - Open Telegram pairing link - -

- Expires at {new Date(pairing.expires_at).toLocaleString()}. SurfSense stores this - channel's messages for agent memory and operational debugging. -

-
- ) : null} + {renderPairingPanel("telegram")}
+ {whatsappMode !== "disabled" ? ( + + +
+ + + WhatsApp + + + {whatsapp?.health_status ?? "not configured"} + +
+

+ Pair this search space with WhatsApp using the configured gateway mode. +

+
+ + {whatsappMode === "cloud" ? ( +
+ + {renderPairingPanel("whatsapp")} +
+ ) : null} + {whatsappMode === "baileys" ? ( +
+

+ Self-hosted WhatsApp uses Message Yourself mode. After pairing, send messages in + your own WhatsApp chat with yourself; messages from other chats are ignored. +

+ setBaileysPhone(event.target.value)} + /> + + {baileysCode ? ( +
+

WhatsApp pairing code

+

{baileysCode}

+
+ ) : null} +
+ ) : null} + {whatsappStatus ? ( +

{whatsappStatus}

+ ) : null} +
+
+ ) : null} + Active Chats {activeBindings.length === 0 ? ( -

No Telegram chats paired yet.

+

No external chats connected yet.

) : ( activeBindings.map((binding) => (