feat(ui): i18n for AI hub tables

This commit is contained in:
吹雪 2026-06-11 03:18:23 +09:00
parent fd28940f49
commit 71624ed3b7
6 changed files with 380 additions and 121 deletions

View file

@ -2,8 +2,35 @@ import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { vi } from "vitest";
import { flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table";
import { TFunction } from "i18next";
import { getAgentHubTableColumns, AgentHubData } from "./AgentHubTableColumns";
const mockT = ((key: string, opts?: Record<string, unknown>) => {
if (key === "aiHub.agentHubTableColumns.skillCount") {
const count = opts?.count as number;
return count === 1 ? `${count} skill` : `${count} skills`;
}
const last = key.split(".").pop() ?? key;
const map: Record<string, string> = {
colAgentName: "Agent Name",
copyAgentName: "Copy agent name",
colVersion: "Version",
colProtocol: "Protocol",
colSkills: "Skills",
colCapabilities: "Capabilities",
colIOModes: "I/O Modes",
ioIn: "In:",
ioOut: "Out:",
colPublic: "Public",
detailsShort: "Info",
description: "Description",
details: "Details",
yes: "Yes",
no: "No",
};
return map[last] ?? key;
}) as unknown as TFunction;
const mockAgent: AgentHubData = {
agent_id: "agent-1",
protocolVersion: "1.0",
@ -33,7 +60,7 @@ function TestTable({
showModal?: ReturnType<typeof vi.fn>;
copyToClipboard?: ReturnType<typeof vi.fn>;
}) {
const columns = getAgentHubTableColumns(showModal, copyToClipboard, publicPage);
const columns = getAgentHubTableColumns(mockT, showModal, copyToClipboard, publicPage);
const table = useReactTable({ data, columns, getCoreRowModel: getCoreRowModel() });
return (

View file

@ -2,6 +2,7 @@ import { ColumnDef } from "@tanstack/react-table";
import { Button, Badge, Text } from "@tremor/react";
import { Tooltip, Tag } from "antd";
import { CopyOutlined, InfoCircleOutlined } from "@ant-design/icons";
import { TFunction } from "i18next";
export interface AgentHubData {
agent_id?: string;
@ -29,13 +30,14 @@ export interface AgentHubData {
}
export const getAgentHubTableColumns = (
t: TFunction,
showModal: (agent: AgentHubData) => void,
copyToClipboard: (text: string) => void,
publicPage: boolean = false,
): ColumnDef<AgentHubData>[] => {
const allColumns: ColumnDef<AgentHubData>[] = [
{
header: "Agent Name",
header: t("aiHub.agentHubTableColumns.colAgentName"),
accessorKey: "name",
enableSorting: true,
sortingFn: "alphanumeric",
@ -46,7 +48,7 @@ export const getAgentHubTableColumns = (
<div className="space-y-1">
<div className="flex items-center space-x-2">
<Text className="font-medium text-sm">{agent.name}</Text>
<Tooltip title="Copy agent name">
<Tooltip title={t("aiHub.agentHubTableColumns.copyAgentName")}>
<CopyOutlined
onClick={() => copyToClipboard(agent.name)}
className="cursor-pointer text-gray-500 hover:text-blue-500 text-xs"
@ -62,7 +64,7 @@ export const getAgentHubTableColumns = (
},
},
{
header: "Description",
header: t("common.description"),
accessorKey: "description",
enableSorting: true,
sortingFn: "alphanumeric",
@ -76,7 +78,7 @@ export const getAgentHubTableColumns = (
},
},
{
header: "Version",
header: t("aiHub.agentHubTableColumns.colVersion"),
accessorKey: "version",
enableSorting: true,
sortingFn: "alphanumeric",
@ -94,7 +96,7 @@ export const getAgentHubTableColumns = (
},
},
{
header: "Protocol",
header: t("aiHub.agentHubTableColumns.colProtocol"),
accessorKey: "protocolVersion",
enableSorting: true,
sortingFn: "alphanumeric",
@ -108,7 +110,7 @@ export const getAgentHubTableColumns = (
},
},
{
header: "Skills",
header: t("aiHub.agentHubTableColumns.colSkills"),
accessorKey: "skills",
enableSorting: false,
cell: ({ row }) => {
@ -118,7 +120,7 @@ export const getAgentHubTableColumns = (
return (
<div className="space-y-1">
<Text className="text-xs font-medium">
{skills.length} skill{skills.length !== 1 ? "s" : ""}
{t("aiHub.agentHubTableColumns.skillCount", { count: skills.length })}
</Text>
{skills.length > 0 && (
<div className="flex flex-wrap gap-1">
@ -135,7 +137,7 @@ export const getAgentHubTableColumns = (
},
},
{
header: "Capabilities",
header: t("aiHub.agentHubTableColumns.colCapabilities"),
accessorKey: "capabilities",
enableSorting: false,
cell: ({ row }) => {
@ -161,7 +163,7 @@ export const getAgentHubTableColumns = (
},
},
{
header: "I/O Modes",
header: t("aiHub.agentHubTableColumns.colIOModes"),
accessorKey: "defaultInputModes",
enableSorting: false,
cell: ({ row }) => {
@ -172,10 +174,11 @@ export const getAgentHubTableColumns = (
return (
<div className="space-y-1">
<Text className="text-xs">
<span className="font-medium">In:</span> {inputModes.join(", ") || "-"}
<span className="font-medium">{t("aiHub.agentHubTableColumns.ioIn")}</span> {inputModes.join(", ") || "-"}
</Text>
<Text className="text-xs">
<span className="font-medium">Out:</span> {outputModes.join(", ") || "-"}
<span className="font-medium">{t("aiHub.agentHubTableColumns.ioOut")}</span>{" "}
{outputModes.join(", ") || "-"}
</Text>
</div>
);
@ -185,7 +188,7 @@ export const getAgentHubTableColumns = (
},
},
{
header: "Public",
header: t("aiHub.agentHubTableColumns.colPublic"),
accessorKey: "is_public",
enableSorting: true,
sortingFn: (rowA, rowB) => {
@ -198,11 +201,11 @@ export const getAgentHubTableColumns = (
return agent.is_public === true ? (
<Badge color="green" size="xs">
Yes
{t("common.yes")}
</Badge>
) : (
<Badge color="gray" size="xs">
No
{t("common.no")}
</Badge>
);
},
@ -211,7 +214,7 @@ export const getAgentHubTableColumns = (
},
},
{
header: "Details",
header: t("common.details"),
id: "details",
enableSorting: false,
cell: ({ row }) => {
@ -219,8 +222,8 @@ export const getAgentHubTableColumns = (
return (
<Button size="xs" variant="secondary" onClick={() => showModal(agent)} icon={InfoCircleOutlined}>
<span className="hidden lg:inline">Details</span>
<span className="lg:hidden">Info</span>
<span className="hidden lg:inline">{t("common.details")}</span>
<span className="lg:hidden">{t("aiHub.agentHubTableColumns.detailsShort")}</span>
</Button>
);
},

View file

@ -31,6 +31,7 @@ import { useRouter } from "next/navigation";
import React, { useCallback, useEffect, useState } from "react";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { useUISettings } from "@/app/(dashboard)/hooks/uiSettings/useUISettings";
import { useTranslation } from "react-i18next";
import { checkTokenValidity } from "@/utils/jwtUtils";
import { getCookie } from "@/utils/cookieUtils";
@ -64,6 +65,7 @@ const ModelHubTable: React.FC<ModelHubTableProps> = ({ accessToken, publicPage,
// Admin Viewer follows the read-parity rule: see the AI Hub catalog, but
// cannot toggle public visibility (write).
const canModify = isProxyAdminRole(userRole || "");
const { t } = useTranslation();
const [publicPageAllowed, setPublicPageAllowed] = useState<boolean>(false);
const [modelHubData, setModelHubData] = useState<ModelGroupInfo[] | null>(null);
@ -306,7 +308,7 @@ const ModelHubTable: React.FC<ModelHubTableProps> = ({ accessToken, publicPage,
const copyToClipboard = (text: string) => {
navigator.clipboard.writeText(text);
NotificationsManager.success("Copied to clipboard!");
NotificationsManager.success(t("aiHub.modelHubTable.copiedToClipboard"));
};
const formatCapabilityName = (key: string) => {
@ -399,23 +401,21 @@ const ModelHubTable: React.FC<ModelHubTableProps> = ({ accessToken, publicPage,
{/* Header with Title, Description and URL */}
<div className="flex justify-between items-center mb-6">
<div className="flex flex-col items-start">
<Title className="text-center">AI Hub</Title>
<Title className="text-center">{t("aiHub.modelHubTable.title")}</Title>
{isAdminRole(userRole || "") ? (
<p className="text-sm text-gray-600">
Make models, agents, and MCP servers public for developers to know what&apos;s available.
</p>
<p className="text-sm text-gray-600">{t("aiHub.modelHubTable.adminDescription")}</p>
) : (
<p className="text-sm text-gray-600">A list of all public model names personally available to you.</p>
<p className="text-sm text-gray-600">{t("aiHub.modelHubTable.userDescription")}</p>
)}
</div>
<div className="flex items-center space-x-4">
<Text>Model Hub URL:</Text>
<Text>{t("aiHub.modelHubTable.modelHubUrl")}</Text>
<div className="flex items-center bg-gray-200 px-2 py-1 rounded">
<Text className="mr-2">{`${getProxyBaseUrl()}/ui/model_hub_table`}</Text>
<button
onClick={() => copyToClipboard(`${getProxyBaseUrl()}/ui/model_hub_table`)}
className="p-1 hover:bg-gray-300 rounded transition-colors"
title="Copy URL"
title={t("aiHub.modelHubTable.copyUrl")}
>
<Copy size={16} className="text-gray-600" />
</button>
@ -433,10 +433,10 @@ const ModelHubTable: React.FC<ModelHubTableProps> = ({ accessToken, publicPage,
{/* Tab System for Model Hub, Agent Hub, MCP Hub, and Plugin Marketplace */}
<TabGroup>
<TabList className="mb-4">
<Tab>Model Hub</Tab>
<Tab>Agent Hub</Tab>
<Tab>MCP Hub</Tab>
<Tab>Skill Hub</Tab>
<Tab>{t("aiHub.modelHubTable.tabModelHub")}</Tab>
<Tab>{t("aiHub.modelHubTable.tabAgentHub")}</Tab>
<Tab>{t("aiHub.modelHubTable.tabMcpHub")}</Tab>
<Tab>{t("aiHub.modelHubTable.tabSkillHub")}</Tab>
</TabList>
<TabPanels>
@ -447,7 +447,9 @@ const ModelHubTable: React.FC<ModelHubTableProps> = ({ accessToken, publicPage,
{/* Header with Make Public Button */}
{publicPage == false && canModify && (
<div className="flex justify-end mb-4">
<Button onClick={() => handleMakePublicPage()}>Select Models to Make Public</Button>
<Button onClick={() => handleMakePublicPage()}>
{t("aiHub.modelHubTable.selectModelsPublic")}
</Button>
</div>
)}
@ -465,7 +467,10 @@ const ModelHubTable: React.FC<ModelHubTableProps> = ({ accessToken, publicPage,
<div className="mt-4 text-center space-y-2">
<Text className="text-sm text-gray-600">
Showing {filteredData.length} of {modelHubData?.length || 0} models
{t("aiHub.modelHubTable.showingModels", {
filtered: filteredData.length,
total: modelHubData?.length || 0,
})}
</Text>
</div>
</TabPanel>
@ -476,13 +481,15 @@ const ModelHubTable: React.FC<ModelHubTableProps> = ({ accessToken, publicPage,
{/* Header with Make Public Button */}
{publicPage == false && canModify && (
<div className="flex justify-end mb-4">
<Button onClick={() => handleMakeAgentPublicPage()}>Select Agents to Make Public</Button>
<Button onClick={() => handleMakeAgentPublicPage()}>
{t("aiHub.modelHubTable.selectAgentsPublic")}
</Button>
</div>
)}
{/* Agent Table */}
<ModelDataTable
columns={getAgentHubTableColumns(showAgentModal, copyToClipboard, publicPage)}
columns={getAgentHubTableColumns(t, showAgentModal, copyToClipboard, publicPage)}
data={agentHubData || []}
isLoading={agentLoading}
defaultSorting={[{ id: "name", desc: false }]}
@ -491,7 +498,7 @@ const ModelHubTable: React.FC<ModelHubTableProps> = ({ accessToken, publicPage,
<div className="mt-4 text-center space-y-2">
<Text className="text-sm text-gray-600">
Showing {agentHubData?.length || 0} agent{agentHubData?.length !== 1 ? "s" : ""}
{t("aiHub.modelHubTable.showingAgents", { count: agentHubData?.length || 0 })}
</Text>
</div>
</TabPanel>
@ -502,7 +509,9 @@ const ModelHubTable: React.FC<ModelHubTableProps> = ({ accessToken, publicPage,
{/* Header with Make Public Button */}
{publicPage == false && canModify && (
<div className="flex justify-end mb-4">
<Button onClick={() => handleMakeMcpPublicPage()}>Select MCP Servers to Make Public</Button>
<Button onClick={() => handleMakeMcpPublicPage()}>
{t("aiHub.modelHubTable.selectMcpPublic")}
</Button>
</div>
)}
@ -517,7 +526,7 @@ const ModelHubTable: React.FC<ModelHubTableProps> = ({ accessToken, publicPage,
<div className="mt-4 text-center space-y-2">
<Text className="text-sm text-gray-600">
Showing {mcpHubData?.length || 0} MCP server{mcpHubData?.length !== 1 ? "s" : ""}
{t("aiHub.modelHubTable.showingMcpServers", { count: mcpHubData?.length || 0 })}
</Text>
</div>
</TabPanel>
@ -526,7 +535,9 @@ const ModelHubTable: React.FC<ModelHubTableProps> = ({ accessToken, publicPage,
<TabPanel>
{publicPage == false && canModify && (
<div className="flex justify-end mb-4">
<Button onClick={() => setIsMakeSkillPublicModalVisible(true)}>Select Skills to Make Public</Button>
<Button onClick={() => setIsMakeSkillPublicModalVisible(true)}>
{t("aiHub.modelHubTable.selectSkillsPublic")}
</Button>
</div>
)}
<SkillHubDashboard
@ -546,14 +557,14 @@ const ModelHubTable: React.FC<ModelHubTableProps> = ({ accessToken, publicPage,
</div>
) : (
<Card className="mx-auto max-w-xl mt-10">
<Text className="text-xl text-center mb-2 text-black">Public Model Hub not enabled.</Text>
<p className="text-base text-center text-slate-800">Ask your proxy admin to enable this on their Admin UI.</p>
<Text className="text-xl text-center mb-2 text-black">{t("aiHub.modelHubTable.publicHubNotEnabled")}</Text>
<p className="text-base text-center text-slate-800">{t("aiHub.modelHubTable.askAdminToEnable")}</p>
</Card>
)}
{/* Public Page Modal */}
<Modal
title="Public Model Hub"
title={t("aiHub.modelHubTable.publicModelHubTitle")}
width={600}
open={isPublicPageModalVisible}
footer={null}
@ -562,20 +573,20 @@ const ModelHubTable: React.FC<ModelHubTableProps> = ({ accessToken, publicPage,
>
<div className="pt-5 pb-5">
<div className="flex justify-between mb-4">
<Text className="text-base mr-2">Shareable Link:</Text>
<Text className="text-base mr-2">{t("aiHub.modelHubTable.shareableLink")}</Text>
<Text className="max-w-sm ml-2 bg-gray-200 pr-2 pl-2 pt-1 pb-1 text-center rounded">
{`${getProxyBaseUrl()}/ui/model_hub_table`}
</Text>
</div>
<div className="flex justify-end">
<Button onClick={goToPublicModelPage}>See Page</Button>
<Button onClick={goToPublicModelPage}>{t("aiHub.modelHubTable.seePage")}</Button>
</div>
</div>
</Modal>
{/* Model Details Modal */}
<Modal
title={selectedModel?.model_group || "Model Details"}
title={selectedModel?.model_group || t("aiHub.modelHubTable.modelDetails")}
width={1000}
open={isModalVisible}
footer={null}
@ -586,18 +597,18 @@ const ModelHubTable: React.FC<ModelHubTableProps> = ({ accessToken, publicPage,
<div className="space-y-6">
{/* Model Overview */}
<div>
<Text className="text-lg font-semibold mb-4">Model Overview</Text>
<Text className="text-lg font-semibold mb-4">{t("aiHub.modelHubTable.modelOverview")}</Text>
<div className="grid grid-cols-2 gap-4 mb-4">
<div>
<Text className="font-medium">Model Group:</Text>
<Text className="font-medium">{t("aiHub.modelHubTable.fieldModelGroup")}</Text>
<Text>{selectedModel.model_group}</Text>
</div>
<div>
<Text className="font-medium">Mode:</Text>
<Text>{selectedModel.mode || "Not specified"}</Text>
<Text className="font-medium">{t("aiHub.modelHubTable.fieldMode")}</Text>
<Text>{selectedModel.mode || t("aiHub.modelHubTable.notSpecified")}</Text>
</div>
<div>
<Text className="font-medium">Providers:</Text>
<Text className="font-medium">{t("aiHub.modelHubTable.fieldProviders")}</Text>
<div className="flex flex-wrap gap-1 mt-1">
{selectedModel.providers.map((provider) => (
<Badge key={provider} color="blue">
@ -611,30 +622,34 @@ const ModelHubTable: React.FC<ModelHubTableProps> = ({ accessToken, publicPage,
{/* Token and Cost Information */}
<div>
<Text className="text-lg font-semibold mb-4">Token & Cost Information</Text>
<Text className="text-lg font-semibold mb-4">{t("aiHub.modelHubTable.tokenCostInfo")}</Text>
<div className="grid grid-cols-2 gap-4">
<div>
<Text className="font-medium">Max Input Tokens:</Text>
<Text>{selectedModel.max_input_tokens?.toLocaleString() || "Not specified"}</Text>
</div>
<div>
<Text className="font-medium">Max Output Tokens:</Text>
<Text>{selectedModel.max_output_tokens?.toLocaleString() || "Not specified"}</Text>
</div>
<div>
<Text className="font-medium">Input Cost per 1M Tokens:</Text>
<Text className="font-medium">{t("aiHub.modelHubTable.fieldMaxInputTokens")}</Text>
<Text>
{selectedModel.input_cost_per_token
? formatCost(selectedModel.input_cost_per_token)
: "Not specified"}
{selectedModel.max_input_tokens?.toLocaleString() || t("aiHub.modelHubTable.notSpecified")}
</Text>
</div>
<div>
<Text className="font-medium">Output Cost per 1M Tokens:</Text>
<Text className="font-medium">{t("aiHub.modelHubTable.fieldMaxOutputTokens")}</Text>
<Text>
{selectedModel.max_output_tokens?.toLocaleString() || t("aiHub.modelHubTable.notSpecified")}
</Text>
</div>
<div>
<Text className="font-medium">{t("aiHub.modelHubTable.fieldInputCost")}</Text>
<Text>
{selectedModel.input_cost_per_token
? formatCost(selectedModel.input_cost_per_token)
: t("aiHub.modelHubTable.notSpecified")}
</Text>
</div>
<div>
<Text className="font-medium">{t("aiHub.modelHubTable.fieldOutputCost")}</Text>
<Text>
{selectedModel.output_cost_per_token
? formatCost(selectedModel.output_cost_per_token)
: "Not specified"}
: t("aiHub.modelHubTable.notSpecified")}
</Text>
</div>
</div>
@ -642,14 +657,14 @@ const ModelHubTable: React.FC<ModelHubTableProps> = ({ accessToken, publicPage,
{/* Capabilities */}
<div>
<Text className="text-lg font-semibold mb-4">Capabilities</Text>
<Text className="text-lg font-semibold mb-4">{t("aiHub.modelHubTable.sectionCapabilities")}</Text>
<div className="flex flex-wrap gap-2">
{(() => {
const capabilities = getModelCapabilities(selectedModel);
const colors = ["green", "blue", "purple", "orange", "red", "yellow"];
if (capabilities.length === 0) {
return <Text className="text-gray-500">No special capabilities listed</Text>;
return <Text className="text-gray-500">{t("aiHub.modelHubTable.noCapabilities")}</Text>;
}
return capabilities.map((capability, index) => (
@ -664,17 +679,17 @@ const ModelHubTable: React.FC<ModelHubTableProps> = ({ accessToken, publicPage,
{/* Rate Limits */}
{(selectedModel.tpm || selectedModel.rpm) && (
<div>
<Text className="text-lg font-semibold mb-4">Rate Limits</Text>
<Text className="text-lg font-semibold mb-4">{t("aiHub.modelHubTable.sectionRateLimits")}</Text>
<div className="grid grid-cols-2 gap-4">
{selectedModel.tpm && (
<div>
<Text className="font-medium">Tokens per Minute:</Text>
<Text className="font-medium">{t("aiHub.modelHubTable.fieldTokensPerMinute")}</Text>
<Text>{selectedModel.tpm.toLocaleString()}</Text>
</div>
)}
{selectedModel.rpm && (
<div>
<Text className="font-medium">Requests per Minute:</Text>
<Text className="font-medium">{t("aiHub.modelHubTable.fieldRequestsPerMinute")}</Text>
<Text>{selectedModel.rpm.toLocaleString()}</Text>
</div>
)}
@ -685,7 +700,7 @@ const ModelHubTable: React.FC<ModelHubTableProps> = ({ accessToken, publicPage,
{/* Supported OpenAI Parameters */}
{selectedModel.supported_openai_params && (
<div>
<Text className="text-lg font-semibold mb-4">Supported OpenAI Parameters</Text>
<Text className="text-lg font-semibold mb-4">{t("aiHub.modelHubTable.sectionSupportedParams")}</Text>
<div className="flex flex-wrap gap-2">
{selectedModel.supported_openai_params.map((param) => (
<Badge key={param} color="green">
@ -698,7 +713,7 @@ const ModelHubTable: React.FC<ModelHubTableProps> = ({ accessToken, publicPage,
{/* Usage Example */}
<div>
<Text className="text-lg font-semibold mb-4">Usage Example</Text>
<Text className="text-lg font-semibold mb-4">{t("aiHub.modelHubTable.sectionUsageExample")}</Text>
<SyntaxHighlighter language="python" className="text-sm">
{`import openai
@ -726,7 +741,7 @@ print(response.choices[0].message.content)`}
{/* Agent Details Modal */}
<Modal
title={selectedAgent?.name || "Agent Details"}
title={selectedAgent?.name || t("aiHub.modelHubTable.agentDetails")}
width={1000}
open={isAgentModalVisible}
footer={null}
@ -737,22 +752,22 @@ print(response.choices[0].message.content)`}
<div className="space-y-6">
{/* Agent Overview */}
<div>
<Text className="text-lg font-semibold mb-4">Agent Overview</Text>
<Text className="text-lg font-semibold mb-4">{t("aiHub.modelHubTable.agentOverview")}</Text>
<div className="grid grid-cols-2 gap-4 mb-4">
<div>
<Text className="font-medium">Name:</Text>
<Text className="font-medium">{t("aiHub.modelHubTable.fieldName")}</Text>
<Text>{selectedAgent.name}</Text>
</div>
<div>
<Text className="font-medium">Version:</Text>
<Text className="font-medium">{t("aiHub.modelHubTable.fieldVersion")}</Text>
<Badge color="blue">v{selectedAgent.version}</Badge>
</div>
<div>
<Text className="font-medium">Protocol Version:</Text>
<Text className="font-medium">{t("aiHub.modelHubTable.fieldProtocolVersion")}</Text>
<Text>{selectedAgent.protocolVersion}</Text>
</div>
<div>
<Text className="font-medium">URL:</Text>
<Text className="font-medium">{t("aiHub.modelHubTable.fieldUrl")}</Text>
<div className="flex items-center space-x-2">
<Text className="truncate">{selectedAgent.url}</Text>
<CopyOutlined
@ -763,7 +778,7 @@ print(response.choices[0].message.content)`}
</div>
</div>
<div>
<Text className="font-medium">Description:</Text>
<Text className="font-medium">{t("common.description")}:</Text>
<Text className="mt-1">{selectedAgent.description}</Text>
</div>
</div>
@ -771,7 +786,7 @@ print(response.choices[0].message.content)`}
{/* Capabilities */}
{selectedAgent.capabilities && Object.keys(selectedAgent.capabilities).length > 0 && (
<div>
<Text className="text-lg font-semibold mb-4">Capabilities</Text>
<Text className="text-lg font-semibold mb-4">{t("aiHub.modelHubTable.sectionCapabilities")}</Text>
<div className="flex flex-wrap gap-2">
{Object.entries(selectedAgent.capabilities)
.filter(([_, value]) => value === true)
@ -786,26 +801,26 @@ print(response.choices[0].message.content)`}
{/* Input/Output Modes */}
<div>
<Text className="text-lg font-semibold mb-4">Input/Output Modes</Text>
<Text className="text-lg font-semibold mb-4">{t("aiHub.modelHubTable.sectionInputOutputModes")}</Text>
<div className="grid grid-cols-2 gap-4">
<div>
<Text className="font-medium">Input Modes:</Text>
<Text className="font-medium">{t("aiHub.modelHubTable.fieldInputModes")}</Text>
<div className="flex flex-wrap gap-1 mt-1">
{selectedAgent.defaultInputModes?.map((mode) => (
<Badge key={mode} color="blue">
{mode}
</Badge>
)) || <Text>Not specified</Text>}
)) || <Text>{t("aiHub.modelHubTable.notSpecified")}</Text>}
</div>
</div>
<div>
<Text className="font-medium">Output Modes:</Text>
<Text className="font-medium">{t("aiHub.modelHubTable.fieldOutputModes")}</Text>
<div className="flex flex-wrap gap-1 mt-1">
{selectedAgent.defaultOutputModes?.map((mode) => (
<Badge key={mode} color="purple">
{mode}
</Badge>
)) || <Text>Not specified</Text>}
)) || <Text>{t("aiHub.modelHubTable.notSpecified")}</Text>}
</div>
</div>
</div>
@ -814,14 +829,16 @@ print(response.choices[0].message.content)`}
{/* Skills */}
{selectedAgent.skills && selectedAgent.skills.length > 0 && (
<div>
<Text className="text-lg font-semibold mb-4">Skills</Text>
<Text className="text-lg font-semibold mb-4">{t("aiHub.modelHubTable.sectionSkills")}</Text>
<div className="space-y-4">
{selectedAgent.skills.map((skill) => (
<div key={skill.id} className="border border-gray-200 rounded p-4">
<div className="flex justify-between items-start mb-2">
<div>
<Text className="font-medium text-base">{skill.name}</Text>
<Text className="text-xs text-gray-500">ID: {skill.id}</Text>
<Text className="text-xs text-gray-500">
{t("aiHub.modelHubTable.skillId", { id: skill.id })}
</Text>
</div>
{skill.tags && skill.tags.length > 0 && (
<div className="flex flex-wrap gap-1">
@ -836,7 +853,9 @@ print(response.choices[0].message.content)`}
<Text className="text-sm mb-2">{skill.description}</Text>
{skill.examples && skill.examples.length > 0 && (
<div>
<Text className="text-xs font-medium text-gray-700">Examples:</Text>
<Text className="text-xs font-medium text-gray-700">
{t("aiHub.modelHubTable.skillExamples")}
</Text>
<div className="flex flex-wrap gap-1 mt-1">
{skill.examples.map((example, idx) => (
<Badge key={idx} color="gray" size="xs">
@ -855,8 +874,8 @@ print(response.choices[0].message.content)`}
{/* Additional Properties */}
{selectedAgent.supportsAuthenticatedExtendedCard && (
<div>
<Text className="text-lg font-semibold mb-4">Additional Features</Text>
<Badge color="green">Supports Authenticated Extended Card</Badge>
<Text className="text-lg font-semibold mb-4">{t("aiHub.modelHubTable.sectionAdditionalFeatures")}</Text>
<Badge color="green">{t("aiHub.modelHubTable.supportsAuthCard")}</Badge>
</div>
)}
</div>
@ -865,7 +884,7 @@ print(response.choices[0].message.content)`}
{/* MCP Server Details Modal */}
<Modal
title={selectedMcpServer?.server_name || "MCP Server Details"}
title={selectedMcpServer?.server_name || t("aiHub.modelHubTable.mcpServerDetails")}
width={1000}
open={isMcpModalVisible}
footer={null}
@ -876,14 +895,14 @@ print(response.choices[0].message.content)`}
<div className="space-y-6">
{/* Server Overview */}
<div>
<Text className="text-lg font-semibold mb-4">Server Overview</Text>
<Text className="text-lg font-semibold mb-4">{t("aiHub.modelHubTable.mcpServerOverview")}</Text>
<div className="grid grid-cols-2 gap-4 mb-4">
<div>
<Text className="font-medium">Server Name:</Text>
<Text className="font-medium">{t("aiHub.modelHubTable.fieldServerName")}</Text>
<Text>{selectedMcpServer.server_name}</Text>
</div>
<div>
<Text className="font-medium">Server ID:</Text>
<Text className="font-medium">{t("aiHub.modelHubTable.fieldServerId")}</Text>
<div className="flex items-center space-x-2">
<Text className="text-xs truncate">{selectedMcpServer.server_id}</Text>
<CopyOutlined
@ -894,22 +913,22 @@ print(response.choices[0].message.content)`}
</div>
{selectedMcpServer.alias && (
<div>
<Text className="font-medium">Alias:</Text>
<Text className="font-medium">{t("aiHub.modelHubTable.fieldAlias")}</Text>
<Text>{selectedMcpServer.alias}</Text>
</div>
)}
<div>
<Text className="font-medium">Transport:</Text>
<Text className="font-medium">{t("aiHub.modelHubTable.fieldTransport")}</Text>
<Badge color="blue">{selectedMcpServer.transport}</Badge>
</div>
<div>
<Text className="font-medium">Auth Type:</Text>
<Text className="font-medium">{t("aiHub.modelHubTable.fieldAuthType")}</Text>
<Badge color={selectedMcpServer.auth_type === "none" ? "gray" : "green"}>
{selectedMcpServer.auth_type}
</Badge>
</div>
<div>
<Text className="font-medium">Status:</Text>
<Text className="font-medium">{t("common.status")}:</Text>
<Badge
color={
selectedMcpServer.status === "active" || selectedMcpServer.status === "healthy"
@ -919,13 +938,13 @@ print(response.choices[0].message.content)`}
: "gray"
}
>
{selectedMcpServer.status || "unknown"}
{selectedMcpServer.status || t("common.unknown")}
</Badge>
</div>
</div>
{selectedMcpServer.description && (
<div className="mt-2">
<Text className="font-medium">Description:</Text>
<Text className="font-medium">{t("common.description")}:</Text>
<Text className="mt-1">{selectedMcpServer.description}</Text>
</div>
)}
@ -933,10 +952,10 @@ print(response.choices[0].message.content)`}
{/* Connection Details */}
<div>
<Text className="text-lg font-semibold mb-4">Connection Details</Text>
<Text className="text-lg font-semibold mb-4">{t("aiHub.modelHubTable.sectionConnectionDetails")}</Text>
<div className="space-y-2">
<div>
<Text className="font-medium">URL:</Text>
<Text className="font-medium">{t("aiHub.modelHubTable.fieldUrl")}</Text>
<div className="flex items-center space-x-2 mt-1">
<Text className="text-sm break-all bg-gray-100 p-2 rounded flex-1">{selectedMcpServer.url}</Text>
<CopyOutlined
@ -947,7 +966,7 @@ print(response.choices[0].message.content)`}
</div>
{selectedMcpServer.command && (
<div>
<Text className="font-medium">Command:</Text>
<Text className="font-medium">{t("aiHub.modelHubTable.fieldCommand")}</Text>
<Text className="text-sm bg-gray-100 p-2 rounded mt-1 font-mono">{selectedMcpServer.command}</Text>
</div>
)}
@ -957,7 +976,7 @@ print(response.choices[0].message.content)`}
{/* Tools */}
{selectedMcpServer.allowed_tools && selectedMcpServer.allowed_tools.length > 0 && (
<div>
<Text className="text-lg font-semibold mb-4">Allowed Tools</Text>
<Text className="text-lg font-semibold mb-4">{t("aiHub.modelHubTable.sectionAllowedTools")}</Text>
<div className="flex flex-wrap gap-2">
{selectedMcpServer.allowed_tools.map((tool, idx) => (
<Badge key={idx} color="purple">
@ -971,7 +990,7 @@ print(response.choices[0].message.content)`}
{/* Teams */}
{selectedMcpServer.teams && selectedMcpServer.teams.length > 0 && (
<div>
<Text className="text-lg font-semibold mb-4">Teams</Text>
<Text className="text-lg font-semibold mb-4">{t("aiHub.modelHubTable.sectionTeams")}</Text>
<div className="flex flex-wrap gap-2">
{selectedMcpServer.teams.map((team, idx) => (
<Badge key={idx} color="blue">
@ -985,7 +1004,7 @@ print(response.choices[0].message.content)`}
{/* Access Groups */}
{selectedMcpServer.mcp_access_groups && selectedMcpServer.mcp_access_groups.length > 0 && (
<div>
<Text className="text-lg font-semibold mb-4">Access Groups</Text>
<Text className="text-lg font-semibold mb-4">{t("aiHub.modelHubTable.sectionAccessGroups")}</Text>
<div className="flex flex-wrap gap-2">
{selectedMcpServer.mcp_access_groups.map((group, idx) => (
<Badge key={idx} color="green">
@ -998,34 +1017,34 @@ print(response.choices[0].message.content)`}
{/* Metadata */}
<div>
<Text className="text-lg font-semibold mb-4">Metadata</Text>
<Text className="text-lg font-semibold mb-4">{t("aiHub.modelHubTable.sectionMetadata")}</Text>
<div className="grid grid-cols-2 gap-4">
<div>
<Text className="font-medium">Created By:</Text>
<Text className="font-medium">{t("aiHub.modelHubTable.fieldCreatedBy")}</Text>
<Text>{selectedMcpServer.created_by}</Text>
</div>
<div>
<Text className="font-medium">Updated By:</Text>
<Text className="font-medium">{t("aiHub.modelHubTable.fieldUpdatedBy")}</Text>
<Text>{selectedMcpServer.updated_by}</Text>
</div>
<div>
<Text className="font-medium">Created At:</Text>
<Text className="font-medium">{t("common.createdAt")}:</Text>
<Text className="text-sm">{new Date(selectedMcpServer.created_at).toLocaleString()}</Text>
</div>
<div>
<Text className="font-medium">Updated At:</Text>
<Text className="font-medium">{t("common.updatedAt")}:</Text>
<Text className="text-sm">{new Date(selectedMcpServer.updated_at).toLocaleString()}</Text>
</div>
{selectedMcpServer.last_health_check && (
<div>
<Text className="font-medium">Last Health Check:</Text>
<Text className="font-medium">{t("aiHub.modelHubTable.fieldLastHealthCheck")}</Text>
<Text className="text-sm">{new Date(selectedMcpServer.last_health_check).toLocaleString()}</Text>
</div>
)}
</div>
{selectedMcpServer.health_check_error && (
<div className="mt-2 p-2 bg-red-50 rounded">
<Text className="font-medium text-red-700">Health Check Error:</Text>
<Text className="font-medium text-red-700">{t("aiHub.modelHubTable.fieldHealthCheckError")}</Text>
<Text className="text-sm text-red-600 mt-1">{selectedMcpServer.health_check_error}</Text>
</div>
)}
@ -1033,7 +1052,7 @@ print(response.choices[0].message.content)`}
{/* Usage Example */}
<div>
<Text className="text-lg font-semibold mb-4">Usage Example</Text>
<Text className="text-lg font-semibold mb-4">{t("aiHub.modelHubTable.sectionUsageExample")}</Text>
<SyntaxHighlighter language="python" className="text-sm">
{`from fastmcp import Client
import asyncio

View file

@ -72,7 +72,7 @@ const SkillHubDashboard: React.FC<SkillHubDashboardProps> = ({
}
if (isLoading) {
return <div className="text-center py-16 text-gray-400">Loading skills...</div>;
return <div className="text-center py-16 text-gray-400">{t("aiHub.skillHubDashboard.loadingSkills")}</div>;
}
return (
@ -80,15 +80,15 @@ const SkillHubDashboard: React.FC<SkillHubDashboardProps> = ({
{/* Stats row */}
<div className="grid grid-cols-3 gap-4">
<div className="border border-gray-200 rounded-lg p-4">
<div className="text-xs text-gray-500 mb-1">Total Skills</div>
<div className="text-xs text-gray-500 mb-1">{t("aiHub.skillHubDashboard.statTotalSkills")}</div>
<div className="text-2xl font-semibold text-gray-900">{totalSkills}</div>
</div>
<div className="border border-gray-200 rounded-lg p-4">
<div className="text-xs text-gray-500 mb-1">Namespaces</div>
<div className="text-xs text-gray-500 mb-1">{t("aiHub.skillHubDashboard.statNamespaces")}</div>
<div className="text-2xl font-semibold text-gray-900">{namespaces.length}</div>
</div>
<div className="border border-gray-200 rounded-lg p-4">
<div className="text-xs text-gray-500 mb-1">Domains</div>
<div className="text-xs text-gray-500 mb-1">{t("aiHub.skillHubDashboard.statDomains")}</div>
<div className="text-2xl font-semibold text-gray-900">{domains.length}</div>
</div>
</div>
@ -96,10 +96,12 @@ const SkillHubDashboard: React.FC<SkillHubDashboardProps> = ({
{/* Search + filters + table */}
<div>
<div className="flex items-center justify-between mb-3">
<h3 className="text-sm font-semibold text-gray-700">All {publicPage ? "Public " : ""}Skills</h3>
<h3 className="text-sm font-semibold text-gray-700">
{publicPage ? t("aiHub.skillHubDashboard.allPublicSkills") : t("aiHub.skillHubDashboard.allSkills")}
</h3>
<div className="flex items-center gap-2">
<Select
placeholder="All Domains"
placeholder={t("aiHub.skillHubDashboard.allDomainsPlaceholder")}
allowClear
value={domainFilter}
onChange={(val) => setDomainFilter(val)}
@ -108,7 +110,7 @@ const SkillHubDashboard: React.FC<SkillHubDashboardProps> = ({
/>
<Input
prefix={<SearchOutlined className="text-gray-400" />}
placeholder="Search by name, namespace, or tag…"
placeholder={t("aiHub.skillHubDashboard.searchPlaceholder")}
value={search}
onChange={(e) => setSearch(e.target.value)}
style={{ width: 280 }}
@ -124,7 +126,7 @@ const SkillHubDashboard: React.FC<SkillHubDashboardProps> = ({
/>
<div className="mt-3 text-center">
<Text className="text-sm text-gray-500">
Showing {filteredSkills.length} of {totalSkills} skill{totalSkills !== 1 ? "s" : ""}
{t("aiHub.skillHubDashboard.showing", { filtered: filteredSkills.length, count: totalSkills })}
</Text>
</div>
</div>

View file

@ -6490,5 +6490,109 @@
"saveSuccess": "Permissions updated successfully",
"saveFailed": "Failed to update permissions"
}
},
"aiHub": {
"agentHubTableColumns": {
"colAgentName": "Agent Name",
"copyAgentName": "Copy agent name",
"colVersion": "Version",
"colProtocol": "Protocol",
"colSkills": "Skills",
"skillCount_one": "{{count}} skill",
"skillCount_other": "{{count}} skills",
"colCapabilities": "Capabilities",
"colIOModes": "I/O Modes",
"ioIn": "In:",
"ioOut": "Out:",
"colPublic": "Public",
"detailsShort": "Info"
},
"modelHubTable": {
"title": "AI Hub",
"adminDescription": "Make models, agents, and MCP servers public for developers to know what's available.",
"userDescription": "A list of all public model names personally available to you.",
"modelHubUrl": "Model Hub URL:",
"copyUrl": "Copy URL",
"copiedToClipboard": "Copied to clipboard!",
"tabModelHub": "Model Hub",
"tabAgentHub": "Agent Hub",
"tabMcpHub": "MCP Hub",
"tabSkillHub": "Skill Hub",
"selectModelsPublic": "Select Models to Make Public",
"selectAgentsPublic": "Select Agents to Make Public",
"selectMcpPublic": "Select MCP Servers to Make Public",
"selectSkillsPublic": "Select Skills to Make Public",
"showingModels": "Showing {{filtered}} of {{total}} models",
"showingAgents_one": "Showing {{count}} agent",
"showingAgents_other": "Showing {{count}} agents",
"showingMcpServers_one": "Showing {{count}} MCP server",
"showingMcpServers_other": "Showing {{count}} MCP servers",
"publicHubNotEnabled": "Public Model Hub not enabled.",
"askAdminToEnable": "Ask your proxy admin to enable this on their Admin UI.",
"publicModelHubTitle": "Public Model Hub",
"shareableLink": "Shareable Link:",
"seePage": "See Page",
"modelDetails": "Model Details",
"modelOverview": "Model Overview",
"fieldModelGroup": "Model Group:",
"fieldMode": "Mode:",
"fieldProviders": "Providers:",
"fieldMaxInputTokens": "Max Input Tokens:",
"fieldMaxOutputTokens": "Max Output Tokens:",
"fieldInputCost": "Input Cost per 1M Tokens:",
"fieldOutputCost": "Output Cost per 1M Tokens:",
"notSpecified": "Not specified",
"tokenCostInfo": "Token & Cost Information",
"sectionCapabilities": "Capabilities",
"noCapabilities": "No special capabilities listed",
"sectionRateLimits": "Rate Limits",
"fieldTokensPerMinute": "Tokens per Minute:",
"fieldRequestsPerMinute": "Requests per Minute:",
"sectionSupportedParams": "Supported OpenAI Parameters",
"sectionUsageExample": "Usage Example",
"agentDetails": "Agent Details",
"agentOverview": "Agent Overview",
"fieldName": "Name:",
"fieldVersion": "Version:",
"fieldProtocolVersion": "Protocol Version:",
"fieldUrl": "URL:",
"sectionInputOutputModes": "Input/Output Modes",
"fieldInputModes": "Input Modes:",
"fieldOutputModes": "Output Modes:",
"sectionSkills": "Skills",
"skillId": "ID: {{id}}",
"skillExamples": "Examples:",
"sectionAdditionalFeatures": "Additional Features",
"supportsAuthCard": "Supports Authenticated Extended Card",
"mcpServerDetails": "MCP Server Details",
"mcpServerOverview": "Server Overview",
"fieldServerName": "Server Name:",
"fieldServerId": "Server ID:",
"fieldAlias": "Alias:",
"fieldTransport": "Transport:",
"fieldAuthType": "Auth Type:",
"sectionConnectionDetails": "Connection Details",
"fieldCommand": "Command:",
"sectionAllowedTools": "Allowed Tools",
"sectionTeams": "Teams",
"sectionAccessGroups": "Access Groups",
"sectionMetadata": "Metadata",
"fieldCreatedBy": "Created By:",
"fieldUpdatedBy": "Updated By:",
"fieldLastHealthCheck": "Last Health Check:",
"fieldHealthCheckError": "Health Check Error:"
},
"skillHubDashboard": {
"loadingSkills": "Loading skills...",
"statTotalSkills": "Total Skills",
"statNamespaces": "Namespaces",
"statDomains": "Domains",
"allSkills": "All Skills",
"allPublicSkills": "All Public Skills",
"allDomainsPlaceholder": "All Domains",
"searchPlaceholder": "Search by name, namespace, or tag…",
"showing_one": "Showing {{filtered}} of {{count}} skill",
"showing_other": "Showing {{filtered}} of {{count}} skills"
}
}
}

View file

@ -6490,5 +6490,109 @@
"saveSuccess": "权限更新成功",
"saveFailed": "更新权限失败"
}
},
"aiHub": {
"agentHubTableColumns": {
"colAgentName": "Agent 名称",
"copyAgentName": "复制 Agent 名称",
"colVersion": "版本",
"colProtocol": "协议",
"colSkills": "技能",
"skillCount_one": "{{count}} 个技能",
"skillCount_other": "{{count}} 个技能",
"colCapabilities": "能力",
"colIOModes": "输入/输出模式",
"ioIn": "输入:",
"ioOut": "输出:",
"colPublic": "公开",
"detailsShort": "详情"
},
"modelHubTable": {
"title": "AI Hub",
"adminDescription": "将模型、Agent 和 MCP 服务器设为公开,让开发者了解可用资源。",
"userDescription": "您个人可用的所有公开模型名称列表。",
"modelHubUrl": "模型 Hub URL:",
"copyUrl": "复制 URL",
"copiedToClipboard": "已复制到剪贴板!",
"tabModelHub": "模型 Hub",
"tabAgentHub": "Agent Hub",
"tabMcpHub": "MCP Hub",
"tabSkillHub": "技能 Hub",
"selectModelsPublic": "选择要公开的模型",
"selectAgentsPublic": "选择要公开的 Agent",
"selectMcpPublic": "选择要公开的 MCP 服务器",
"selectSkillsPublic": "选择要公开的技能",
"showingModels": "显示 {{filtered}} / {{total}} 个模型",
"showingAgents_one": "显示 {{count}} 个 Agent",
"showingAgents_other": "显示 {{count}} 个 Agent",
"showingMcpServers_one": "显示 {{count}} 个 MCP 服务器",
"showingMcpServers_other": "显示 {{count}} 个 MCP 服务器",
"publicHubNotEnabled": "公开模型 Hub 未启用。",
"askAdminToEnable": "请联系代理管理员在管理界面中启用此功能。",
"publicModelHubTitle": "公开模型 Hub",
"shareableLink": "可分享链接:",
"seePage": "查看页面",
"modelDetails": "模型详情",
"modelOverview": "模型概览",
"fieldModelGroup": "模型组:",
"fieldMode": "模式:",
"fieldProviders": "提供商:",
"fieldMaxInputTokens": "最大输入 Token 数:",
"fieldMaxOutputTokens": "最大输出 Token 数:",
"fieldInputCost": "每百万 Token 输入费用:",
"fieldOutputCost": "每百万 Token 输出费用:",
"notSpecified": "未指定",
"tokenCostInfo": "Token 与费用信息",
"sectionCapabilities": "能力",
"noCapabilities": "暂无特殊能力说明",
"sectionRateLimits": "速率限制",
"fieldTokensPerMinute": "每分钟 Token 数:",
"fieldRequestsPerMinute": "每分钟请求数:",
"sectionSupportedParams": "支持的 OpenAI 参数",
"sectionUsageExample": "使用示例",
"agentDetails": "Agent 详情",
"agentOverview": "Agent 概览",
"fieldName": "名称:",
"fieldVersion": "版本:",
"fieldProtocolVersion": "协议版本:",
"fieldUrl": "URL:",
"sectionInputOutputModes": "输入/输出模式",
"fieldInputModes": "输入模式:",
"fieldOutputModes": "输出模式:",
"sectionSkills": "技能",
"skillId": "ID:{{id}}",
"skillExamples": "示例:",
"sectionAdditionalFeatures": "附加功能",
"supportsAuthCard": "支持已认证扩展卡片",
"mcpServerDetails": "MCP 服务器详情",
"mcpServerOverview": "服务器概览",
"fieldServerName": "服务器名称:",
"fieldServerId": "服务器 ID:",
"fieldAlias": "别名:",
"fieldTransport": "传输方式:",
"fieldAuthType": "认证类型:",
"sectionConnectionDetails": "连接详情",
"fieldCommand": "命令:",
"sectionAllowedTools": "允许的工具",
"sectionTeams": "团队",
"sectionAccessGroups": "访问组",
"sectionMetadata": "元数据",
"fieldCreatedBy": "创建人:",
"fieldUpdatedBy": "更新人:",
"fieldLastHealthCheck": "最后健康检查:",
"fieldHealthCheckError": "健康检查错误:"
},
"skillHubDashboard": {
"loadingSkills": "正在加载技能...",
"statTotalSkills": "技能总数",
"statNamespaces": "命名空间",
"statDomains": "域",
"allSkills": "全部技能",
"allPublicSkills": "全部公开技能",
"allDomainsPlaceholder": "所有域",
"searchPlaceholder": "按名称、命名空间或标签搜索…",
"showing_one": "显示 {{filtered}} / {{count}} 个技能",
"showing_other": "显示 {{filtered}} / {{count}} 个技能"
}
}
}