Merge branch 'master' of github.com:Mintplex-Labs/anything-llm

This commit is contained in:
Timothy Carambat
2026-03-31 20:51:46 -07:00
13 changed files with 576 additions and 619 deletions

View File

@@ -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.",

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}
*/

View File

@@ -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" />
</>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}
*/

View File

@@ -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>

View File

@@ -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>
);
}

View File

@@ -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>

View File

@@ -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";

View File

@@ -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",
},

View File

@@ -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;