mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-05 02:41:56 +00:00
feat(ui): i18n for chat conversation list and MCP panels
This commit is contained in:
parent
ae6818f83c
commit
caf77c8419
6 changed files with 244 additions and 74 deletions
|
|
@ -11,6 +11,7 @@ import {
|
|||
MessageOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import dayjs from "dayjs";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Conversation } from "./types";
|
||||
|
||||
const { Text } = Typography;
|
||||
|
|
@ -71,6 +72,7 @@ interface ConversationRowProps {
|
|||
}
|
||||
|
||||
const ConversationRow: React.FC<ConversationRowProps> = ({ conv, isActive, onSelect, onDelete, onRename }) => {
|
||||
const { t } = useTranslation();
|
||||
const [editing, setEditing] = useState(false);
|
||||
const [editValue, setEditValue] = useState(conv.title);
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
|
|
@ -181,7 +183,7 @@ const ConversationRow: React.FC<ConversationRowProps> = ({ conv, isActive, onSel
|
|||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Tooltip title="Rename">
|
||||
<Tooltip title={t("chat.conversationList.rename")}>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
|
|
@ -191,13 +193,13 @@ const ConversationRow: React.FC<ConversationRowProps> = ({ conv, isActive, onSel
|
|||
/>
|
||||
</Tooltip>
|
||||
<Popconfirm
|
||||
title="Delete this conversation?"
|
||||
title={t("chat.conversationList.deleteConversationTitle")}
|
||||
onConfirm={() => onDelete(conv.id)}
|
||||
okText="Delete"
|
||||
cancelText="Cancel"
|
||||
okText={t("common.delete")}
|
||||
cancelText={t("common.cancel")}
|
||||
okButtonProps={{ danger: true }}
|
||||
>
|
||||
<Tooltip title="Delete">
|
||||
<Tooltip title={t("common.delete")}>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
|
|
@ -224,6 +226,7 @@ interface SearchModalProps {
|
|||
}
|
||||
|
||||
const SearchModal: React.FC<SearchModalProps> = ({ open, conversations, onSelect, onClose }) => {
|
||||
const { t } = useTranslation();
|
||||
const [query, setQuery] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -251,7 +254,7 @@ const SearchModal: React.FC<SearchModalProps> = ({ open, conversations, onSelect
|
|||
<Input
|
||||
autoFocus
|
||||
prefix={<SearchOutlined style={{ color: "#bbb" }} />}
|
||||
placeholder="Search conversations…"
|
||||
placeholder={t("chat.conversationList.searchPlaceholder")}
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
style={{ marginBottom: 12 }}
|
||||
|
|
@ -260,7 +263,9 @@ const SearchModal: React.FC<SearchModalProps> = ({ open, conversations, onSelect
|
|||
|
||||
<div style={{ maxHeight: 320, overflowY: "auto" }}>
|
||||
{filtered.length === 0 ? (
|
||||
<div style={{ textAlign: "center", padding: "24px 0", color: "#999" }}>No conversations found</div>
|
||||
<div style={{ textAlign: "center", padding: "24px 0", color: "#999" }}>
|
||||
{t("chat.conversationList.noConversationsFound")}
|
||||
</div>
|
||||
) : (
|
||||
filtered.map((conv) => {
|
||||
const truncated = conv.title.length > 55 ? conv.title.slice(0, 55) + "…" : conv.title;
|
||||
|
|
@ -308,8 +313,19 @@ const ConversationList: React.FC<Props> = ({
|
|||
onNewChat,
|
||||
onRename,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [searchModalOpen, setSearchModalOpen] = useState(false);
|
||||
|
||||
const dateGroupLabel = (group: DateGroup): string => {
|
||||
const keyMap: Record<DateGroup, string> = {
|
||||
Today: "chat.conversationList.dateGroupToday",
|
||||
Yesterday: "chat.conversationList.dateGroupYesterday",
|
||||
"Last 7 Days": "chat.conversationList.dateGroupLast7Days",
|
||||
Older: "chat.conversationList.dateGroupOlder",
|
||||
};
|
||||
return t(keyMap[group]);
|
||||
};
|
||||
|
||||
// Cmd+K / Ctrl+K listener
|
||||
const handleGlobalKeyDown = useCallback((e: KeyboardEvent) => {
|
||||
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
|
||||
|
|
@ -345,12 +361,9 @@ const ConversationList: React.FC<Props> = ({
|
|||
>
|
||||
{/* Top: New Chat button */}
|
||||
<div style={{ padding: "12px 10px 8px" }}>
|
||||
<Tooltip
|
||||
title="Chats are saved locally in this browser. All requests are logged in Spend → Logs."
|
||||
placement="right"
|
||||
>
|
||||
<Tooltip title={t("chat.conversationList.chatsLocalStorageTooltip")} placement="right">
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={onNewChat} style={{ width: "100%" }}>
|
||||
New Chat
|
||||
{t("chat.conversationList.newChat")}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
|
@ -371,11 +384,10 @@ const ConversationList: React.FC<Props> = ({
|
|||
fontSize: 12,
|
||||
marginTop: 32,
|
||||
padding: "0 12px",
|
||||
whiteSpace: "pre-line",
|
||||
}}
|
||||
>
|
||||
No conversations yet.
|
||||
<br />
|
||||
Start a new chat above.
|
||||
{t("chat.conversationList.noConversationsYet")}
|
||||
</div>
|
||||
) : (
|
||||
grouped.map(({ group, items }) => (
|
||||
|
|
@ -390,7 +402,7 @@ const ConversationList: React.FC<Props> = ({
|
|||
padding: "8px 8px 4px",
|
||||
}}
|
||||
>
|
||||
{group}
|
||||
{dateGroupLabel(group)}
|
||||
</div>
|
||||
{items.map((conv) => (
|
||||
<ConversationRow
|
||||
|
|
@ -431,7 +443,7 @@ const ConversationList: React.FC<Props> = ({
|
|||
textOverflow: "ellipsis",
|
||||
}}
|
||||
>
|
||||
My Account
|
||||
{t("chat.conversationList.myAccount")}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { Spin, Input, Button, Skeleton } from "antd";
|
||||
import { SearchOutlined, ArrowLeftOutlined, RightOutlined, ToolOutlined, CheckCircleOutlined } from "@ant-design/icons";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
deleteMCPOAuthUserCredential,
|
||||
fetchMCPServers,
|
||||
|
|
@ -29,6 +30,7 @@ const OAuth2ConnectButton: React.FC<OAuth2ConnectButtonProps> = ({
|
|||
onConnect,
|
||||
variant = "badge",
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const name = server.server_name ?? server.alias ?? server.server_id;
|
||||
const { startOAuthFlow, status } = useUserMcpOAuthFlow({
|
||||
accessToken,
|
||||
|
|
@ -47,7 +49,7 @@ const OAuth2ConnectButton: React.FC<OAuth2ConnectButtonProps> = ({
|
|||
onClick={startOAuthFlow}
|
||||
style={{ borderRadius: 8, fontWeight: 600, height: 38, minWidth: 110 }}
|
||||
>
|
||||
{loading ? "Connecting…" : "Connect"}
|
||||
{loading ? t("chat.mCPAppsPanel.connecting") : t("chat.mCPAppsPanel.connect")}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
|
@ -70,7 +72,7 @@ const OAuth2ConnectButton: React.FC<OAuth2ConnectButtonProps> = ({
|
|||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
{loading ? "Connecting…" : "Connect"}
|
||||
{loading ? t("chat.mCPAppsPanel.connecting") : t("chat.mCPAppsPanel.connect")}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
|
@ -104,6 +106,7 @@ function getAvatarColor(name: string): string {
|
|||
type TabKey = "all" | "connected";
|
||||
|
||||
const MCPAppsPanel: React.FC<Props> = ({ accessToken, selectedServers, onChange }) => {
|
||||
const { t } = useTranslation();
|
||||
const [servers, setServers] = useState<MCPServer[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [query, setQuery] = useState("");
|
||||
|
|
@ -228,7 +231,7 @@ const MCPAppsPanel: React.FC<Props> = ({ accessToken, selectedServers, onChange
|
|||
const idToFetch = serverId ?? serverName;
|
||||
const result = await listMCPTools(accessToken, idToFetch);
|
||||
if (result?.error) {
|
||||
MessageManager.warning(`Could not load tools for ${serverName}`);
|
||||
MessageManager.warning(t("chat.mCPAppsPanel.couldNotLoadTools", { name: serverName }));
|
||||
return;
|
||||
}
|
||||
// Use the ref so we read the most up-to-date list; guard against duplicates
|
||||
|
|
@ -237,7 +240,7 @@ const MCPAppsPanel: React.FC<Props> = ({ accessToken, selectedServers, onChange
|
|||
onChange([...selectedServersRef.current, serverName]);
|
||||
}
|
||||
} catch {
|
||||
MessageManager.warning(`Could not load tools for ${serverName}`);
|
||||
MessageManager.warning(t("chat.mCPAppsPanel.couldNotLoadTools", { name: serverName }));
|
||||
} finally {
|
||||
setTogglingOn((prev) => {
|
||||
const next = new Set(prev);
|
||||
|
|
@ -313,7 +316,7 @@ const MCPAppsPanel: React.FC<Props> = ({ accessToken, selectedServers, onChange
|
|||
}}
|
||||
>
|
||||
<ArrowLeftOutlined style={{ fontSize: 12 }} />
|
||||
Back
|
||||
{t("common.back")}
|
||||
</button>
|
||||
|
||||
{/* Avatar + name + connect */}
|
||||
|
|
@ -321,7 +324,7 @@ const MCPAppsPanel: React.FC<Props> = ({ accessToken, selectedServers, onChange
|
|||
{detailServer.mcp_info?.logo_url ? (
|
||||
<img
|
||||
src={detailServer.mcp_info.logo_url}
|
||||
alt={`${name} logo`}
|
||||
alt={t("chat.mCPAppsPanel.logoAlt", { name })}
|
||||
style={{
|
||||
width: 64,
|
||||
height: 64,
|
||||
|
|
@ -356,7 +359,9 @@ const MCPAppsPanel: React.FC<Props> = ({ accessToken, selectedServers, onChange
|
|||
</div>
|
||||
<div style={{ flex: 1 }}>
|
||||
<h2 style={{ margin: "0 0 4px", fontSize: 22, fontWeight: 700, color: "#111827" }}>{name}</h2>
|
||||
<p style={{ margin: 0, fontSize: 14, color: "#6b7280" }}>{detailServer.description ?? "MCP server"}</p>
|
||||
<p style={{ margin: 0, fontSize: 14, color: "#6b7280" }}>
|
||||
{detailServer.description ?? t("chat.mCPAppsPanel.mcpServerFallbackDesc")}
|
||||
</p>
|
||||
</div>
|
||||
{detailServer.auth_type === AUTH_TYPE.OAUTH2 ? (
|
||||
oauthConnected.has(detailServer.server_id) ? (
|
||||
|
|
@ -378,7 +383,7 @@ const MCPAppsPanel: React.FC<Props> = ({ accessToken, selectedServers, onChange
|
|||
}}
|
||||
style={{ borderRadius: 8, fontWeight: 600, height: 38, minWidth: 110 }}
|
||||
>
|
||||
Disconnect
|
||||
{t("chat.mCPAppsPanel.disconnect")}
|
||||
</Button>
|
||||
) : (
|
||||
<OAuth2ConnectButton
|
||||
|
|
@ -397,18 +402,20 @@ const MCPAppsPanel: React.FC<Props> = ({ accessToken, selectedServers, onChange
|
|||
onClick={() => handleToggle(name, !isConnected, detailServer.server_id)}
|
||||
style={{ borderRadius: 8, fontWeight: 600, height: 38, minWidth: 110 }}
|
||||
>
|
||||
{isConnected ? "Disconnect" : "Connect"}
|
||||
{isConnected ? t("chat.mCPAppsPanel.disconnect") : t("chat.mCPAppsPanel.connect")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Info table */}
|
||||
<h3 style={{ margin: "0 0 12px", fontSize: 15, fontWeight: 600, color: "#111827" }}>Information</h3>
|
||||
<h3 style={{ margin: "0 0 12px", fontSize: 15, fontWeight: 600, color: "#111827" }}>
|
||||
{t("chat.mCPAppsPanel.information")}
|
||||
</h3>
|
||||
<div style={{ border: "1px solid #e5e7eb", borderRadius: 10, overflow: "hidden", marginBottom: 28 }}>
|
||||
{[
|
||||
["Server ID", detailServer.server_id],
|
||||
["Transport", handleTransport(detailServer.transport, detailServer.spec_path)],
|
||||
["Status", isConnected ? "Connected" : "Not connected"],
|
||||
[t("chat.mCPAppsPanel.serverIdLabel"), detailServer.server_id],
|
||||
[t("chat.mCPAppsPanel.transportLabel"), handleTransport(detailServer.transport, detailServer.spec_path)],
|
||||
[t("common.status"), isConnected ? t("chat.mCPAppsPanel.connected") : t("chat.mCPAppsPanel.notConnected")],
|
||||
]
|
||||
.filter(([, v]) => v)
|
||||
.map(([label, value], i, arr) => (
|
||||
|
|
@ -429,7 +436,9 @@ const MCPAppsPanel: React.FC<Props> = ({ accessToken, selectedServers, onChange
|
|||
|
||||
{/* Tools section */}
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 12 }}>
|
||||
<h3 style={{ margin: 0, fontSize: 15, fontWeight: 600, color: "#111827" }}>Available Tools</h3>
|
||||
<h3 style={{ margin: 0, fontSize: 15, fontWeight: 600, color: "#111827" }}>
|
||||
{t("chat.mCPAppsPanel.availableTools")}
|
||||
</h3>
|
||||
{!loadingTools && (
|
||||
<span
|
||||
style={{
|
||||
|
|
@ -450,7 +459,9 @@ const MCPAppsPanel: React.FC<Props> = ({ accessToken, selectedServers, onChange
|
|||
<Spin size="small" />
|
||||
</div>
|
||||
) : detailTools.length === 0 ? (
|
||||
<div style={{ color: "#9ca3af", fontSize: 13, padding: "8px 0" }}>No tools available</div>
|
||||
<div style={{ color: "#9ca3af", fontSize: 13, padding: "8px 0" }}>
|
||||
{t("chat.mCPAppsPanel.noToolsAvailable")}
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
||||
{detailTools.map((tool) => (
|
||||
|
|
@ -496,7 +507,9 @@ const MCPAppsPanel: React.FC<Props> = ({ accessToken, selectedServers, onChange
|
|||
>
|
||||
<div>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 4 }}>
|
||||
<h2 style={{ margin: 0, fontSize: 18, fontWeight: 600, color: "#111827" }}>MCP Servers</h2>
|
||||
<h2 style={{ margin: 0, fontSize: 18, fontWeight: 600, color: "#111827" }}>
|
||||
{t("chat.mCPAppsPanel.mcpServersTitle")}
|
||||
</h2>
|
||||
<span
|
||||
style={{
|
||||
fontSize: 10,
|
||||
|
|
@ -509,29 +522,27 @@ const MCPAppsPanel: React.FC<Props> = ({ accessToken, selectedServers, onChange
|
|||
textTransform: "uppercase",
|
||||
}}
|
||||
>
|
||||
Beta
|
||||
{t("chat.mCPAppsPanel.beta")}
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 12 }}>
|
||||
<p style={{ margin: 0, fontSize: 13, color: "#6b7280" }}>
|
||||
Browse tools, authenticate once, use in chat — no setup needed.
|
||||
</p>
|
||||
<p style={{ margin: 0, fontSize: 13, color: "#6b7280" }}>{t("chat.mCPAppsPanel.browsePitch")}</p>
|
||||
{loadingCounts ? (
|
||||
<span style={{ display: "flex", alignItems: "center", gap: 5, fontSize: 12, color: "#9ca3af" }}>
|
||||
<Spin size="small" style={{ transform: "scale(0.7)" }} />
|
||||
Loading tools...
|
||||
{t("chat.mCPAppsPanel.loadingTools")}
|
||||
</span>
|
||||
) : totalTools > 0 ? (
|
||||
<span style={{ display: "flex", alignItems: "center", gap: 4, fontSize: 12, color: "#6b7280" }}>
|
||||
<ToolOutlined style={{ fontSize: 11 }} />
|
||||
{totalTools} tool{totalTools !== 1 ? "s" : ""} available
|
||||
{t("chat.mCPAppsPanel.toolsAvailable", { count: totalTools })}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<Input
|
||||
prefix={<SearchOutlined style={{ color: "#9ca3af", fontSize: 13 }} />}
|
||||
placeholder="Search servers..."
|
||||
placeholder={t("chat.mCPAppsPanel.searchServersPlaceholder")}
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
allowClear
|
||||
|
|
@ -558,7 +569,11 @@ const MCPAppsPanel: React.FC<Props> = ({ accessToken, selectedServers, onChange
|
|||
marginBottom: -1,
|
||||
}}
|
||||
>
|
||||
{tab === "all" ? "All" : `Connected${connectedCount > 0 ? ` (${connectedCount})` : ""}`}
|
||||
{tab === "all"
|
||||
? t("chat.mCPAppsPanel.tabAll")
|
||||
: connectedCount > 0
|
||||
? t("chat.mCPAppsPanel.tabConnectedCount", { count: connectedCount })
|
||||
: t("chat.mCPAppsPanel.tabConnected")}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
|
@ -571,10 +586,10 @@ const MCPAppsPanel: React.FC<Props> = ({ accessToken, selectedServers, onChange
|
|||
) : filtered.length === 0 ? (
|
||||
<div style={{ textAlign: "center", color: "#9ca3af", fontSize: 13, padding: "48px 12px" }}>
|
||||
{servers.length === 0
|
||||
? "No MCP servers configured. Add servers in Tools → MCP Servers."
|
||||
? t("chat.mCPAppsPanel.noServersConfigured")
|
||||
: activeTab === "connected"
|
||||
? "No servers connected yet."
|
||||
: "No servers match your search."}
|
||||
? t("chat.mCPAppsPanel.noServersConnected")
|
||||
: t("chat.mCPAppsPanel.noServersMatch")}
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
|
|
@ -621,7 +636,7 @@ const MCPAppsPanel: React.FC<Props> = ({ accessToken, selectedServers, onChange
|
|||
{server.mcp_info?.logo_url ? (
|
||||
<img
|
||||
src={server.mcp_info.logo_url}
|
||||
alt={`${name} logo`}
|
||||
alt={t("chat.mCPAppsPanel.logoAlt", { name })}
|
||||
style={{
|
||||
width: 38,
|
||||
height: 38,
|
||||
|
|
@ -678,7 +693,7 @@ const MCPAppsPanel: React.FC<Props> = ({ accessToken, selectedServers, onChange
|
|||
}}
|
||||
>
|
||||
<span style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
|
||||
{server.description ?? "MCP server"}
|
||||
{server.description ?? t("chat.mCPAppsPanel.mcpServerFallbackDesc")}
|
||||
</span>
|
||||
{count !== undefined ? (
|
||||
count > 0 ? (
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import { Switch, Spin } from "antd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import MessageManager from "@/components/molecules/message_manager";
|
||||
import { fetchMCPServers, listMCPTools } from "../networking";
|
||||
import { MCPServer } from "../mcp_tools/types";
|
||||
|
|
@ -11,6 +12,7 @@ interface Props {
|
|||
}
|
||||
|
||||
const MCPConnectPicker: React.FC<Props> = ({ accessToken, selectedServers, onChange }) => {
|
||||
const { t } = useTranslation();
|
||||
const [servers, setServers] = useState<MCPServer[]>([]);
|
||||
const [loadingServers, setLoadingServers] = useState(true);
|
||||
// Track which individual servers are being toggled on (verifying tools)
|
||||
|
|
@ -58,13 +60,13 @@ const MCPConnectPicker: React.FC<Props> = ({ accessToken, selectedServers, onCha
|
|||
const result = await listMCPTools(accessToken, serverName);
|
||||
// listMCPTools never throws; it returns { tools, error, message } on failure
|
||||
if (result?.error) {
|
||||
MessageManager.warning(`Could not load tools for ${serverName} — it will be excluded from this message.`);
|
||||
MessageManager.warning(t("chat.mCPConnectPicker.couldNotLoadTools", { name: serverName }));
|
||||
// Do not add to selectedServers
|
||||
return;
|
||||
}
|
||||
onChange([...selectedServers, serverName]);
|
||||
} catch {
|
||||
MessageManager.warning(`Could not load tools for ${serverName} — it will be excluded from this message.`);
|
||||
MessageManager.warning(t("chat.mCPConnectPicker.couldNotLoadTools", { name: serverName }));
|
||||
// Do not add to selectedServers
|
||||
} finally {
|
||||
setTogglingOn((prev) => {
|
||||
|
|
@ -90,7 +92,7 @@ const MCPConnectPicker: React.FC<Props> = ({ accessToken, selectedServers, onCha
|
|||
</div>
|
||||
) : servers.length === 0 ? (
|
||||
<div style={{ padding: "16px 12px", color: "#8c8c8c", fontSize: 13, textAlign: "center" }}>
|
||||
No MCP servers configured
|
||||
{t("chat.mCPConnectPicker.noServersConfigured")}
|
||||
</div>
|
||||
) : (
|
||||
servers.map((server) => {
|
||||
|
|
@ -112,7 +114,7 @@ const MCPConnectPicker: React.FC<Props> = ({ accessToken, selectedServers, onCha
|
|||
{server.mcp_info?.logo_url && (
|
||||
<img
|
||||
src={server.mcp_info.logo_url}
|
||||
alt={`${name} logo`}
|
||||
alt={t("chat.mCPConnectPicker.logoAlt", { name })}
|
||||
style={{
|
||||
width: 24,
|
||||
height: 24,
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@
|
|||
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { Spin } from "antd";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import type { TFunction } from "i18next";
|
||||
import MessageManager from "@/components/molecules/message_manager";
|
||||
import { DeleteOutlined, LinkOutlined } from "@ant-design/icons";
|
||||
import { Badge, Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow } from "@tremor/react";
|
||||
|
|
@ -18,42 +20,43 @@ interface Props {
|
|||
accessToken: string;
|
||||
}
|
||||
|
||||
function relativeTime(isoString: string | null | undefined): string {
|
||||
function relativeTime(isoString: string | null | undefined, t: TFunction): string {
|
||||
if (!isoString) return "";
|
||||
try {
|
||||
const date = new Date(isoString);
|
||||
const diffMs = Date.now() - date.getTime();
|
||||
const diffSec = Math.floor(diffMs / 1000);
|
||||
if (diffSec < 60) return "just now";
|
||||
if (diffSec < 60) return t("chat.mCPCredentialsTab.justNow");
|
||||
const diffMin = Math.floor(diffSec / 60);
|
||||
if (diffMin < 60) return `${diffMin}m ago`;
|
||||
if (diffMin < 60) return t("chat.mCPCredentialsTab.minutesAgo", { count: diffMin });
|
||||
const diffHr = Math.floor(diffMin / 60);
|
||||
if (diffHr < 24) return `${diffHr}h ago`;
|
||||
return `${Math.floor(diffHr / 24)}d ago`;
|
||||
if (diffHr < 24) return t("chat.mCPCredentialsTab.hoursAgo", { count: diffHr });
|
||||
return t("chat.mCPCredentialsTab.daysAgo", { count: Math.floor(diffHr / 24) });
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function expiryLabel(isoString: string | null | undefined): string {
|
||||
if (!isoString) return "Does not expire";
|
||||
function expiryLabel(isoString: string | null | undefined, t: TFunction): string {
|
||||
if (!isoString) return t("chat.mCPCredentialsTab.doesNotExpire");
|
||||
try {
|
||||
const exp = new Date(isoString);
|
||||
const diffMs = exp.getTime() - Date.now();
|
||||
if (diffMs <= 0) return "Expired";
|
||||
if (diffMs <= 0) return t("chat.mCPCredentialsTab.expired");
|
||||
const diffSec = Math.floor(diffMs / 1000);
|
||||
const diffMin = Math.floor(diffSec / 60);
|
||||
const diffHr = Math.floor(diffMin / 60);
|
||||
const diffDay = Math.floor(diffHr / 24);
|
||||
if (diffDay > 0) return `Expires in ${diffDay}d`;
|
||||
if (diffHr > 0) return `Expires in ${diffHr}h`;
|
||||
return `Expires in ${diffMin}m`;
|
||||
if (diffDay > 0) return t("chat.mCPCredentialsTab.expiresInDays", { count: diffDay });
|
||||
if (diffHr > 0) return t("chat.mCPCredentialsTab.expiresInHours", { count: diffHr });
|
||||
return t("chat.mCPCredentialsTab.expiresInMinutes", { count: diffMin });
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
const MCPCredentialsTab: React.FC<Props> = ({ accessToken }) => {
|
||||
const { t } = useTranslation();
|
||||
const [credentials, setCredentials] = useState<MCPUserCredentialListItem[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [revoking, setRevoking] = useState<Set<string>>(new Set());
|
||||
|
|
@ -76,7 +79,7 @@ const MCPCredentialsTab: React.FC<Props> = ({ accessToken }) => {
|
|||
await deleteMCPOAuthUserCredential(accessToken, serverId);
|
||||
setCredentials((prev) => prev.filter((c) => c.server_id !== serverId));
|
||||
} catch {
|
||||
MessageManager.error("Failed to revoke connection. Please try again.");
|
||||
MessageManager.error(t("chat.mCPCredentialsTab.revokeError"));
|
||||
} finally {
|
||||
setRevoking((prev) => {
|
||||
const n = new Set(prev);
|
||||
|
|
@ -92,8 +95,8 @@ const MCPCredentialsTab: React.FC<Props> = ({ accessToken }) => {
|
|||
<div className="w-full">
|
||||
{/* Header */}
|
||||
<div className="mb-4">
|
||||
<h2 className="text-base font-semibold text-gray-900 mb-0.5">App Credentials</h2>
|
||||
<p className="text-sm text-gray-500 m-0">Your stored OAuth connections — used automatically in chat.</p>
|
||||
<h2 className="text-base font-semibold text-gray-900 mb-0.5">{t("chat.mCPCredentialsTab.appCredentials")}</h2>
|
||||
<p className="text-sm text-gray-500 m-0">{t("chat.mCPCredentialsTab.oauthConnectionsDesc")}</p>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
|
|
@ -103,20 +106,26 @@ const MCPCredentialsTab: React.FC<Props> = ({ accessToken }) => {
|
|||
) : credentials.length === 0 ? (
|
||||
<div className="text-center text-gray-400 text-sm py-12 border border-dashed border-gray-200 rounded-lg">
|
||||
<LinkOutlined className="text-2xl mb-3 block text-gray-300" />
|
||||
No connections yet.
|
||||
{t("chat.mCPCredentialsTab.noConnectionsYet")}
|
||||
<br />
|
||||
Go to <strong>Apps</strong> and click <strong>Connect</strong> to authorize an MCP server.
|
||||
<Trans i18nKey="chat.mCPCredentialsTab.goToAppsConnect" components={{ strong: <strong /> }} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-lg border border-gray-200 overflow-hidden">
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableHeaderCell className="text-xs font-medium text-gray-500 py-2 px-4">App</TableHeaderCell>
|
||||
<TableHeaderCell className="text-xs font-medium text-gray-500 py-2 px-4">Connected</TableHeaderCell>
|
||||
<TableHeaderCell className="text-xs font-medium text-gray-500 py-2 px-4">Status</TableHeaderCell>
|
||||
<TableHeaderCell className="text-xs font-medium text-gray-500 py-2 px-4">
|
||||
{t("chat.mCPCredentialsTab.colApp")}
|
||||
</TableHeaderCell>
|
||||
<TableHeaderCell className="text-xs font-medium text-gray-500 py-2 px-4">
|
||||
{t("chat.mCPCredentialsTab.colConnected")}
|
||||
</TableHeaderCell>
|
||||
<TableHeaderCell className="text-xs font-medium text-gray-500 py-2 px-4">
|
||||
{t("common.status")}
|
||||
</TableHeaderCell>
|
||||
<TableHeaderCell className="text-xs font-medium text-gray-500 py-2 px-4 text-right">
|
||||
Actions
|
||||
{t("common.actions")}
|
||||
</TableHeaderCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
|
|
@ -124,9 +133,9 @@ const MCPCredentialsTab: React.FC<Props> = ({ accessToken }) => {
|
|||
{credentials.map((cred) => {
|
||||
const name = displayName(cred);
|
||||
const isRevoking = revoking.has(cred.server_id);
|
||||
const exp = expiryLabel(cred.expires_at);
|
||||
const connected = relativeTime(cred.connected_at);
|
||||
const isExpired = exp === "Expired";
|
||||
const exp = expiryLabel(cred.expires_at, t);
|
||||
const connected = relativeTime(cred.connected_at, t);
|
||||
const isExpired = exp === t("chat.mCPCredentialsTab.expired");
|
||||
|
||||
return (
|
||||
<TableRow key={cred.server_id} className="h-10 hover:bg-gray-50">
|
||||
|
|
@ -145,7 +154,7 @@ const MCPCredentialsTab: React.FC<Props> = ({ accessToken }) => {
|
|||
<button
|
||||
onClick={() => handleRevoke(cred.server_id)}
|
||||
disabled={isRevoking}
|
||||
title="Revoke connection"
|
||||
title={t("chat.mCPCredentialsTab.revokeConnection")}
|
||||
className={`inline-flex items-center justify-center rounded-md border border-gray-200 px-2 py-1 text-gray-400 hover:text-red-500 hover:border-red-200 transition-colors ${isRevoking ? "opacity-50 cursor-not-allowed" : "cursor-pointer"}`}
|
||||
style={{ background: "none" }}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -6930,6 +6930,72 @@
|
|||
"responseN": "Response {{n}}",
|
||||
"generating": "Generating…",
|
||||
"scrollToBottom": "Scroll to bottom"
|
||||
},
|
||||
"conversationList": {
|
||||
"rename": "Rename",
|
||||
"deleteConversationTitle": "Delete this conversation?",
|
||||
"searchPlaceholder": "Search conversations…",
|
||||
"noConversationsFound": "No conversations found",
|
||||
"chatsLocalStorageTooltip": "Chats are saved locally in this browser. All requests are logged in Spend → Logs.",
|
||||
"newChat": "New Chat",
|
||||
"noConversationsYet": "No conversations yet.\nStart a new chat above.",
|
||||
"myAccount": "My Account",
|
||||
"dateGroupToday": "Today",
|
||||
"dateGroupYesterday": "Yesterday",
|
||||
"dateGroupLast7Days": "Last 7 Days",
|
||||
"dateGroupOlder": "Older"
|
||||
},
|
||||
"mCPAppsPanel": {
|
||||
"connecting": "Connecting…",
|
||||
"connect": "Connect",
|
||||
"disconnect": "Disconnect",
|
||||
"mcpServerFallbackDesc": "MCP server",
|
||||
"information": "Information",
|
||||
"serverIdLabel": "Server ID",
|
||||
"transportLabel": "Transport",
|
||||
"connected": "Connected",
|
||||
"notConnected": "Not connected",
|
||||
"availableTools": "Available Tools",
|
||||
"noToolsAvailable": "No tools available",
|
||||
"mcpServersTitle": "MCP Servers",
|
||||
"beta": "Beta",
|
||||
"browsePitch": "Browse tools, authenticate once, use in chat — no setup needed.",
|
||||
"loadingTools": "Loading tools...",
|
||||
"toolsAvailable_one": "{{count}} tool available",
|
||||
"toolsAvailable_other": "{{count}} tools available",
|
||||
"searchServersPlaceholder": "Search servers...",
|
||||
"tabAll": "All",
|
||||
"tabConnected": "Connected",
|
||||
"tabConnectedCount": "Connected ({{count}})",
|
||||
"noServersConfigured": "No MCP servers configured. Add servers in Tools → MCP Servers.",
|
||||
"noServersConnected": "No servers connected yet.",
|
||||
"noServersMatch": "No servers match your search.",
|
||||
"couldNotLoadTools": "Could not load tools for {{name}}",
|
||||
"logoAlt": "{{name}} logo"
|
||||
},
|
||||
"mCPConnectPicker": {
|
||||
"noServersConfigured": "No MCP servers configured",
|
||||
"couldNotLoadTools": "Could not load tools for {{name}} — it will be excluded from this message.",
|
||||
"logoAlt": "{{name}} logo"
|
||||
},
|
||||
"mCPCredentialsTab": {
|
||||
"justNow": "just now",
|
||||
"minutesAgo": "{{count}}m ago",
|
||||
"hoursAgo": "{{count}}h ago",
|
||||
"daysAgo": "{{count}}d ago",
|
||||
"doesNotExpire": "Does not expire",
|
||||
"expired": "Expired",
|
||||
"expiresInDays": "Expires in {{count}}d",
|
||||
"expiresInHours": "Expires in {{count}}h",
|
||||
"expiresInMinutes": "Expires in {{count}}m",
|
||||
"appCredentials": "App Credentials",
|
||||
"oauthConnectionsDesc": "Your stored OAuth connections — used automatically in chat.",
|
||||
"noConnectionsYet": "No connections yet.",
|
||||
"goToAppsConnect": "Go to <strong>Apps</strong> and click <strong>Connect</strong> to authorize an MCP server.",
|
||||
"colApp": "App",
|
||||
"colConnected": "Connected",
|
||||
"revokeConnection": "Revoke connection",
|
||||
"revokeError": "Failed to revoke connection. Please try again."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6930,6 +6930,72 @@
|
|||
"responseN": "响应 {{n}}",
|
||||
"generating": "生成中…",
|
||||
"scrollToBottom": "滚动到底部"
|
||||
},
|
||||
"conversationList": {
|
||||
"rename": "重命名",
|
||||
"deleteConversationTitle": "删除此对话?",
|
||||
"searchPlaceholder": "搜索对话…",
|
||||
"noConversationsFound": "未找到对话",
|
||||
"chatsLocalStorageTooltip": "对话已保存在此浏览器本地。所有请求均记录在花费 → 日志中。",
|
||||
"newChat": "新建对话",
|
||||
"noConversationsYet": "暂无对话。\n点击上方新建对话。",
|
||||
"myAccount": "我的账号",
|
||||
"dateGroupToday": "今天",
|
||||
"dateGroupYesterday": "昨天",
|
||||
"dateGroupLast7Days": "最近 7 天",
|
||||
"dateGroupOlder": "更早"
|
||||
},
|
||||
"mCPAppsPanel": {
|
||||
"connecting": "连接中…",
|
||||
"connect": "连接",
|
||||
"disconnect": "断开连接",
|
||||
"mcpServerFallbackDesc": "MCP 服务器",
|
||||
"information": "信息",
|
||||
"serverIdLabel": "服务器 ID",
|
||||
"transportLabel": "传输方式",
|
||||
"connected": "已连接",
|
||||
"notConnected": "未连接",
|
||||
"availableTools": "可用工具",
|
||||
"noToolsAvailable": "无可用工具",
|
||||
"mcpServersTitle": "MCP 服务器",
|
||||
"beta": "Beta",
|
||||
"browsePitch": "浏览工具,一次认证,在对话中使用 — 无需额外配置。",
|
||||
"loadingTools": "加载工具中...",
|
||||
"toolsAvailable_one": "{{count}} 个工具可用",
|
||||
"toolsAvailable_other": "{{count}} 个工具可用",
|
||||
"searchServersPlaceholder": "搜索服务器...",
|
||||
"tabAll": "全部",
|
||||
"tabConnected": "已连接",
|
||||
"tabConnectedCount": "已连接 ({{count}})",
|
||||
"noServersConfigured": "未配置 MCP 服务器。请在工具 → MCP 服务器中添加。",
|
||||
"noServersConnected": "暂无已连接的服务器。",
|
||||
"noServersMatch": "没有服务器匹配您的搜索。",
|
||||
"couldNotLoadTools": "无法加载 {{name}} 的工具",
|
||||
"logoAlt": "{{name}} 图标"
|
||||
},
|
||||
"mCPConnectPicker": {
|
||||
"noServersConfigured": "未配置 MCP 服务器",
|
||||
"couldNotLoadTools": "无法加载 {{name}} 的工具 — 该服务器将不参与本次请求。",
|
||||
"logoAlt": "{{name}} 图标"
|
||||
},
|
||||
"mCPCredentialsTab": {
|
||||
"justNow": "刚刚",
|
||||
"minutesAgo": "{{count}} 分钟前",
|
||||
"hoursAgo": "{{count}} 小时前",
|
||||
"daysAgo": "{{count}} 天前",
|
||||
"doesNotExpire": "永不过期",
|
||||
"expired": "已过期",
|
||||
"expiresInDays": "{{count}} 天后过期",
|
||||
"expiresInHours": "{{count}} 小时后过期",
|
||||
"expiresInMinutes": "{{count}} 分钟后过期",
|
||||
"appCredentials": "应用凭证",
|
||||
"oauthConnectionsDesc": "您存储的 OAuth 连接 — 在对话中自动使用。",
|
||||
"noConnectionsYet": "暂无连接。",
|
||||
"goToAppsConnect": "前往 <strong>应用</strong>,点击 <strong>连接</strong> 以授权 MCP 服务器。",
|
||||
"colApp": "应用",
|
||||
"colConnected": "连接时间",
|
||||
"revokeConnection": "撤销连接",
|
||||
"revokeError": "撤销连接失败,请重试。"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue