mirror of
https://github.com/Mintplex-Labs/anything-llm
synced 2026-04-25 17:15:37 +02:00
Redesign Telegram bot settings UI (#5306)
* redesign telegram bot settings ui/refactor ui components * fix positioning of user row * move ConnectedBotCard to subcomponent * fix redirect * remove redundant guard --------- Co-authored-by: Timothy Carambat <rambat1010@gmail.com>
This commit is contained in:
@@ -688,7 +688,7 @@ const TRANSLATIONS = {
|
||||
step2: {
|
||||
title: "Step 2: Connect your bot",
|
||||
description:
|
||||
"Paste the API token you received from @BotFather and select a default workspace for your bot to chat with.",
|
||||
"Paste the API token you received from @BotFather to connect your bot.",
|
||||
"bot-token": "Bot Token",
|
||||
"default-workspace": "Default Workspace",
|
||||
"no-workspace": "No available workspaces. A new one will be created.",
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { TelegramLogo } from "@phosphor-icons/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function ConnectedBotCard({ config }) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="flex flex-col gap-y-[18px]">
|
||||
<p className="text-base font-semibold text-white light:text-slate-900">
|
||||
Connected Bot
|
||||
</p>
|
||||
<div className="flex items-start gap-x-1 border border-zinc-700 light:border-slate-200 rounded-xl p-3 w-[700px]">
|
||||
<div className="flex items-center justify-center w-9 h-9 rounded-full bg-[#00ADEC] shrink-0">
|
||||
<TelegramLogo className="h-5 w-5 !text-white" weight="fill" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-y-1 ml-1">
|
||||
<p className="text-sm font-semibold text-white light:text-slate-900">
|
||||
@{config.bot_username}
|
||||
</p>
|
||||
<p className="text-xs text-zinc-400 light:text-slate-600">
|
||||
{t("telegram.connected.status")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import { useState } from "react";
|
||||
import { CircleNotch } from "@phosphor-icons/react";
|
||||
import Telegram from "@/models/telegram";
|
||||
import showToast from "@/utils/toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
export default function DetailsSection({ config, onDisconnected }) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="flex flex-col gap-y-[18px]">
|
||||
<p className="text-base font-semibold text-white light:text-slate-900">
|
||||
Details
|
||||
</p>
|
||||
<div className="border border-zinc-700 light:border-slate-200 rounded-xl p-4 w-[700px]">
|
||||
<div className="flex flex-col gap-y-4 text-sm">
|
||||
<DetailRow
|
||||
label={t("telegram.connected.workspace")}
|
||||
value={config.default_workspace}
|
||||
/>
|
||||
<DetailRow label="Thread" value={config.active_thread_name} />
|
||||
<DetailRow label="Model" value={config.chat_model} />
|
||||
<DetailRow
|
||||
label={t("telegram.connected.bot-link")}
|
||||
value={
|
||||
<Link
|
||||
to={`https://t.me/${config.bot_username}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-blue-400 light:text-blue-500 underline"
|
||||
>
|
||||
t.me/{config.bot_username}
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DisconnectButton onDisconnected={onDisconnected} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DetailRow({ label, value }) {
|
||||
return (
|
||||
<div className="flex items-start justify-between">
|
||||
<span className="font-medium text-white light:text-slate-900">
|
||||
{label}
|
||||
</span>
|
||||
<span className="text-zinc-300 light:text-slate-700">{value}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DisconnectButton({ onDisconnected }) {
|
||||
const { t } = useTranslation();
|
||||
const [disconnecting, setDisconnecting] = useState(false);
|
||||
|
||||
async function handleDisconnect() {
|
||||
setDisconnecting(true);
|
||||
const res = await Telegram.disconnect();
|
||||
setDisconnecting(false);
|
||||
|
||||
if (!res.success) {
|
||||
showToast(
|
||||
res.error || t("telegram.connected.toast-disconnect-failed"),
|
||||
"error"
|
||||
);
|
||||
return;
|
||||
}
|
||||
onDisconnected();
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={handleDisconnect}
|
||||
disabled={disconnecting}
|
||||
className="flex items-center justify-center gap-x-2 text-sm font-medium bg-zinc-50 light:bg-slate-900 text-zinc-950 light:text-white rounded-lg h-9 px-5 w-fit hover:opacity-90 transition-opacity duration-200 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{disconnecting ? (
|
||||
<>
|
||||
<CircleNotch className="h-4 w-4 animate-spin" />
|
||||
{t("telegram.connected.disconnecting")}
|
||||
</>
|
||||
) : (
|
||||
t("telegram.connected.disconnect")
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
import { useState } from "react";
|
||||
import {
|
||||
CircleNotch,
|
||||
Eye,
|
||||
EyeSlash,
|
||||
TelegramLogo,
|
||||
} from "@phosphor-icons/react";
|
||||
import Telegram from "@/models/telegram";
|
||||
import showToast from "@/utils/toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function DisconnectedView({
|
||||
config,
|
||||
onReconnected,
|
||||
newToken,
|
||||
setNewToken,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [reconnecting, setReconnecting] = useState(false);
|
||||
const [showToken, setShowToken] = useState(false);
|
||||
const Icon = showToken ? Eye : EyeSlash;
|
||||
|
||||
async function handleReconnect(e) {
|
||||
e.preventDefault();
|
||||
if (!newToken.trim()) return;
|
||||
setReconnecting(true);
|
||||
const res = await Telegram.connect(
|
||||
newToken.trim(),
|
||||
config.default_workspace
|
||||
);
|
||||
setReconnecting(false);
|
||||
if (!res.success)
|
||||
return showToast(
|
||||
res.error || t("telegram.connected.toast-reconnect-failed"),
|
||||
"error"
|
||||
);
|
||||
|
||||
setNewToken("");
|
||||
const configRes = await Telegram.getConfig();
|
||||
onReconnected(configRes?.config);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-8 mt-8">
|
||||
<div className="flex flex-col gap-y-[18px]">
|
||||
<p className="text-base font-semibold text-white light:text-slate-900">
|
||||
Connected Bot
|
||||
</p>
|
||||
<div className="flex items-start gap-x-1 border border-red-500/30 light:border-red-300 rounded-xl p-3 w-[700px]">
|
||||
<div className="flex items-center justify-center w-9 h-9 rounded-full bg-red-500/20 shrink-0">
|
||||
<TelegramLogo
|
||||
className="h-5 w-5 text-red-400 light:text-red-500"
|
||||
weight="fill"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-y-1 ml-1">
|
||||
<p className="text-sm font-semibold text-white light:text-slate-900">
|
||||
@{config.bot_username}
|
||||
</p>
|
||||
<p className="text-xs text-red-400 light:text-red-500">
|
||||
{t("telegram.connected.status-disconnected")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<form
|
||||
onSubmit={handleReconnect}
|
||||
className="flex items-end gap-x-2 w-[700px]"
|
||||
>
|
||||
<div className="bg-zinc-800 light:bg-white light:border light:border-slate-300 h-8 rounded-lg px-3.5 flex items-center gap-x-2 flex-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowToken(!showToken)}
|
||||
className="text-zinc-400 light:text-slate-500 hover:text-zinc-300 light:hover:text-slate-700 transition-colors shrink-0"
|
||||
>
|
||||
<Icon className="h-4 w-4" />
|
||||
</button>
|
||||
<input
|
||||
type={showToken ? "text" : "password"}
|
||||
value={newToken}
|
||||
onChange={(e) => setNewToken(e.target.value)}
|
||||
placeholder={t("telegram.connected.placeholder-token")}
|
||||
className="bg-transparent flex-1 text-sm text-white light:text-slate-900 placeholder:text-zinc-400 light:placeholder:text-slate-500 outline-none min-w-0"
|
||||
autoComplete="off"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={reconnecting}
|
||||
className="flex items-center justify-center gap-x-1.5 text-sm font-medium bg-zinc-50 light:bg-slate-900 text-zinc-900 light:text-white rounded-lg h-8 px-5 hover:opacity-90 transition-opacity duration-200 disabled:opacity-50 disabled:cursor-not-allowed whitespace-nowrap"
|
||||
>
|
||||
{reconnecting ? (
|
||||
<CircleNotch className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
t("telegram.connected.reconnect")
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
This code is disabled for now - works fine, but I am not sure we want to enabled this feature.
|
||||
How many people really need a REPLY with voice mode? Even then, we should support on device TTS
|
||||
and more it out the frontend so people can do voice gen without having to pay for it.
|
||||
|
||||
When we do enabled this, we should uncomment this code and remove the disabled comment.
|
||||
|
||||
const getVoiceModeOptions = (t) => {
|
||||
return [
|
||||
{ value: "text_only", label: t("telegram.connected.voice-text-only") },
|
||||
{ value: "mirror", label: t("telegram.connected.voice-mirror") },
|
||||
{ value: "always_voice", label: t("telegram.connected.voice-always") },
|
||||
];
|
||||
};
|
||||
|
||||
function VoiceModeSelector({ config }) {
|
||||
const { t } = useTranslation();
|
||||
const [voiceMode, setVoiceMode] = useState(
|
||||
config.voice_response_mode || "text_only"
|
||||
);
|
||||
|
||||
async function handleVoiceModeChange(e) {
|
||||
const mode = e.target.value;
|
||||
setVoiceMode(mode);
|
||||
const res = await Telegram.updateConfig({ voice_response_mode: mode });
|
||||
if (!res.success) {
|
||||
showToast(
|
||||
res.error || t("telegram.connected.toast-voice-failed"),
|
||||
"error"
|
||||
);
|
||||
setVoiceMode(config.voice_response_mode || "text_only");
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs text-white">
|
||||
{t("telegram.connected.voice-response")}
|
||||
</span>
|
||||
<select
|
||||
value={voiceMode}
|
||||
onChange={handleVoiceModeChange}
|
||||
className="text-xs text-right bg-transparent text-white rounded-md px-2 py-1 outline-none max-w-[260px]"
|
||||
>
|
||||
{getVoiceModeOptions(t).map((opt) => (
|
||||
<option key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
*/
|
||||
@@ -0,0 +1,148 @@
|
||||
import { X, Check } from "@phosphor-icons/react";
|
||||
import Telegram from "@/models/telegram";
|
||||
import showToast from "@/utils/toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function UsersSection({
|
||||
pendingUsers,
|
||||
approvedUsers,
|
||||
fetchUsers,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
async function handleApprove(chatId) {
|
||||
const res = await Telegram.approveUser(chatId);
|
||||
if (!res.success) {
|
||||
showToast(
|
||||
res.error || t("telegram.connected.toast-approve-failed"),
|
||||
"error"
|
||||
);
|
||||
return;
|
||||
}
|
||||
fetchUsers();
|
||||
}
|
||||
|
||||
async function handleDeny(chatId) {
|
||||
const res = await Telegram.denyUser(chatId);
|
||||
if (!res.success) {
|
||||
showToast(
|
||||
res.error || t("telegram.connected.toast-deny-failed"),
|
||||
"error"
|
||||
);
|
||||
return;
|
||||
}
|
||||
fetchUsers();
|
||||
}
|
||||
|
||||
async function handleRevoke(chatId) {
|
||||
const res = await Telegram.revokeUser(chatId);
|
||||
if (!res.success) {
|
||||
showToast(
|
||||
res.error || t("telegram.connected.toast-revoke-failed"),
|
||||
"error"
|
||||
);
|
||||
return;
|
||||
}
|
||||
fetchUsers();
|
||||
}
|
||||
|
||||
const hasPending = pendingUsers.length > 0;
|
||||
const hasApproved = approvedUsers.length > 0;
|
||||
if (!hasPending && !hasApproved) return null;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-[18px] w-[700px]">
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<p className="text-base font-semibold text-white light:text-slate-900">
|
||||
Users
|
||||
</p>
|
||||
<p className="text-xs text-zinc-400 light:text-slate-600">
|
||||
{t("telegram.users.pending-description")}
|
||||
</p>
|
||||
</div>
|
||||
<div className="border-t border-zinc-700 light:border-slate-200" />
|
||||
<div className="flex flex-col gap-y-2">
|
||||
{pendingUsers.map((user) => (
|
||||
<UserRow
|
||||
key={user.chatId || user}
|
||||
user={user}
|
||||
isPending
|
||||
onApprove={handleApprove}
|
||||
onDeny={handleDeny}
|
||||
/>
|
||||
))}
|
||||
{approvedUsers.map((user) => (
|
||||
<UserRow
|
||||
key={user.chatId || user}
|
||||
user={user}
|
||||
onRevoke={handleRevoke}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function UserRow({ user, isPending = false, onApprove, onDeny, onRevoke }) {
|
||||
const { t } = useTranslation();
|
||||
const chatId = typeof user === "string" ? user : user.chatId;
|
||||
const username = user.telegramUsername || user.username || null;
|
||||
const firstName = user.firstName || null;
|
||||
const displayName = username
|
||||
? `@${username}`
|
||||
: firstName || t("telegram.users.unknown");
|
||||
const initial = (username || firstName || "?")[0].toUpperCase();
|
||||
const code = user.code;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center">
|
||||
<div className="flex items-center gap-x-3 flex-1 min-w-0">
|
||||
<div className="bg-zinc-800 light:bg-slate-300 size-8 rounded-full flex items-center justify-center shrink-0">
|
||||
<span className="text-sm font-semibold text-white light:text-slate-900">
|
||||
{initial}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-sm font-medium text-white light:text-slate-900 truncate">
|
||||
{displayName}
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-[60px] flex items-center justify-center shrink-0 mr-36">
|
||||
{isPending && code && (
|
||||
<div className="bg-zinc-950 light:bg-slate-200 h-[26px] w-[60px] flex items-center justify-center rounded">
|
||||
<span className="text-sm text-white/80 light:text-slate-900 text-center">
|
||||
{code}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center justify-end gap-x-3 w-[80px] shrink-0">
|
||||
{isPending ? (
|
||||
<>
|
||||
<button
|
||||
onClick={() => onDeny(chatId)}
|
||||
className="text-zinc-400 light:text-slate-400 hover:text-red-400 light:hover:text-red-500 transition-colors"
|
||||
>
|
||||
<X className="h-4 w-4" weight="bold" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onApprove(chatId)}
|
||||
className="text-zinc-400 light:text-slate-400 hover:text-green-400 light:hover:text-green-500 transition-colors"
|
||||
>
|
||||
<Check className="h-4 w-4" weight="bold" />
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => onRevoke(chatId)}
|
||||
className="text-sm text-white/80 light:text-slate-500 hover:text-white light:hover:text-slate-700 transition-colors"
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="border-t border-zinc-800 light:border-slate-200" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,153 +0,0 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Telegram from "@/models/telegram";
|
||||
import showToast from "@/utils/toast";
|
||||
|
||||
export default function UsersTable({
|
||||
title,
|
||||
description,
|
||||
users = [],
|
||||
isPending = false,
|
||||
fetchUsers = () => {},
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
if (users.length === 0) return null;
|
||||
const colCount = isPending ? 4 : 3;
|
||||
|
||||
async function handleApprove(chatId) {
|
||||
const res = await Telegram.approveUser(chatId);
|
||||
if (!res.success) {
|
||||
showToast(
|
||||
res.error || t("telegram.connected.toast-approve-failed"),
|
||||
"error"
|
||||
);
|
||||
return;
|
||||
}
|
||||
fetchUsers();
|
||||
}
|
||||
|
||||
async function handleDeny(chatId) {
|
||||
const res = await Telegram.denyUser(chatId);
|
||||
if (!res.success) {
|
||||
showToast(
|
||||
res.error || t("telegram.connected.toast-deny-failed"),
|
||||
"error"
|
||||
);
|
||||
return;
|
||||
}
|
||||
fetchUsers();
|
||||
}
|
||||
|
||||
async function handleRevoke(chatId) {
|
||||
const res = await Telegram.revokeUser(chatId);
|
||||
if (!res.success) {
|
||||
showToast(
|
||||
res.error || t("telegram.connected.toast-revoke-failed"),
|
||||
"error"
|
||||
);
|
||||
return;
|
||||
}
|
||||
fetchUsers();
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<p className="text-sm font-semibold text-theme-text-primary">{title}</p>
|
||||
<p className="text-xs text-theme-text-secondary">{description}</p>
|
||||
<div className="overflow-x-auto mt-2">
|
||||
<table className="w-1/2 text-xs text-left rounded-lg min-w-[480px] border-spacing-0">
|
||||
<thead className="text-theme-text-secondary text-xs leading-[18px] font-bold uppercase border-white/10 border-b">
|
||||
<tr>
|
||||
<th scope="col" className="px-6 py-3">
|
||||
{t("telegram.users.user")}
|
||||
</th>
|
||||
{isPending && (
|
||||
<th scope="col" className="px-6 py-3">
|
||||
{t("telegram.users.pairing-code")}
|
||||
</th>
|
||||
)}
|
||||
<th scope="col" className="px-6 py-3">
|
||||
{" "}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{users.length === 0 ? (
|
||||
<tr className="bg-transparent text-theme-text-secondary text-sm font-medium">
|
||||
<td colSpan={colCount} className="px-6 py-4 text-center">
|
||||
{isPending
|
||||
? t("telegram.users.no-pending")
|
||||
: t("telegram.users.no-approved")}
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
users.map((user) => {
|
||||
const chatId = typeof user === "string" ? user : user.chatId;
|
||||
const username = user.telegramUsername || user.username || null;
|
||||
const firstName = user.firstName || null;
|
||||
const displayName = username
|
||||
? `@${username}`
|
||||
: firstName || t("telegram.users.unknown");
|
||||
const code = user.code;
|
||||
return (
|
||||
<tr
|
||||
key={chatId}
|
||||
className="bg-transparent text-white text-opacity-80 text-xs font-medium border-b border-white/10 h-10"
|
||||
>
|
||||
<td className="px-6 whitespace-nowrap">
|
||||
<span className="text-sm text-theme-text-primary">
|
||||
{displayName}
|
||||
</span>
|
||||
</td>
|
||||
{isPending && (
|
||||
<td className="px-6 whitespace-nowrap">
|
||||
<code className="bg-theme-bg-primary px-2 py-1 rounded text-theme-text-primary font-mono text-sm">
|
||||
{code}
|
||||
</code>
|
||||
</td>
|
||||
)}
|
||||
<td className="px-6 flex items-center gap-x-6 h-full mt-1">
|
||||
{isPending ? (
|
||||
<>
|
||||
<ActionButton
|
||||
onClick={() => handleApprove(chatId)}
|
||||
className="hover:light:bg-green-50 hover:light:text-green-500 hover:text-green-300"
|
||||
>
|
||||
{t("telegram.users.approve")}
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
onClick={() => handleDeny(chatId)}
|
||||
className="hover:light:bg-red-50 hover:light:text-red-500 hover:text-red-300"
|
||||
>
|
||||
{t("telegram.users.deny")}
|
||||
</ActionButton>
|
||||
</>
|
||||
) : (
|
||||
<ActionButton
|
||||
onClick={() => handleRevoke(chatId)}
|
||||
className="hover:light:bg-red-50"
|
||||
>
|
||||
{t("telegram.users.revoke")}
|
||||
</ActionButton>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ActionButton({ onClick, className = "", children }) {
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={`border-none flex items-center justify-center text-xs font-medium text-white/80 light:text-black/80 rounded-lg p-1 hover:bg-white hover:bg-opacity-10 ${className}`}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -1,31 +1,19 @@
|
||||
import { useEffect, useState, useCallback } from "react";
|
||||
import {
|
||||
ArrowSquareOut,
|
||||
CircleNotch,
|
||||
Eye,
|
||||
EyeSlash,
|
||||
TelegramLogo,
|
||||
} from "@phosphor-icons/react";
|
||||
import Telegram from "@/models/telegram";
|
||||
import showToast from "@/utils/toast";
|
||||
import UsersTable from "./UsersTable";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import ConnectedBotCard from "./ConnectedBotCard";
|
||||
import DetailsSection from "./DetailsSection";
|
||||
import UsersSection from "./UsersSection";
|
||||
import DisconnectedView from "./DisconnectedView";
|
||||
|
||||
export default function ConnectedView({
|
||||
config,
|
||||
workspaces,
|
||||
onDisconnected,
|
||||
onReconnected,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const connected = config.connected;
|
||||
const [newToken, setNewToken] = useState("");
|
||||
const [pendingUsers, setPendingUsers] = useState([]);
|
||||
const [approvedUsers, setApprovedUsers] = useState([]);
|
||||
const workspaceName =
|
||||
workspaces.find((ws) => ws.slug === config.default_workspace)?.name ||
|
||||
config.default_workspace;
|
||||
|
||||
const fetchUsers = useCallback(async () => {
|
||||
const [pending, approved] = await Promise.all([
|
||||
@@ -54,260 +42,14 @@ export default function ConnectedView({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mt-6 flex flex-col gap-y-6">
|
||||
<div className="flex flex-col gap-y-4 max-w-[480px]">
|
||||
<div className="flex items-center gap-x-3 rounded-lg border border-green-500/20 bg-green-500/5 light:border-green-700/20 light:bg-green-500/10 p-4">
|
||||
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-green-500/10">
|
||||
<TelegramLogo
|
||||
className="h-5 w-5 text-green-400 light:text-green-700"
|
||||
weight="fill"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<p className="text-sm font-semibold text-theme-text-primary">
|
||||
@{config.bot_username}
|
||||
</p>
|
||||
<p className="text-xs text-green-400 light:text-green-700">
|
||||
{t("telegram.connected.status")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-y-2 rounded-lg bg-black light:bg-black/5 light:border light:border-black/10 p-4">
|
||||
<WorkspaceName name={workspaceName} />
|
||||
<BotLink username={config.bot_username} />
|
||||
{/*
|
||||
Disabled for now - works fine, but I am not sure we want to enabled this feature.
|
||||
How many people really need a REPLY with voice mode? Even then, we should support on device TTS
|
||||
and more it out the frontend so people can do voice gen without having to pay for it.
|
||||
*/}
|
||||
{/* <VoiceModeSelector config={config} /> */}
|
||||
<DisconnectButton onDisconnected={onDisconnected} />
|
||||
</div>
|
||||
</div>
|
||||
<UsersTable
|
||||
title={t("telegram.users.pending-title")}
|
||||
description={t("telegram.users.pending-description")}
|
||||
users={pendingUsers}
|
||||
isPending
|
||||
fetchUsers={fetchUsers}
|
||||
/>
|
||||
|
||||
<UsersTable
|
||||
title={t("telegram.users.approved-title")}
|
||||
description={t("telegram.users.approved-description")}
|
||||
users={approvedUsers}
|
||||
<div className="flex flex-col gap-y-8 mt-8">
|
||||
<ConnectedBotCard config={config} />
|
||||
<DetailsSection config={config} onDisconnected={onDisconnected} />
|
||||
<UsersSection
|
||||
pendingUsers={pendingUsers}
|
||||
approvedUsers={approvedUsers}
|
||||
fetchUsers={fetchUsers}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function BotLink({ username }) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs text-white">
|
||||
{t("telegram.connected.bot-link")}
|
||||
</span>
|
||||
<Link
|
||||
to={`https://t.me/${username}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-xs text-sky-500 light:text-sky-600 hover:underline flex items-center gap-x-1"
|
||||
>
|
||||
t.me/{username}
|
||||
<ArrowSquareOut className="h-3 w-3" />
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DisconnectButton({ onDisconnected }) {
|
||||
const { t } = useTranslation();
|
||||
const [disconnecting, setDisconnecting] = useState(false);
|
||||
|
||||
async function handleDisconnect() {
|
||||
setDisconnecting(true);
|
||||
const res = await Telegram.disconnect();
|
||||
setDisconnecting(false);
|
||||
|
||||
if (!res.success) {
|
||||
showToast(
|
||||
res.error || t("telegram.connected.toast-disconnect-failed"),
|
||||
"error"
|
||||
);
|
||||
return;
|
||||
}
|
||||
onDisconnected();
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={handleDisconnect}
|
||||
disabled={disconnecting}
|
||||
className="flex items-center justify-center gap-x-2 text-sm font-medium text-white bg-red-800 hover:bg-red-700 light:bg-red-600 light:hover:bg-red-500 light:text-white rounded-lg px-4 py-0.5 w-fit transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{disconnecting ? (
|
||||
<>
|
||||
<CircleNotch className="h-4 w-4 animate-spin" />
|
||||
{t("telegram.connected.disconnecting")}
|
||||
</>
|
||||
) : (
|
||||
t("telegram.connected.disconnect")
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function WorkspaceName({ name }) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs text-white">
|
||||
{t("telegram.connected.workspace")}
|
||||
</span>
|
||||
<span className="text-xs text-white font-medium">{name}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DisconnectedView({ config, onReconnected, newToken, setNewToken }) {
|
||||
const { t } = useTranslation();
|
||||
const [reconnecting, setReconnecting] = useState(false);
|
||||
const [showToken, setShowToken] = useState(false);
|
||||
const Icon = showToken ? Eye : EyeSlash;
|
||||
|
||||
async function handleReconnect(e) {
|
||||
e.preventDefault();
|
||||
if (!newToken.trim()) return;
|
||||
setReconnecting(true);
|
||||
const res = await Telegram.connect(
|
||||
newToken.trim(),
|
||||
config.default_workspace
|
||||
);
|
||||
setReconnecting(false);
|
||||
if (!res.success)
|
||||
return showToast(
|
||||
res.error || t("telegram.connected.toast-reconnect-failed"),
|
||||
"error"
|
||||
);
|
||||
|
||||
setNewToken("");
|
||||
onReconnected({
|
||||
active: true,
|
||||
connected: true,
|
||||
bot_username: res.bot_username,
|
||||
default_workspace: config.default_workspace,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mt-6 flex flex-col gap-y-6">
|
||||
<div className="flex flex-col gap-y-4 max-w-[480px]">
|
||||
<div className="flex flex-col gap-y-3">
|
||||
<div className="flex items-center gap-x-3 rounded-lg border border-red-500/20 bg-red-500/5 p-4">
|
||||
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-red-500/10">
|
||||
<TelegramLogo className="h-5 w-5 text-red-400" weight="fill" />
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<p className="text-sm font-semibold text-white">
|
||||
@{config.bot_username}
|
||||
</p>
|
||||
<p className="text-xs text-red-400">
|
||||
{t("telegram.connected.status-disconnected")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<form onSubmit={handleReconnect} className="flex items-end gap-x-2">
|
||||
<div className="relative w-full">
|
||||
<input
|
||||
type={showToken ? "text" : "password"}
|
||||
value={newToken}
|
||||
onChange={(e) => setNewToken(e.target.value)}
|
||||
placeholder={t("telegram.connected.placeholder-token")}
|
||||
className="w-[99%] bg-theme-settings-input-bg text-theme-text-primary placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5 pr-10"
|
||||
autoComplete="off"
|
||||
/>
|
||||
{newToken.length > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowToken(!showToken)}
|
||||
className="absolute right-2.5 top-1/2 -translate-y-1/2 text-theme-text-secondary hover:text-theme-text-primary transition-colors"
|
||||
>
|
||||
<Icon className="h-5 w-5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={reconnecting}
|
||||
className="flex items-center gap-x-2 text-sm font-medium !text-white bg-sky-500 hover:bg-sky-600 rounded-lg px-4 py-2.5 whitespace-nowrap transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{reconnecting ? (
|
||||
<CircleNotch className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
t("telegram.connected.reconnect")
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
This code is disabled for now - works fine, but I am not sure we want to enabled this feature.
|
||||
How many people really need a REPLY with voice mode? Even then, we should support on device TTS
|
||||
and more it out the frontend so people can do voice gen without having to pay for it.
|
||||
|
||||
When we do enabled this, we should uncomment this code and remove the disabled comment.
|
||||
|
||||
const getVoiceModeOptions = (t) => {
|
||||
return [
|
||||
{ value: "text_only", label: t("telegram.connected.voice-text-only") },
|
||||
{ value: "mirror", label: t("telegram.connected.voice-mirror") },
|
||||
{ value: "always_voice", label: t("telegram.connected.voice-always") },
|
||||
];
|
||||
};
|
||||
|
||||
function VoiceModeSelector({ config }) {
|
||||
const { t } = useTranslation();
|
||||
const [voiceMode, setVoiceMode] = useState(
|
||||
config.voice_response_mode || "text_only"
|
||||
);
|
||||
|
||||
async function handleVoiceModeChange(e) {
|
||||
const mode = e.target.value;
|
||||
setVoiceMode(mode);
|
||||
const res = await Telegram.updateConfig({ voice_response_mode: mode });
|
||||
if (!res.success) {
|
||||
showToast(
|
||||
res.error || t("telegram.connected.toast-voice-failed"),
|
||||
"error"
|
||||
);
|
||||
setVoiceMode(config.voice_response_mode || "text_only");
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs text-white">
|
||||
{t("telegram.connected.voice-response")}
|
||||
</span>
|
||||
<select
|
||||
value={voiceMode}
|
||||
onChange={handleVoiceModeChange}
|
||||
className="text-xs text-right bg-transparent text-white rounded-md px-2 py-1 outline-none max-w-[260px]"
|
||||
>
|
||||
{getVoiceModeOptions(t).map((opt) => (
|
||||
<option key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { QRCodeSVG } from "qrcode.react";
|
||||
import { ShieldCheck, TelegramLogo } from "@phosphor-icons/react";
|
||||
import { TelegramLogo } from "@phosphor-icons/react";
|
||||
import Logo from "@/media/logo/anything-llm-infinity.png";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
@@ -8,72 +8,70 @@ const BOTFATHER_URL = "https://t.me/BotFather";
|
||||
|
||||
export default function CreateBotSection() {
|
||||
const { t } = useTranslation();
|
||||
const qrSize = 180;
|
||||
const logoSize = { width: 35 * 1.2, height: 22 * 1.2 };
|
||||
const qrSize = 137;
|
||||
const logoSize = { width: 35, height: 22 };
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-4">
|
||||
<div className="flex flex-col gap-y-8">
|
||||
<div className="flex flex-col gap-y-1">
|
||||
<p className="text-sm font-semibold text-theme-text-primary">
|
||||
<p className="text-sm light:text-base font-semibold text-white light:text-slate-900">
|
||||
{t("telegram.setup.step1.title")}
|
||||
</p>
|
||||
<p className="text-xs text-theme-text-secondary mb-3">
|
||||
<p className="text-xs text-zinc-400 light:text-slate-600 max-w-[600px]">
|
||||
<Trans
|
||||
i18nKey="telegram.setup.step1.description"
|
||||
components={{
|
||||
code: (
|
||||
<code className="bg-theme-bg-primary px-1 py-0.5 rounded text-theme-text-primary" />
|
||||
<code className="bg-zinc-800 light:bg-slate-200 px-1 py-0.5 rounded text-white light:text-slate-900" />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
<div className="flex items-start gap-x-6 flex-wrap gap-y-4">
|
||||
<div className="flex flex-col items-center gap-y-2">
|
||||
<div className="bg-white/10 light:bg-black/5 rounded-lg p-4 flex items-center justify-center">
|
||||
<QRCodeSVG
|
||||
value={BOTFATHER_URL}
|
||||
size={qrSize}
|
||||
bgColor="transparent"
|
||||
fgColor="currentColor"
|
||||
className="text-white light:text-black light:[&_image]:invert"
|
||||
level="L"
|
||||
imageSettings={{
|
||||
src: Logo,
|
||||
x: qrSize / 2 - logoSize.width / 2,
|
||||
y: qrSize / 2 - logoSize.height / 2,
|
||||
height: logoSize.height,
|
||||
width: logoSize.width,
|
||||
excavate: true,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-y-3 pt-2">
|
||||
<Link
|
||||
to={BOTFATHER_URL}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="flex items-center gap-x-2 text-sm font-medium text-white bg-sky-500 hover:bg-sky-600 rounded-lg px-4 py-2.5 w-fit transition-colors duration-200"
|
||||
>
|
||||
<TelegramLogo className="h-4 w-4" weight="fill" />
|
||||
{t("telegram.setup.step1.open-botfather")}
|
||||
</Link>
|
||||
<div className="flex flex-col gap-y-1.5 text-xs text-theme-text-secondary">
|
||||
<p>{t("telegram.setup.step1.instruction-1")}</p>
|
||||
<p>
|
||||
<Trans
|
||||
i18nKey="telegram.setup.step1.instruction-2"
|
||||
components={{
|
||||
code: (
|
||||
<code className="bg-theme-bg-primary px-1 py-0.5 rounded text-theme-text-primary" />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
<p>{t("telegram.setup.step1.instruction-3")}</p>
|
||||
<p>{t("telegram.setup.step1.instruction-4")}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-x-8">
|
||||
<div className="flex flex-col items-start gap-y-2">
|
||||
<div className="bg-zinc-700 light:bg-slate-200 rounded-2xl p-[18px]">
|
||||
<QRCodeSVG
|
||||
value={BOTFATHER_URL}
|
||||
size={qrSize}
|
||||
bgColor="transparent"
|
||||
fgColor="currentColor"
|
||||
className="text-white light:text-slate-900 light:[&_image]:invert"
|
||||
level="L"
|
||||
imageSettings={{
|
||||
src: Logo,
|
||||
x: qrSize / 2 - logoSize.width / 2,
|
||||
y: qrSize / 2 - logoSize.height / 2,
|
||||
height: logoSize.height,
|
||||
width: logoSize.width,
|
||||
excavate: true,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Link
|
||||
to={BOTFATHER_URL}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="flex items-center justify-center gap-x-1.5 text-sm font-medium bg-zinc-50 light:bg-slate-900 text-zinc-900 light:text-white rounded-lg h-9 w-[172px] hover:opacity-90 transition-opacity duration-200"
|
||||
>
|
||||
<TelegramLogo className="h-5 w-5" weight="fill" />
|
||||
{t("telegram.setup.step1.open-botfather")}
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex flex-col text-sm text-white light:text-slate-900">
|
||||
<p className="leading-5">{t("telegram.setup.step1.instruction-1")}</p>
|
||||
<p className="leading-5">
|
||||
<Trans
|
||||
i18nKey="telegram.setup.step1.instruction-2"
|
||||
components={{
|
||||
code: (
|
||||
<code className="bg-zinc-800 light:bg-slate-200 px-1 py-0.5 rounded" />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
<p className="leading-5">{t("telegram.setup.step1.instruction-3")}</p>
|
||||
<p className="leading-5">{t("telegram.setup.step1.instruction-4")}</p>
|
||||
</div>
|
||||
</div>
|
||||
<SecurityTips />
|
||||
@@ -85,32 +83,16 @@ function SecurityTips() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-2 p-3 rounded-lg bg-theme-bg-primary border border-theme-sidebar-border">
|
||||
<div className="flex items-center gap-x-2">
|
||||
<ShieldCheck
|
||||
className="h-4 w-4 text-theme-text-secondary"
|
||||
weight="bold"
|
||||
/>
|
||||
<p className="text-xs font-semibold text-theme-text-primary">
|
||||
{t("telegram.setup.security.title")}
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-xs text-theme-text-secondary">
|
||||
<div className="border border-zinc-600 light:border-slate-400 rounded-xl p-[18px] max-w-[640px]">
|
||||
<p className="text-sm font-medium text-white light:text-slate-900 mb-1">
|
||||
{t("telegram.setup.security.title")}
|
||||
</p>
|
||||
<p className="text-sm text-zinc-400 light:text-slate-600 mb-2">
|
||||
{t("telegram.setup.security.description")}
|
||||
</p>
|
||||
<ul className="text-xs text-theme-text-secondary list-disc list-inside space-y-1 ml-1">
|
||||
<li>
|
||||
<code className="bg-theme-bg-secondary px-1 py-0.5 rounded text-theme-text-primary">
|
||||
Disable Groups
|
||||
</code>{" "}
|
||||
{t("telegram.setup.security.disable-groups")}
|
||||
</li>
|
||||
<li>
|
||||
<code className="bg-theme-bg-secondary px-1 py-0.5 rounded text-theme-text-primary">
|
||||
Disable Inline
|
||||
</code>{" "}
|
||||
{t("telegram.setup.security.disable-inline")}
|
||||
</li>
|
||||
<ul className="text-sm text-zinc-400 light:text-slate-600 list-disc list-inside space-y-0.5">
|
||||
<li>Disable Groups {t("telegram.setup.security.disable-groups")}</li>
|
||||
<li>Disable Inline {t("telegram.setup.security.disable-inline")}</li>
|
||||
<li>{t("telegram.setup.security.obscure-username")}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -10,12 +10,9 @@ import showToast from "@/utils/toast";
|
||||
import CreateBotSection from "./CreateBotSection";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function SetupView({ workspaces, onConnected }) {
|
||||
export default function SetupView({ onConnected }) {
|
||||
const { t } = useTranslation();
|
||||
const [botToken, setBotToken] = useState("");
|
||||
const [selectedWorkspace, setSelectedWorkspace] = useState(
|
||||
workspaces[0]?.slug || ""
|
||||
);
|
||||
const [connecting, setConnecting] = useState(false);
|
||||
|
||||
async function handleConnect(e) {
|
||||
@@ -24,58 +21,48 @@ export default function SetupView({ workspaces, onConnected }) {
|
||||
return showToast(t("telegram.setup.toast-enter-token"), "error");
|
||||
|
||||
setConnecting(true);
|
||||
const res = await Telegram.connect(botToken.trim(), selectedWorkspace);
|
||||
const res = await Telegram.connect(botToken.trim());
|
||||
setConnecting(false);
|
||||
|
||||
if (!res.success) {
|
||||
showToast(res.error || t("telegram.setup.toast-connect-failed"), "error");
|
||||
return;
|
||||
}
|
||||
onConnected({
|
||||
active: true,
|
||||
connected: true,
|
||||
bot_username: res.bot_username,
|
||||
default_workspace: selectedWorkspace || null,
|
||||
});
|
||||
|
||||
const configRes = await Telegram.getConfig();
|
||||
onConnected(configRes?.config);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-6 mt-6">
|
||||
<div className="flex flex-col gap-y-8 mt-8">
|
||||
<CreateBotSection />
|
||||
<form onSubmit={handleConnect} className="flex flex-col gap-y-4">
|
||||
<div className="flex flex-col gap-y-1">
|
||||
<p className="text-sm font-semibold text-theme-text-primary">
|
||||
<form onSubmit={handleConnect} className="flex flex-col gap-y-[18px]">
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<p className="text-sm light:text-base font-semibold text-white light:text-slate-900">
|
||||
{t("telegram.setup.step2.title")}
|
||||
</p>
|
||||
<p className="text-xs text-theme-text-secondary">
|
||||
<p className="text-xs text-zinc-400 light:text-slate-600 max-w-[700px]">
|
||||
{t("telegram.setup.step2.description")}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-y-4 max-w-[480px]">
|
||||
<BotTokenInput botToken={botToken} setBotToken={setBotToken} />
|
||||
<WorkspaceSelect
|
||||
workspaces={workspaces}
|
||||
selectedWorkspace={selectedWorkspace}
|
||||
setSelectedWorkspace={setSelectedWorkspace}
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={connecting}
|
||||
className="flex items-center justify-center gap-x-2 text-sm font-medium text-white bg-sky-500 hover:bg-sky-600 rounded-lg px-4 py-2.5 w-fit transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{connecting ? (
|
||||
<>
|
||||
<CircleNotch className="h-4 w-4 animate-spin" />
|
||||
{t("telegram.setup.step2.connecting")}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<TelegramLogo className="h-4 w-4" weight="bold" />
|
||||
{t("telegram.setup.step2.connect-bot")}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
<BotTokenInput botToken={botToken} setBotToken={setBotToken} />
|
||||
<button
|
||||
type="submit"
|
||||
disabled={connecting}
|
||||
className="flex items-center justify-center gap-x-1.5 text-sm font-medium bg-zinc-50 light:bg-slate-900 text-zinc-900 light:text-white rounded-lg h-9 px-5 w-fit hover:opacity-90 transition-opacity duration-200 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{connecting ? (
|
||||
<>
|
||||
<CircleNotch className="h-4 w-4 animate-spin" />
|
||||
{t("telegram.setup.step2.connecting")}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<TelegramLogo className="h-5 w-5" weight="fill" />
|
||||
{t("telegram.setup.step2.connect-bot")}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
@@ -87,73 +74,27 @@ function BotTokenInput({ botToken, setBotToken }) {
|
||||
const Icon = showToken ? Eye : EyeSlash;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-1">
|
||||
<label className="text-xs font-medium text-theme-text-secondary">
|
||||
<div className="flex flex-col gap-y-1.5 w-[320px]">
|
||||
<label className="text-sm font-medium text-zinc-200 light:text-slate-900">
|
||||
{t("telegram.setup.step2.bot-token")}
|
||||
</label>
|
||||
<div className="relative">
|
||||
<div className="bg-zinc-800 light:bg-white light:border light:border-slate-300 h-8 rounded-lg px-3.5 flex items-center gap-x-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowToken(!showToken)}
|
||||
className="text-zinc-400 light:text-slate-500 hover:text-zinc-300 light:hover:text-slate-700 transition-colors shrink-0"
|
||||
>
|
||||
<Icon className="h-4 w-4" />
|
||||
</button>
|
||||
<input
|
||||
type={showToken ? "text" : "password"}
|
||||
value={botToken}
|
||||
onChange={(e) => setBotToken(e.target.value)}
|
||||
placeholder="123456:ABC-DEF1234ghIkl-zyx57W2v..."
|
||||
className="bg-theme-settings-input-bg text-theme-text-primary placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5 pr-10"
|
||||
placeholder="123456:ABC-DEF123ghlkl-zyx57W2v"
|
||||
className="bg-transparent flex-1 text-sm text-white light:text-slate-900 placeholder:text-zinc-400 light:placeholder:text-slate-500 outline-none min-w-0"
|
||||
autoComplete="off"
|
||||
/>
|
||||
{botToken.length > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowToken(!showToken)}
|
||||
className="absolute right-2.5 top-1/2 -translate-y-1/2 text-theme-text-secondary hover:text-theme-text-primary transition-colors"
|
||||
>
|
||||
<Icon className="h-5 w-5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function WorkspaceSelect({
|
||||
workspaces,
|
||||
selectedWorkspace,
|
||||
setSelectedWorkspace,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (!workspaces.length) {
|
||||
return (
|
||||
<div className="flex flex-col gap-y-1">
|
||||
<label className="text-xs font-medium text-theme-text-secondary">
|
||||
{t("telegram.setup.step2.default-workspace")}{" "}
|
||||
<span className="italic font-normal">({t("common.optional")})</span>
|
||||
</label>
|
||||
<input
|
||||
disabled
|
||||
placeholder={t("telegram.setup.step2.no-workspace")}
|
||||
className="bg-theme-settings-input-bg text-theme-text-primary text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-1">
|
||||
<label className="text-xs font-medium text-theme-text-secondary">
|
||||
{t("telegram.setup.step2.default-workspace")}{" "}
|
||||
<span className="italic font-normal">({t("common.optional")})</span>
|
||||
</label>
|
||||
<select
|
||||
value={selectedWorkspace}
|
||||
onChange={(e) => setSelectedWorkspace(e.target.value)}
|
||||
className="bg-theme-settings-input-bg text-theme-text-primary text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5"
|
||||
>
|
||||
{workspaces.map((ws) => (
|
||||
<option key={ws.slug} value={ws.slug}>
|
||||
{ws.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import Sidebar from "@/components/SettingsSidebar";
|
||||
import { isMobile } from "react-device-detect";
|
||||
import { CircleNotch } from "@phosphor-icons/react";
|
||||
import Workspace from "@/models/workspace";
|
||||
import Telegram from "@/models/telegram";
|
||||
import ConnectedView from "./ConnectedView";
|
||||
import SetupView from "./SetupView";
|
||||
@@ -11,21 +11,19 @@ import System from "@/models/system";
|
||||
import paths from "@/utils/paths";
|
||||
|
||||
export default function TelegramBotSettings() {
|
||||
const navigate = useNavigate();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [config, setConfig] = useState(null);
|
||||
const [workspaces, setWorkspaces] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchData() {
|
||||
const [isMultiUserMode, configRes, allWorkspaces] = await Promise.all([
|
||||
const [isMultiUserMode, configRes] = await Promise.all([
|
||||
System.isMultiUserMode(),
|
||||
Telegram.getConfig(),
|
||||
Workspace.all(),
|
||||
]);
|
||||
|
||||
if (isMultiUserMode) window.location = paths.home();
|
||||
if (isMultiUserMode) navigate(paths.home());
|
||||
setConfig(configRes?.config || null);
|
||||
setWorkspaces(allWorkspaces || []);
|
||||
setLoading(false);
|
||||
}
|
||||
fetchData();
|
||||
@@ -38,7 +36,7 @@ export default function TelegramBotSettings() {
|
||||
return (
|
||||
<ConnectionsLayout>
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<CircleNotch className="h-8 w-8 text-theme-text-secondary animate-spin" />
|
||||
<CircleNotch className="h-8 w-8 text-zinc-400 light:text-slate-400 animate-spin" />
|
||||
</div>
|
||||
</ConnectionsLayout>
|
||||
);
|
||||
@@ -48,7 +46,7 @@ export default function TelegramBotSettings() {
|
||||
if (!hasConfig) {
|
||||
return (
|
||||
<ConnectionsLayout fullPage={true}>
|
||||
<SetupView workspaces={workspaces} onConnected={handleConnected} />
|
||||
<SetupView onConnected={handleConnected} />
|
||||
</ConnectionsLayout>
|
||||
);
|
||||
}
|
||||
@@ -57,7 +55,6 @@ export default function TelegramBotSettings() {
|
||||
<ConnectionsLayout fullPage={true}>
|
||||
<ConnectedView
|
||||
config={config}
|
||||
workspaces={workspaces}
|
||||
onDisconnected={handleDisconnected}
|
||||
onReconnected={handleConnected}
|
||||
/>
|
||||
@@ -68,23 +65,29 @@ export default function TelegramBotSettings() {
|
||||
function ConnectionsLayout({ children, fullPage = false }) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="w-screen h-screen overflow-hidden bg-theme-bg-container flex md:mt-0 mt-6">
|
||||
<div className="w-screen h-screen overflow-hidden bg-zinc-950 light:bg-slate-50 flex md:mt-0 mt-6">
|
||||
<Sidebar />
|
||||
<div
|
||||
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
|
||||
className="relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-theme-bg-secondary w-full h-full overflow-y-scroll p-4 md:p-0"
|
||||
className="relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-2xl bg-zinc-900 light:bg-white light:border light:border-slate-300 w-full h-full overflow-y-scroll p-4 md:p-0"
|
||||
>
|
||||
{fullPage ? (
|
||||
<div className="flex flex-col w-full px-1 md:pl-6 md:pr-[50px] md:py-6 py-16">
|
||||
<div className="w-full flex flex-col gap-y-1 pb-4 border-white/10 border-b-2">
|
||||
<div className="items-center flex gap-x-4">
|
||||
<p className="text-lg leading-6 font-bold text-theme-text-primary">
|
||||
{t("telegram.title")}
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-xs leading-[18px] font-base text-theme-text-secondary mt-2">
|
||||
<div className="w-full flex flex-col gap-y-2 pb-6 border-b border-white/20 light:border-slate-300">
|
||||
<p className="text-lg font-semibold leading-7 text-white light:text-slate-900">
|
||||
{t("telegram.title")}
|
||||
</p>
|
||||
<p className="text-xs leading-4 text-zinc-400 light:text-slate-600 max-w-[700px]">
|
||||
{t("telegram.description")}
|
||||
</p>
|
||||
<a
|
||||
href={paths.docs("/channels/telegram")}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-xs leading-4 text-white light:text-slate-900 underline w-fit"
|
||||
>
|
||||
View Documentation
|
||||
</a>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@@ -52,8 +52,8 @@ export default {
|
||||
discord: () => {
|
||||
return "https://discord.com/invite/6UyHPeGZAC";
|
||||
},
|
||||
docs: () => {
|
||||
return "https://docs.anythingllm.com";
|
||||
docs: (path = "") => {
|
||||
return `https://docs.anythingllm.com${path}`;
|
||||
},
|
||||
chatModes: () => {
|
||||
return "https://docs.anythingllm.com/features/chat-modes";
|
||||
|
||||
@@ -8,6 +8,7 @@ const { isSingleUserMode } = require("../utils/middleware/multiUserProtected");
|
||||
const { reqBody } = require("../utils/http");
|
||||
const { EventLogs } = require("../models/eventLogs");
|
||||
const { Workspace } = require("../models/workspace");
|
||||
const { WorkspaceThread } = require("../models/workspaceThread");
|
||||
const { encryptToken } = require("../utils/telegramBot/utils");
|
||||
|
||||
function telegramEndpoints(app) {
|
||||
@@ -24,12 +25,32 @@ function telegramEndpoints(app) {
|
||||
}
|
||||
|
||||
const service = new TelegramBotService();
|
||||
|
||||
// Resolve workspace, thread, and model from the first approved user's
|
||||
// active state, falling back to the default workspace config.
|
||||
const approvedUsers = connector.config.approved_users || [];
|
||||
const activeUser = approvedUsers[0];
|
||||
const workspaceSlug =
|
||||
activeUser?.active_workspace ||
|
||||
connector.config.default_workspace ||
|
||||
null;
|
||||
const threadSlug = activeUser?.active_thread || null;
|
||||
|
||||
let workspace = await Workspace.get({ slug: workspaceSlug });
|
||||
if (!workspace) {
|
||||
const available = await Workspace.where({}, 1);
|
||||
if (available.length) workspace = available[0];
|
||||
}
|
||||
const thread = await WorkspaceThread.get({ slug: threadSlug });
|
||||
|
||||
return response.status(200).json({
|
||||
config: {
|
||||
active: connector.active,
|
||||
connected: service.isRunning,
|
||||
bot_username: connector.config.bot_username || null,
|
||||
default_workspace: connector.config.default_workspace || null,
|
||||
default_workspace: workspace?.name || workspaceSlug || "—",
|
||||
active_thread_name: thread?.name || "Default",
|
||||
chat_model: workspace?.chatModel || "System default",
|
||||
voice_response_mode:
|
||||
connector.config.voice_response_mode || "text_only",
|
||||
},
|
||||
|
||||
@@ -324,6 +324,7 @@ class TelegramBotService {
|
||||
#setupHandlers() {
|
||||
const ctx = this.#createContext();
|
||||
const guard = async (msg, handler) => {
|
||||
if (!this.#config) return;
|
||||
if (!isVerified(this.#config.approved_users, msg.chat.id)) {
|
||||
sendPairingRequest(this.#bot, msg, this.#pendingPairings);
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user