mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-06-02 19:55:18 +02:00
feat(web): add WhatsApp messaging channel controls
This commit is contained in:
parent
185759de1f
commit
bba33b5947
2 changed files with 111 additions and 18 deletions
|
|
@ -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_FASTAPI_BACKEND_AUTH_TYPE=LOCAL or GOOGLE
|
||||||
NEXT_PUBLIC_ETL_SERVICE=UNSTRUCTURED or LLAMACLOUD or DOCLING
|
NEXT_PUBLIC_ETL_SERVICE=UNSTRUCTURED or LLAMACLOUD or DOCLING
|
||||||
NEXT_PUBLIC_ZERO_CACHE_URL=http://localhost:4848
|
NEXT_PUBLIC_ZERO_CACHE_URL=http://localhost:4848
|
||||||
|
NEXT_PUBLIC_GATEWAY_WHATSAPP_INTAKE_MODE=disabled
|
||||||
|
|
||||||
# Contact Form Vars (optional)
|
# Contact Form Vars (optional)
|
||||||
DATABASE_URL=postgresql://postgres:[YOUR-PASSWORD]@db.sdsf.supabase.co:5432/postgres
|
DATABASE_URL=postgresql://postgres:[YOUR-PASSWORD]@db.sdsf.supabase.co:5432/postgres
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
import { MessageCircle, RefreshCw, ShieldAlert } from "lucide-react";
|
import { MessageCircle, RefreshCw, ShieldAlert } from "lucide-react";
|
||||||
import { useParams } from "next/navigation";
|
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 { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
|
@ -11,6 +11,7 @@ import { BACKEND_URL } from "@/lib/env-config";
|
||||||
|
|
||||||
type Binding = {
|
type Binding = {
|
||||||
id: number;
|
id: number;
|
||||||
|
platform?: string;
|
||||||
state: string;
|
state: string;
|
||||||
search_space_id: number;
|
search_space_id: number;
|
||||||
external_display_name?: string | null;
|
external_display_name?: string | null;
|
||||||
|
|
@ -34,13 +35,20 @@ type Pairing = {
|
||||||
expires_at: string;
|
expires_at: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type PairingPlatform = "telegram" | "whatsapp";
|
||||||
|
|
||||||
export function MessagingChannelsContent() {
|
export function MessagingChannelsContent() {
|
||||||
const params = useParams<{ search_space_id: string }>();
|
const params = useParams<{ search_space_id: string }>();
|
||||||
const searchSpaceId = Number(params.search_space_id);
|
const searchSpaceId = Number(params.search_space_id);
|
||||||
const [bindings, setBindings] = useState<Binding[]>([]);
|
const [bindings, setBindings] = useState<Binding[]>([]);
|
||||||
const [platforms, setPlatforms] = useState<Platform[]>([]);
|
const [platforms, setPlatforms] = useState<Platform[]>([]);
|
||||||
const [pairing, setPairing] = useState<Pairing | null>(null);
|
const [pairing, setPairing] = useState<Pairing | null>(null);
|
||||||
|
const [pairingPlatform, setPairingPlatform] = useState<PairingPlatform | null>(null);
|
||||||
|
const [whatsappStatus, setWhatsappStatus] = useState<string | null>(null);
|
||||||
|
const [baileysPhone, setBaileysPhone] = useState("");
|
||||||
|
const [baileysCode, setBaileysCode] = useState<string | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [isPending, startTransition] = useTransition();
|
||||||
|
|
||||||
const refresh = useCallback(async () => {
|
const refresh = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
@ -57,16 +65,40 @@ export function MessagingChannelsContent() {
|
||||||
void refresh();
|
void refresh();
|
||||||
}, [refresh]);
|
}, [refresh]);
|
||||||
|
|
||||||
async function startPairing() {
|
async function startPairing(platform: PairingPlatform) {
|
||||||
const res = await authenticatedFetch(`${BACKEND_URL}/api/v1/gateway/bindings/start`, {
|
const res = await authenticatedFetch(`${BACKEND_URL}/api/v1/gateway/bindings/start`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
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());
|
setPairing(await res.json());
|
||||||
|
setPairingPlatform(platform);
|
||||||
await refresh();
|
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) {
|
async function revoke(id: number) {
|
||||||
await authenticatedFetch(`${BACKEND_URL}/api/v1/gateway/bindings/${id}`, {
|
await authenticatedFetch(`${BACKEND_URL}/api/v1/gateway/bindings/${id}`, {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
|
|
@ -82,7 +114,26 @@ export function MessagingChannelsContent() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const telegram = platforms.find((p) => p.platform === "telegram");
|
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 activeBindings = bindings.filter((binding) => binding.search_space_id === searchSpaceId);
|
||||||
|
const renderPairingPanel = (platform: PairingPlatform) => {
|
||||||
|
if (!pairing || pairingPlatform !== platform) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="rounded-md border border-border bg-muted/30 p-4">
|
||||||
|
<p className="text-sm font-medium">Pairing code</p>
|
||||||
|
<p className="mt-2 font-mono text-lg">{pairing.code}</p>
|
||||||
|
<a className="mt-2 block text-sm text-primary underline" href={pairing.deep_link}>
|
||||||
|
Open {platform === "whatsapp" ? "WhatsApp" : "Telegram"} pairing link
|
||||||
|
</a>
|
||||||
|
<p className="mt-2 text-xs text-muted-foreground">
|
||||||
|
Expires at {new Date(pairing.expires_at).toLocaleString()}. SurfSense stores this
|
||||||
|
channel's messages for agent memory and operational debugging.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-5">
|
<div className="space-y-5">
|
||||||
|
|
@ -104,36 +155,77 @@ export function MessagingChannelsContent() {
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
<Button onClick={startPairing}>Pair Telegram Chat</Button>
|
<Button onClick={() => startPairing("telegram")}>Pair Telegram Chat</Button>
|
||||||
<Button variant="outline" onClick={refresh} disabled={loading}>
|
<Button variant="outline" onClick={refresh} disabled={loading}>
|
||||||
<RefreshCw className="mr-2 h-4 w-4" />
|
<RefreshCw className="mr-2 h-4 w-4" />
|
||||||
Refresh
|
Refresh
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{pairing ? (
|
{renderPairingPanel("telegram")}
|
||||||
<div className="rounded-md border border-border bg-muted/30 p-4">
|
|
||||||
<p className="text-sm font-medium">Pairing code</p>
|
|
||||||
<p className="mt-2 font-mono text-lg">{pairing.code}</p>
|
|
||||||
<a className="mt-2 block text-sm text-primary underline" href={pairing.deep_link}>
|
|
||||||
Open Telegram pairing link
|
|
||||||
</a>
|
|
||||||
<p className="mt-2 text-xs text-muted-foreground">
|
|
||||||
Expires at {new Date(pairing.expires_at).toLocaleString()}. SurfSense stores this
|
|
||||||
channel's messages for agent memory and operational debugging.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
{whatsappMode !== "disabled" ? (
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between gap-3">
|
||||||
|
<CardTitle className="flex items-center gap-2 text-base">
|
||||||
|
<MessageCircle className="h-4 w-4" />
|
||||||
|
WhatsApp
|
||||||
|
</CardTitle>
|
||||||
|
<Badge variant={whatsapp?.health_status === "ok" ? "default" : "secondary"}>
|
||||||
|
{whatsapp?.health_status ?? "not configured"}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Pair this search space with WhatsApp using the configured gateway mode.
|
||||||
|
</p>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
{whatsappMode === "cloud" ? (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<Button onClick={() => startPairing("whatsapp")}>Pair WhatsApp</Button>
|
||||||
|
{renderPairingPanel("whatsapp")}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{whatsappMode === "baileys" ? (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Self-hosted WhatsApp uses Message Yourself mode. After pairing, send messages in
|
||||||
|
your own WhatsApp chat with yourself; messages from other chats are ignored.
|
||||||
|
</p>
|
||||||
|
<input
|
||||||
|
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
|
||||||
|
placeholder="Phone number with country code, e.g. 15551234567"
|
||||||
|
value={baileysPhone}
|
||||||
|
onChange={(event) => setBaileysPhone(event.target.value)}
|
||||||
|
/>
|
||||||
|
<Button onClick={pairBaileys} disabled={isPending || !baileysPhone.trim()}>
|
||||||
|
Pair WhatsApp
|
||||||
|
</Button>
|
||||||
|
{baileysCode ? (
|
||||||
|
<div className="rounded-md border border-border bg-muted/30 p-4">
|
||||||
|
<p className="text-sm font-medium">WhatsApp pairing code</p>
|
||||||
|
<p className="mt-2 font-mono text-lg">{baileysCode}</p>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{whatsappStatus ? (
|
||||||
|
<p className="text-sm text-muted-foreground">{whatsappStatus}</p>
|
||||||
|
) : null}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-base">Active Chats</CardTitle>
|
<CardTitle className="text-base">Active Chats</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-3">
|
<CardContent className="space-y-3">
|
||||||
{activeBindings.length === 0 ? (
|
{activeBindings.length === 0 ? (
|
||||||
<p className="text-sm text-muted-foreground">No Telegram chats paired yet.</p>
|
<p className="text-sm text-muted-foreground">No external chats connected yet.</p>
|
||||||
) : (
|
) : (
|
||||||
activeBindings.map((binding) => (
|
activeBindings.map((binding) => (
|
||||||
<div
|
<div
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue