From 5f9d16530d61fb5e1d327c6fcd4d83f32065c21e Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Wed, 27 May 2026 23:41:18 +0530 Subject: [PATCH] feat(web): add messaging channels settings page --- .../components/MessagingChannelsContent.tsx | 174 ++++++++++++++++++ .../user-settings/layout-shell.tsx | 9 +- .../user-settings/messaging-channels/page.tsx | 6 + 3 files changed, 188 insertions(+), 1 deletion(-) create mode 100644 surfsense_web/app/dashboard/[search_space_id]/user-settings/components/MessagingChannelsContent.tsx create mode 100644 surfsense_web/app/dashboard/[search_space_id]/user-settings/messaging-channels/page.tsx 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 new file mode 100644 index 000000000..0c35533c6 --- /dev/null +++ b/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/MessagingChannelsContent.tsx @@ -0,0 +1,174 @@ +"use client"; + +import { MessageCircle, RefreshCw, ShieldAlert } from "lucide-react"; +import { useParams } from "next/navigation"; +import { useCallback, useEffect, useState } from "react"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { authenticatedFetch } from "@/lib/auth-utils"; +import { BACKEND_URL } from "@/lib/env-config"; + +type Binding = { + id: number; + state: string; + search_space_id: number; + external_display_name?: string | null; + external_username?: string | null; + suspended_reason?: string | null; +}; + +type Platform = { + id: number; + platform: string; + mode: string; + bot_username?: string | null; + health_status: string; + last_health_check_at?: string | null; +}; + +type Pairing = { + binding_id: number; + code: string; + deep_link: string; + expires_at: string; +}; + +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 [loading, setLoading] = useState(true); + + const refresh = useCallback(async () => { + setLoading(true); + const [bindingsRes, platformsRes] = await Promise.all([ + authenticatedFetch(`${BACKEND_URL}/api/v1/gateway/bindings`), + authenticatedFetch(`${BACKEND_URL}/api/v1/gateway/platforms`), + ]); + setBindings(await bindingsRes.json()); + setPlatforms(await platformsRes.json()); + setLoading(false); + }, []); + + useEffect(() => { + void refresh(); + }, [refresh]); + + async function startPairing() { + 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 }), + }); + setPairing(await res.json()); + await refresh(); + } + + async function revoke(id: number) { + await authenticatedFetch(`${BACKEND_URL}/api/v1/gateway/bindings/${id}`, { + method: "DELETE", + }); + await refresh(); + } + + async function resume(id: number) { + await authenticatedFetch(`${BACKEND_URL}/api/v1/gateway/bindings/${id}/resume`, { + method: "POST", + }); + await refresh(); + } + + const telegram = platforms.find((p) => p.platform === "telegram"); + const activeBindings = bindings.filter((binding) => binding.search_space_id === searchSpaceId); + + return ( +
+ + +
+ + + Telegram + + + {telegram?.health_status ?? "not configured"} + +
+

+ Pair a Telegram chat with this search space. Telegram conversations stay in Telegram and + are not mirrored in SurfSense chat history. +

+
+ +
+ + +
+ + {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} +
+
+ + + + Active Chats + + + {activeBindings.length === 0 ? ( +

No Telegram chats paired yet.

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

+ {binding.external_display_name || + binding.external_username || + `Binding ${binding.id}`} +

+

{binding.state}

+ {binding.suspended_reason ? ( +

+ + {binding.suspended_reason} +

+ ) : null} +
+
+ {binding.state === "suspended" ? ( + + ) : null} + +
+
+ )) + )} +
+
+
+ ); +} diff --git a/surfsense_web/app/dashboard/[search_space_id]/user-settings/layout-shell.tsx b/surfsense_web/app/dashboard/[search_space_id]/user-settings/layout-shell.tsx index 037568db3..4aac4d2f6 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/user-settings/layout-shell.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/user-settings/layout-shell.tsx @@ -5,6 +5,7 @@ import { Keyboard, KeyRound, Library, + MessageCircle, Monitor, ReceiptText, ShieldCheck, @@ -29,7 +30,8 @@ export type UserSettingsTab = | "agent-status" | "purchases" | "desktop" - | "hotkeys"; + | "hotkeys" + | "messaging-channels"; const DEFAULT_TAB: UserSettingsTab = "profile"; @@ -83,6 +85,11 @@ export function UserSettingsLayoutShell({ searchSpaceId, children }: UserSetting label: "Agent Status", icon: , }, + { + value: "messaging-channels" as const, + label: "Messaging Channels", + icon: , + }, { value: "purchases" as const, label: "Purchase History", diff --git a/surfsense_web/app/dashboard/[search_space_id]/user-settings/messaging-channels/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/user-settings/messaging-channels/page.tsx new file mode 100644 index 000000000..31dc6b56a --- /dev/null +++ b/surfsense_web/app/dashboard/[search_space_id]/user-settings/messaging-channels/page.tsx @@ -0,0 +1,6 @@ +import { MessagingChannelsContent } from "../components/MessagingChannelsContent"; + +export default function Page() { + return ; +} +