refactor(ui): move remaining playground, chat and compliance text onto i18n (#50239)

Compliance framework and category names, the Compare endpoint labels, the
Decisions presets and schema messages, chat metrics, MCP events, reasoning
toggle, conversation date groups, model filters and the MCP boolean argument
labels now resolve through en.json. Ids, stored values and text sent to a
model are unchanged
This commit is contained in:
yuneng-jiang 2026-11-05 12:42:59 -08:00 • committed by GitHub
parent 69ffe4279e
commit 09b721ab2e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 350 additions and 90 deletions

View file

@ -498,7 +498,7 @@ export default function CompareUI({ accessToken, disabledPersonalKeyCreation }:
}
// Validate selection based on endpoint type
if (targetComparisons.some((comparison) => !hasValidSelection(comparison, selectedEndpoint))) {
toast.fromError(endpointConfig.validationMessage);
toast.fromError(t(endpointConfig.validationMessage));
return;
}
@ -825,10 +825,10 @@ export default function CompareUI({ accessToken, disabledPersonalKeyCreation }:
) : isAnyComparisonLoading ? (
<span className="flex items-center gap-2 text-sm text-muted-foreground">
<span className="h-2 w-2 rounded-full bg-info animate-pulse" aria-hidden />
{endpointConfig.loadingMessage}
{t(endpointConfig.loadingMessage)}
</span>
) : (
<span className="text-sm text-muted-foreground">{endpointConfig.inputPlaceholder}</span>
<span className="text-sm text-muted-foreground">{t(endpointConfig.inputPlaceholder)}</span>
)}
</div>
{uploadedFile && (

View file

@ -1,6 +1,7 @@
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi } from "vitest";
import i18n from "@/i18n/i18n";
import { UnifiedSelector } from "./UnifiedSelector";
import { EndpointId, ENDPOINT_CONFIGS } from "../endpoint_config";
@ -32,19 +33,19 @@ describe("UnifiedSelector", () => {
it("prompts with the endpoint's own selector copy", () => {
render(<UnifiedSelector value="" options={[]} loading={false} config={CHAT} onChange={vi.fn()} />);
expect(promptIsVisible(CHAT.selectorPlaceholder)).toBe(true);
expect(promptIsVisible(i18n.t(CHAT.selectorPlaceholder))).toBe(true);
});
it("prompts with the agent endpoint's copy when configured for agents", () => {
render(<UnifiedSelector value="" options={[]} loading={false} config={AGENTS} onChange={vi.fn()} />);
expect(promptIsVisible(AGENTS.selectorPlaceholder)).toBe(true);
expect(promptIsVisible(i18n.t(AGENTS.selectorPlaceholder))).toBe(true);
});
it("swaps the prompt for a loading message while options are in flight", () => {
render(<UnifiedSelector value="" options={[]} loading config={CHAT} onChange={vi.fn()} />);
expect(promptIsVisible(`Loading ${CHAT.selectorLabel.toLowerCase()}s...`)).toBe(true);
expect(promptIsVisible(`Loading ${i18n.t(CHAT.selectorLabel).toLowerCase()}s...`)).toBe(true);
});
it("reports the chosen option's value, not its label", async () => {
@ -116,6 +117,6 @@ describe("UnifiedSelector", () => {
await openList(user);
expect(await screen.findByText(`No ${CHAT.selectorLabel.toLowerCase()}s available`)).toBeInTheDocument();
expect(await screen.findByText(`No ${i18n.t(CHAT.selectorLabel).toLowerCase()}s available`)).toBeInTheDocument();
});
});

View file

@ -29,7 +29,7 @@ const matchesQuery = (option: SelectorOption, query: string): boolean =>
export function UnifiedSelector({ value, options, loading, config, onChange }: UnifiedSelectorProps) {
const { t } = useTranslation();
const selected = options.find((option) => option.value === value) ?? null;
const noun = config.selectorLabel.toLowerCase();
const noun = t(config.selectorLabel).toLowerCase();
return (
<Combobox
@ -41,7 +41,7 @@ export function UnifiedSelector({ value, options, loading, config, onChange }: U
filter={matchesQuery}
>
<ComboboxInput
placeholder={loading ? t("playground.compare.selector.loading", { noun }) : config.selectorPlaceholder}
placeholder={loading ? t("playground.compare.selector.loading", { noun }) : t(config.selectorPlaceholder)}
className="w-48 md:w-64 lg:w-72"
/>
<ComboboxContent>

View file

@ -1,4 +1,5 @@
import { describe, expect, it } from "vitest";
import i18n from "@/i18n/i18n";
import {
EndpointId,
ENDPOINT_CONFIGS,
@ -44,7 +45,7 @@ describe("endpoint_config", () => {
const config = getEndpointConfig(EndpointId.CHAT_COMPLETIONS);
expect(config.id).toBe(EndpointId.CHAT_COMPLETIONS);
expect(config.selectorType).toBe("model");
expect(config.selectorLabel).toBe("Model");
expect(i18n.t(config.selectorLabel)).toBe("Model");
});
it("should check if endpoint is agent endpoint", () => {

View file

@ -3,6 +3,7 @@
* Add new endpoints here to extend the comparison functionality.
*/
import type { ParseKeys } from "i18next";
import { Agent } from "../../llm_calls/fetch_agents";
// Endpoint identifiers
@ -23,11 +24,11 @@ export interface EndpointConfig {
id: EndpointIdType;
label: string;
selectorType: SelectorType;
selectorLabel: string;
selectorPlaceholder: string;
inputPlaceholder: string;
loadingMessage: string;
validationMessage: string;
selectorLabel: ParseKeys;
selectorPlaceholder: ParseKeys;
inputPlaceholder: ParseKeys;
loadingMessage: ParseKeys;
validationMessage: ParseKeys;
}
// Endpoint configurations
@ -36,21 +37,21 @@ export const ENDPOINT_CONFIGS: Record<EndpointIdType, EndpointConfig> = {
id: EndpointId.CHAT_COMPLETIONS,
label: "/v1/chat/completions",
selectorType: "model",
selectorLabel: "Model",
selectorPlaceholder: "Select a model",
inputPlaceholder: "Send a prompt to compare models",
loadingMessage: "Gathering responses from all models...",
validationMessage: "Select a model before sending a message.",
selectorLabel: "playground.compare.endpoints.chatCompletions.selectorLabel",
selectorPlaceholder: "playground.compare.endpoints.chatCompletions.selectorPlaceholder",
inputPlaceholder: "playground.compare.endpoints.chatCompletions.inputPlaceholder",
loadingMessage: "playground.compare.endpoints.chatCompletions.loadingMessage",
validationMessage: "playground.compare.endpoints.chatCompletions.validationMessage",
},
[EndpointId.A2A_AGENTS]: {
id: EndpointId.A2A_AGENTS,
label: "/a2a (Agents)",
selectorType: "agent",
selectorLabel: "Agent",
selectorPlaceholder: "Select an agent",
inputPlaceholder: "Send a message to compare agents",
loadingMessage: "Gathering responses from all agents...",
validationMessage: "Select an agent before sending a message.",
selectorLabel: "playground.compare.endpoints.a2aAgents.selectorLabel",
selectorPlaceholder: "playground.compare.endpoints.a2aAgents.selectorPlaceholder",
inputPlaceholder: "playground.compare.endpoints.a2aAgents.inputPlaceholder",
loadingMessage: "playground.compare.endpoints.a2aAgents.loadingMessage",
validationMessage: "playground.compare.endpoints.a2aAgents.validationMessage",
},
};

View file

@ -1,6 +1,8 @@
"use client";
import {
COMPLIANCE_CATEGORY_LABEL_KEYS,
COMPLIANCE_FRAMEWORK_LABEL_KEYS,
getFrameworks,
type ComplianceCategory,
type ComplianceFramework,
@ -46,6 +48,7 @@ import {
Upload,
X,
} from "lucide-react";
import type { ParseKeys, TFunction } from "i18next";
import Papa from "papaparse";
import { Trans, useTranslation } from "react-i18next";
import React, { useCallback, useEffect, useRef, useState } from "react";
@ -67,6 +70,21 @@ const CATEGORY_ICON_MAP: Record<string, React.ComponentType<{ className?: string
"file-text": FileText,
};
const FRAMEWORK_LABEL_KEYS: Readonly<Record<string, ParseKeys>> = {
...COMPLIANCE_FRAMEWORK_LABEL_KEYS,
Custom: "playground.compliance.library.customFramework",
"CSV Upload": "playground.compliance.library.csvFramework",
};
const CATEGORY_LABEL_KEYS: Readonly<Record<string, ParseKeys>> = {
...COMPLIANCE_CATEGORY_LABEL_KEYS,
"Custom Prompts": "playground.compliance.library.customCategory",
"Uploaded Prompts": "playground.compliance.library.csvCategory",
};
const labelFor = (labelKeys: Readonly<Record<string, ParseKeys>>, id: string, t: TFunction): string =>
Object.hasOwn(labelKeys, id) ? t(labelKeys[id]) : id;
function CategoryIcon({
iconKey,
className = "w-4 h-4 text-muted-foreground",
@ -1085,7 +1103,9 @@ export default function ComplianceUI({
)}
<CategoryIcon iconKey={fw.icon} className="w-4 h-4 text-muted-foreground shrink-0" />
<div className="flex-1 min-w-0">
<span className="text-xs font-semibold text-foreground">{fw.name}</span>
<span className="text-xs font-semibold text-foreground">
{labelFor(FRAMEWORK_LABEL_KEYS, fw.name, t)}
</span>
<span className="text-[10px] text-muted-foreground ml-1.5">
{t("playground.compliance.library.promptCount", { total: fwPromptCount })}
</span>
@ -1137,7 +1157,7 @@ export default function ComplianceUI({
/>
</span>
<span className="text-[11px] font-medium text-foreground flex-1 min-w-0 truncate">
{category.name}
{labelFor(CATEGORY_LABEL_KEYS, category.name, t)}
</span>
<span className="text-[10px] text-muted-foreground shrink-0">
{category.prompts.length}
@ -1153,7 +1173,9 @@ export default function ComplianceUI({
<div>
<div className="px-2.5 py-1 flex items-center justify-between">
<p className="text-[10px] text-muted-foreground leading-relaxed flex-1 mr-2 line-clamp-2">
{category.description}
{category.descriptionKey
? term(t(category.descriptionKey))
: category.description}
</p>
<button
type="button"
@ -1524,7 +1546,7 @@ export default function ComplianceUI({
<div className="flex items-center gap-1.5 flex-wrap">
<span className="text-[9px] text-muted-foreground inline-flex items-center gap-0.5">
<CategoryIcon iconKey={result.categoryIcon} className="w-3 h-3" />
{result.category}
{labelFor(CATEGORY_LABEL_KEYS, result.category, t)}
</span>
<span
className={`text-[9px] font-semibold px-1 py-0.5 rounded-sm ${result.expectedResult === "fail" ? "bg-destructive/10 text-destructive" : "bg-success/10 text-success"}`}

View file

@ -67,7 +67,7 @@ function ExamplesMenu({ onPick }: { onPick: (id: string) => void }) {
<DropdownMenuContent align="end" className="w-48">
{DECISION_PRESETS.map((preset) => (
<DropdownMenuItem key={preset.id} onClick={() => onPick(preset.id)}>
{preset.label}
{t(preset.label)}
</DropdownMenuItem>
))}
</DropdownMenuContent>

View file

@ -1,4 +1,5 @@
import { describe, expect, it } from "vitest";
import i18n from "@/i18n/i18n";
import {
DECISION_PRESETS,
emptyPayload,
@ -10,7 +11,7 @@ import {
import { validateOpenAIDecisionsPayload, validateSystemOnePayload } from "./validatePayload";
describe("decision presets", () => {
it.each(DECISION_PRESETS.map((preset) => [preset.label, preset] as const))(
it.each(DECISION_PRESETS.map((preset) => [i18n.t(preset.label), preset] as const))(
"%s is a valid request on both endpoints",
(_label, preset) => {
expect(

View file

@ -1,3 +1,4 @@
import type { ParseKeys } from "i18next";
import type { DecisionRequest, SystemOneRequest } from "./schemas";
import type { OpenAIDecisionsRequest } from "./openAIDecisions";
import { parseJson } from "./validatePayload";
@ -32,7 +33,7 @@ type PresetEndpoint = "/v1/decisions" | "/v1/systemone";
export interface DecisionPreset {
id: string;
label: string;
label: ParseKeys;
request: PresetRequest;
}
@ -126,10 +127,10 @@ const leadScoring = {
} satisfies PresetRequest;
export const DECISION_PRESETS: readonly DecisionPreset[] = [
{ id: "bug-triage", label: "Triage a bug report", request: bugTriage },
{ id: "support-routing", label: "Route a support ticket", request: supportRouting },
{ id: "moderation", label: "Moderate a forum comment", request: moderation },
{ id: "lead-scoring", label: "Score a sales lead", request: leadScoring },
{ id: "bug-triage", label: "playground.systemOne.presets.bugTriage", request: bugTriage },
{ id: "support-routing", label: "playground.systemOne.presets.supportRouting", request: supportRouting },
{ id: "moderation", label: "playground.systemOne.presets.moderation", request: moderation },
{ id: "lead-scoring", label: "playground.systemOne.presets.leadScoring", request: leadScoring },
];
const toOpenAIQuestion = (name: string, question: PresetQuestion): OpenAIDecisionsRequest["questions"][number] => {

View file

@ -1,4 +1,5 @@
import { z } from "zod";
import i18n from "@/i18n/i18n";
const contentPart = z.discriminatedUnion("type", [
z.strictObject({ type: z.literal("input_text"), text: z.string() }),
@ -23,7 +24,7 @@ const choiceQuestion = z
.refine(
(question) => new Set(question.choices.map((choice) => String(choice.value))).size === question.choices.length,
{
message: "Choice values must be unique, including their string representations.",
error: () => i18n.t("playground.systemOne.openAIDecisions.uniqueChoices"),
path: ["choices"],
},
);
@ -31,7 +32,9 @@ const choiceQuestion = z
const requestFields = {
model: z
.string()
.refine((value) => value.trim().length > 0, "Model must be a non-empty string.")
.refine((value) => value.trim().length > 0, {
error: () => i18n.t("playground.systemOne.openAIDecisions.modelRequired"),
})
.optional(),
input: z.union([z.string(), z.array(inputMessage)]),
questions: z

View file

@ -1,4 +1,6 @@
import type { ParseKeys } from "i18next";
import { z } from "zod";
import i18n from "@/i18n/i18n";
export type DecisionEndpoint = "/v1/systemone" | "/typesafe/v1/systemone";
@ -92,13 +94,16 @@ const scoreQuestionSchema = z.looseObject({
.min(2, "Score criteria must contain at least 2 levels."),
});
const QUESTION_ERROR_MESSAGES: Partial<Record<z.core.$ZodIssue["code"], string>> = {
invalid_union: "Question type must be choice, noul, or score.",
invalid_type: "Question must be an object.",
const QUESTION_ERROR_MESSAGES: Partial<Record<z.core.$ZodIssue["code"], ParseKeys>> = {
invalid_union: "playground.systemOne.schemas.questionType",
invalid_type: "playground.systemOne.schemas.questionObject",
};
const questionSchema = z.discriminatedUnion("type", [choiceQuestionSchema, noulQuestionSchema, scoreQuestionSchema], {
error: (iss) => QUESTION_ERROR_MESSAGES[iss.code],
error: (iss) => {
const key = QUESTION_ERROR_MESSAGES[iss.code];
return key === undefined ? undefined : i18n.t(key);
},
});
export const systemOneRequestSchema = z.looseObject(

View file

@ -23,7 +23,7 @@ import {
createPolicyVersion,
updatePolicyVersionStatus,
} from "@/components/networking";
import { getComplianceDatasetPrompts, getFrameworks } from "@/data/compliancePrompts";
import { COMPLIANCE_FRAMEWORK_LABEL_KEYS, getComplianceDatasetPrompts, getFrameworks } from "@/data/compliancePrompts";
import type { CompliancePrompt } from "@/data/compliancePrompts";
const TEST_SOURCE_QUICK = "quick_chat";
@ -853,7 +853,11 @@ type TestSourceOption = { value: string; labelKey: ParseKeys } | { value: string
const testSourceOptions: TestSourceOption[] = [
{ value: TEST_SOURCE_QUICK, labelKey: "policies.flowBuilder.testPanel.sources.quickChat" },
...getFrameworks().map((f) => ({ value: f.name, label: f.name })),
...getFrameworks().map((f) =>
Object.hasOwn(COMPLIANCE_FRAMEWORK_LABEL_KEYS, f.name)
? { value: f.name, labelKey: COMPLIANCE_FRAMEWORK_LABEL_KEYS[f.name] }
: { value: f.name, label: f.name },
),
{ value: TEST_SOURCE_ALL, labelKey: "policies.flowBuilder.testPanel.sources.allDatasets" },
];
@ -1033,7 +1037,7 @@ const PipelineTestPanel: React.FC<PipelineTestPanelProps> = ({ pipeline, accessT
? t("policies.flowBuilder.testPanel.allDatasetsDescription")
: t("policies.flowBuilder.testPanel.frameworkDescription", {
promptCount: promptsForSource.length,
source: testSource,
source: selectedSource ? testSourceLabel(selectedSource, t) : testSource,
})}
</div>
)}

View file

@ -19,6 +19,7 @@ import {
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { ScrollArea } from "@/components/ui/scroll-area";
import dayjs from "dayjs";
import type { ParseKeys } from "i18next";
import { useTranslation } from "react-i18next";
import { Conversation } from "./types";
@ -47,6 +48,13 @@ const getDateGroup = (timestamp: number): DateGroup => {
const DATE_GROUP_ORDER: DateGroup[] = ["Recents", "Yesterday", "Last 7 Days", "Older"];
const DATE_GROUP_LABEL_KEYS: Readonly<Record<DateGroup, ParseKeys>> = {
Recents: "chat.conversationList.recent",
Yesterday: "chat.conversationList.yesterday",
"Last 7 Days": "chat.conversationList.lastSevenDays",
Older: "chat.conversationList.older",
};
interface GroupedConversations {
group: DateGroup;
items: Conversation[];
@ -350,7 +358,7 @@ const ConversationList: React.FC<Props> = ({
<div key={group} className="mb-2">
<div className="moyai-chat-history-heading flex items-center gap-2 px-2 pt-2 pb-1 text-xs font-normal text-sidebar-foreground">
<Clock3 className="size-3.5" aria-hidden="true" />
{group === "Recents" ? t("chat.conversationList.recent") : group}
{t(DATE_GROUP_LABEL_KEYS[group])}
</div>
{items.map((conv) => (
<ConversationRow

View file

@ -1,4 +1,5 @@
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { ChevronRight } from "lucide-react";
import type { MCPEvent } from "@/components/mcp_tools/types";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
@ -58,6 +59,7 @@ interface MCPEventsPanelsProps {
}
function MCPEventsPanels({ toolsEvent, mcpCallEvents }: MCPEventsPanelsProps) {
const { t } = useTranslation();
const [openPanels, setOpenPanels] = useState<Record<string, boolean>>({});
const toggleKey = (key: string, open: boolean) => {
@ -72,7 +74,7 @@ function MCPEventsPanels({ toolsEvent, mcpCallEvents }: MCPEventsPanelsProps) {
{toolsEvent && (
<MCPEventPanel
panelKey="list-tools"
title="List tools"
title={t("chatUi.mcpEvents.listTools")}
open={openPanels["list-tools"] ?? false}
onOpenChange={(open) => toggleKey("list-tools", open)}
>
@ -95,13 +97,15 @@ function MCPEventsPanels({ toolsEvent, mcpCallEvents }: MCPEventsPanelsProps) {
<MCPEventPanel
key={key}
panelKey={key}
title={callEvent.item?.name || "Tool call"}
title={callEvent.item?.name || t("chatUi.mcpEvents.toolCall")}
open={openPanels[key] ?? true}
onOpenChange={(open) => toggleKey(key, open)}
>
<div>
<div className="relative z-raised mb-3 bg-card last:mb-0">
<div className="mb-1 text-[13px] font-medium text-muted-foreground">Request</div>
<div className="mb-1 text-[13px] font-medium text-muted-foreground">
{t("chatUi.mcpEvents.request")}
</div>
<div className="rounded-md border border-border bg-muted p-2 text-xs">
{callEvent.item?.arguments && (
<pre className="m-0 whitespace-pre-wrap break-words font-mono text-foreground">
@ -116,13 +120,15 @@ function MCPEventsPanels({ toolsEvent, mcpCallEvents }: MCPEventsPanelsProps) {
<span className="mr-1.5 font-bold text-success" aria-hidden="true">
✓
</span>
Approved
{t("chatUi.mcpEvents.approved")}
</div>
</div>
{callEvent.item?.output && (
<div className="relative z-raised mb-3 bg-card last:mb-0">
<div className="mb-1 text-[13px] font-medium text-muted-foreground">Response</div>
<div className="mb-1 text-[13px] font-medium text-muted-foreground">
{t("chatUi.mcpEvents.response")}
</div>
<div className="whitespace-pre-wrap font-mono text-[13px] leading-normal text-foreground">
{callEvent.item.output}
</div>

View file

@ -1,4 +1,5 @@
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import ReactMarkdown from "react-markdown";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { coy } from "react-syntax-highlighter/dist/esm/styles/prism";
@ -13,6 +14,7 @@ interface ReasoningContentProps {
}
const ReasoningContent: React.FC<ReasoningContentProps> = ({ reasoningContent }) => {
const { t } = useTranslation();
const syntaxTheme = useSyntaxTheme(coy);
const [isExpanded, setIsExpanded] = useState(true);
@ -32,7 +34,7 @@ const ReasoningContent: React.FC<ReasoningContentProps> = ({ reasoningContent })
}
>
<Lightbulb className="size-3.5" />
{isExpanded ? "Hide reasoning" : "Show reasoning"}
{isExpanded ? t("chatUi.reasoning.hide") : t("chatUi.reasoning.show")}
{isExpanded ? <ChevronDown className="size-3" /> : <ChevronRight className="size-3" />}
</CollapsibleTrigger>

View file

@ -101,6 +101,7 @@ function PromptCacheChips({ usage }: { usage?: TokenUsage }) {
}
const ResponseMetrics: React.FC<ResponseMetricsProps> = ({ timeToFirstToken, totalLatency, usage, toolName }) => {
const { t } = useTranslation();
if (!timeToFirstToken && !totalLatency && !usage) return null;
return (
@ -108,7 +109,7 @@ const ResponseMetrics: React.FC<ResponseMetricsProps> = ({ timeToFirstToken, tot
{timeToFirstToken !== undefined && (
<MetricItem
label="TTFT"
tooltip="Time to first token"
tooltip={t("chatUi.responseMetrics.tooltips.timeToFirstToken")}
icon={<Clock className="size-3" aria-hidden="true" />}
value={`${(timeToFirstToken / 1000).toFixed(2)}s`}
/>
@ -116,8 +117,8 @@ const ResponseMetrics: React.FC<ResponseMetricsProps> = ({ timeToFirstToken, tot
{totalLatency !== undefined && (
<MetricItem
label="Total Latency"
tooltip="Total latency"
label={t("chatUi.responseMetrics.labels.totalLatency")}
tooltip={t("chatUi.responseMetrics.tooltips.totalLatency")}
icon={<Clock className="size-3" aria-hidden="true" />}
value={`${(totalLatency / 1000).toFixed(2)}s`}
/>
@ -125,8 +126,8 @@ const ResponseMetrics: React.FC<ResponseMetricsProps> = ({ timeToFirstToken, tot
{usage?.promptTokens !== undefined && (
<MetricItem
label="In"
tooltip="Prompt tokens"
label={t("chatUi.responseMetrics.labels.in")}
tooltip={t("chatUi.responseMetrics.tooltips.promptTokens")}
icon={<ArrowDownToLine className="size-3" aria-hidden="true" />}
value={String(usage.promptTokens)}
/>
@ -136,8 +137,8 @@ const ResponseMetrics: React.FC<ResponseMetricsProps> = ({ timeToFirstToken, tot
{usage?.completionTokens !== undefined && (
<MetricItem
label="Out"
tooltip="Completion tokens"
label={t("chatUi.responseMetrics.labels.out")}
tooltip={t("chatUi.responseMetrics.tooltips.completionTokens")}
icon={<ArrowUpFromLine className="size-3" aria-hidden="true" />}
value={String(usage.completionTokens)}
/>
@ -145,8 +146,8 @@ const ResponseMetrics: React.FC<ResponseMetricsProps> = ({ timeToFirstToken, tot
{usage?.reasoningTokens !== undefined && (
<MetricItem
label="Reasoning"
tooltip="Reasoning tokens"
label={t("chatUi.responseMetrics.labels.reasoning")}
tooltip={t("chatUi.responseMetrics.tooltips.reasoningTokens")}
icon={<Lightbulb className="size-3" aria-hidden="true" />}
value={String(usage.reasoningTokens)}
/>
@ -154,8 +155,8 @@ const ResponseMetrics: React.FC<ResponseMetricsProps> = ({ timeToFirstToken, tot
{usage?.totalTokens !== undefined && (
<MetricItem
label="Total"
tooltip="Total tokens"
label={t("chatUi.responseMetrics.labels.total")}
tooltip={t("chatUi.responseMetrics.tooltips.totalTokens")}
icon={<Hash className="size-3" aria-hidden="true" />}
value={String(usage.totalTokens)}
/>
@ -163,8 +164,8 @@ const ResponseMetrics: React.FC<ResponseMetricsProps> = ({ timeToFirstToken, tot
{typeof usage?.cost === "number" && Number.isFinite(usage.cost) && (
<MetricItem
label="Cost"
tooltip="Cost"
label={t("chatUi.responseMetrics.labels.cost")}
tooltip={t("chatUi.responseMetrics.tooltips.cost")}
icon={<DollarSign className="size-3" aria-hidden="true" />}
value={`$${usage.cost.toFixed(6)}`}
/>
@ -172,8 +173,8 @@ const ResponseMetrics: React.FC<ResponseMetricsProps> = ({ timeToFirstToken, tot
{toolName && (
<MetricItem
label="Tool"
tooltip="Tool used"
label={t("chatUi.responseMetrics.labels.tool")}
tooltip={t("chatUi.responseMetrics.tooltips.toolUsed")}
icon={<Wrench className="size-3" aria-hidden="true" />}
value={toolName}
/>

View file

@ -19,9 +19,9 @@ const argumentValues = (schema: InputSchema, values: ToolFormValues): Record<str
const STRING_SCHEMA_MESSAGES: Readonly<Record<string, ParseKeys>> = { input: "mcpToolArguments.stringSchemaRequired" };
const BOOLEAN_ITEMS = [
{ value: true, label: "True" },
{ value: false, label: "False" },
const BOOLEAN_ITEMS: readonly { value: boolean; label: ParseKeys }[] = [
{ value: true, label: "mcpToolArguments.booleanTrue" },
{ value: false, label: "mcpToolArguments.booleanFalse" },
];
const isBlank = (value: unknown): boolean => value === undefined || value === null || value === "";
@ -403,8 +403,11 @@ const MCPToolArgumentsForm = forwardRef<MCPToolArgumentsFormRef, MCPToolArgument
<Select
items={
required
? BOOLEAN_ITEMS
: [{ value: null, label: t("mcpToolArguments.selectKey", { key }) }, ...BOOLEAN_ITEMS]
? BOOLEAN_ITEMS.map((item) => ({ ...item, label: t(item.label) }))
: [
{ value: null, label: t("mcpToolArguments.selectKey", { key }) },
...BOOLEAN_ITEMS.map((item) => ({ ...item, label: t(item.label) })),
]
}
value={field.value ?? null}
onValueChange={field.onChange}
@ -421,8 +424,8 @@ const MCPToolArgumentsForm = forwardRef<MCPToolArgumentsFormRef, MCPToolArgument
{!required && (
<SelectItem value={null}>{t("mcpToolArguments.selectKey", { key })}</SelectItem>
)}
<SelectItem value={true}>True</SelectItem>
<SelectItem value={false}>False</SelectItem>
<SelectItem value={true}>{t("mcpToolArguments.booleanTrue")}</SelectItem>
<SelectItem value={false}>{t("mcpToolArguments.booleanFalse")}</SelectItem>
</SelectContent>
</Select>
);

View file

@ -1,4 +1,5 @@
import React, { useState, useEffect, useMemo, useRef } from "react";
import { useTranslation } from "react-i18next";
import { Search } from "lucide-react";
import ModelHubProviders from "@/components/AIHub/ModelHubProviders";
import { Button } from "@/components/ui/button";
@ -77,6 +78,7 @@ const ModelFilters: React.FC<ModelFiltersProps> = ({
showProviderFilter = true,
variant = "dashboard",
}) => {
const { t } = useTranslation();
const [searchTerm, setSearchTerm] = useState<string>("");
const [internalProvider, setInternalProvider] = useState<string>("");
const selectedProvider = controlledProvider ?? internalProvider;
@ -187,8 +189,8 @@ const ModelFilters: React.FC<ModelFiltersProps> = ({
<Search className="size-4" />
</InputGroupAddon>
<InputGroupInput
aria-label="Search models"
placeholder="Search model names..."
aria-label={t("modelFilters.searchLabel")}
placeholder={t("modelFilters.searchPlaceholder")}
value={searchTerm}
onChange={(event) => setSearchTerm(event.target.value)}
/>
@ -201,22 +203,22 @@ const ModelFilters: React.FC<ModelFiltersProps> = ({
/>
)}
<ChatFilterSelect
label="Model type"
emptyLabel="All types"
label={t("modelFilters.modelType")}
emptyLabel={t("modelFilters.allTypes")}
options={getUniqueModes(modelHubData)}
value={selectedMode}
onValueChange={changeMode}
/>
<ChatFilterSelect
label="Model capability"
emptyLabel="All capabilities"
label={t("modelFilters.modelCapability")}
emptyLabel={t("modelFilters.allCapabilities")}
options={getUniqueFeatures(modelHubData)}
value={selectedFeature}
onValueChange={setSelectedFeature}
/>
{hasActiveFilters && (
<Button variant="ghost" onClick={resetFilters} className="chat-model-filter-clear">
Clear Filters
{t("modelFilters.clearFilters")}
</Button>
)}
</div>
@ -226,10 +228,10 @@ const ModelFilters: React.FC<ModelFiltersProps> = ({
const filtersContent = (
<div className="flex flex-wrap gap-4 items-center">
<div>
<p className="text-sm font-medium mb-2">Search Models:</p>
<p className="text-sm font-medium mb-2">{t("modelFilters.searchModels")}</p>
<input
type="text"
placeholder="Search model names..."
placeholder={t("modelFilters.searchPlaceholder")}
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="border rounded-sm px-3 py-2 w-64 h-10 text-sm"
@ -237,14 +239,14 @@ const ModelFilters: React.FC<ModelFiltersProps> = ({
</div>
{showProviderFilter && (
<div>
<p className="text-sm font-medium mb-2">Provider:</p>
<p className="text-sm font-medium mb-2">{t("modelFilters.provider")}</p>
<select
value={selectedProvider}
onChange={(e) => changeProvider(e.target.value)}
className="border rounded-sm px-3 py-2 text-sm text-muted-foreground w-40 h-10"
>
<option value="" className="text-sm text-muted-foreground">
All Providers
{t("modelFilters.allProviders")}
</option>
{modelHubData &&
getUniqueProviders(modelHubData).map((provider) => (
@ -256,14 +258,14 @@ const ModelFilters: React.FC<ModelFiltersProps> = ({
</div>
)}
<div>
<p className="text-sm font-medium mb-2">Mode:</p>
<p className="text-sm font-medium mb-2">{t("modelFilters.mode")}</p>
<select
value={selectedMode}
onChange={(e) => changeMode(e.target.value)}
className="border rounded-sm px-3 py-2 text-sm text-muted-foreground w-32 h-10"
>
<option value="" className="text-sm text-muted-foreground">
All Modes
{t("modelFilters.allModes")}
</option>
{modelHubData &&
getUniqueModes(modelHubData).map((mode) => (
@ -274,14 +276,14 @@ const ModelFilters: React.FC<ModelFiltersProps> = ({
</select>
</div>
<div>
<p className="text-sm font-medium mb-2">Features:</p>
<p className="text-sm font-medium mb-2">{t("modelFilters.features")}</p>
<select
value={selectedFeature}
onChange={(e) => setSelectedFeature(e.target.value)}
className="border rounded-sm px-3 py-2 text-sm text-muted-foreground w-48 h-10"
>
<option value="" className="text-sm text-muted-foreground">
All Features
{t("modelFilters.allFeatures")}
</option>
{modelHubData &&
getUniqueFeatures(modelHubData).map((feature) => (
@ -299,7 +301,7 @@ const ModelFilters: React.FC<ModelFiltersProps> = ({
onClick={resetFilters}
className="text-info hover:text-info/80 text-sm underline h-10 flex items-center"
>
Clear Filters
{t("modelFilters.clearFilters")}
</button>
</div>
)}

View file

@ -1,3 +1,4 @@
import type { ParseKeys } from "i18next";
import { keepTerminology, type Term } from "@/lib/terminology";
import { insultsCompliancePrompts } from "./insultsCompliancePrompts";
import { financialCompliancePrompts } from "./financialCompliancePrompts";
@ -19,6 +20,7 @@ export interface ComplianceCategory {
name: string;
icon: string;
description: string;
descriptionKey?: ParseKeys;
prompts: CompliancePrompt[];
}
@ -29,6 +31,46 @@ export interface ComplianceFramework {
categories: ComplianceCategory[];
}
export const COMPLIANCE_FRAMEWORK_LABEL_KEYS: Readonly<Record<string, ParseKeys>> = {
"EU AI Act": "compliancePrompts.frameworks.euAiAct",
GDPR: "compliancePrompts.frameworks.gdpr",
"Airline Brand Protection": "compliancePrompts.frameworks.airlineBrandProtection",
};
export const COMPLIANCE_CATEGORY_LABEL_KEYS: Readonly<Record<string, ParseKeys>> = {
"Subliminal or Manipulative Techniques": "compliancePrompts.categories.subliminalTechniques.name",
"Exploiting Vulnerabilities": "compliancePrompts.categories.exploitingVulnerabilities.name",
"Social Scoring & Classification": "compliancePrompts.categories.socialScoring.name",
"Risk Assessment for Predicting Criminality": "compliancePrompts.categories.criminalityRiskAssessment.name",
"Facial Image Scraping for Databases": "compliancePrompts.categories.facialImageScraping.name",
"Emotion Recognition in Workplace & Education": "compliancePrompts.categories.emotionRecognition.name",
"Biometric Categorisation of Sensitive Traits": "compliancePrompts.categories.biometricCategorisation.name",
"Right to Erasure": "compliancePrompts.categories.rightToErasure.name",
"Consent & Lawful Basis": "compliancePrompts.categories.consentLawfulBasis.name",
"Data Minimisation": "compliancePrompts.categories.dataMinimisation.name",
"Automated Decision-Making": "compliancePrompts.categories.automatedDecisionMaking.name",
"Destination Intent": "compliancePrompts.categories.destinationIntent.name",
"Competitor Comparison": "compliancePrompts.categories.competitorComparison.name",
"Ambiguous Entity": "compliancePrompts.categories.ambiguousEntity.name",
};
const CATEGORY_DESCRIPTION_KEYS: Readonly<Record<string, ParseKeys>> = {
"Subliminal or Manipulative Techniques": "compliancePrompts.categories.subliminalTechniques.description",
"Exploiting Vulnerabilities": "compliancePrompts.categories.exploitingVulnerabilities.description",
"Social Scoring & Classification": "compliancePrompts.categories.socialScoring.description",
"Risk Assessment for Predicting Criminality": "compliancePrompts.categories.criminalityRiskAssessment.description",
"Facial Image Scraping for Databases": "compliancePrompts.categories.facialImageScraping.description",
"Emotion Recognition in Workplace & Education": "compliancePrompts.categories.emotionRecognition.description",
"Biometric Categorisation of Sensitive Traits": "compliancePrompts.categories.biometricCategorisation.description",
"Right to Erasure": "compliancePrompts.categories.rightToErasure.description",
"Consent & Lawful Basis": "compliancePrompts.categories.consentLawfulBasis.description",
"Data Minimisation": "compliancePrompts.categories.dataMinimisation.description",
"Automated Decision-Making": "compliancePrompts.categories.automatedDecisionMaking.description",
"Destination Intent": "compliancePrompts.categories.destinationIntent.description",
"Competitor Comparison": "compliancePrompts.categories.competitorComparison.description",
"Ambiguous Entity": "compliancePrompts.categories.ambiguousEntity.description",
};
const createPrompt = (
id: string,
framework: string,
@ -585,6 +627,9 @@ export function getFrameworks(term: Term = keepTerminology): ComplianceFramework
name: prompt.category,
icon: prompt.categoryIcon,
description: prompt.categoryDescription,
descriptionKey: Object.hasOwn(CATEGORY_DESCRIPTION_KEYS, prompt.category)
? CATEGORY_DESCRIPTION_KEYS[prompt.category]
: undefined,
prompts: [],
});
}

View file

@ -8716,6 +8716,9 @@
"sheetDescription": "Conversations and chat workspace navigation"
},
"conversationList": {
"yesterday": "Yesterday",
"lastSevenDays": "Last 7 Days",
"older": "Older",
"conversationTitle": "Conversation title",
"renameConversation": "Rename {{title}}",
"rename": "Rename",
@ -9424,6 +9427,10 @@
},
"compliance": {
"library": {
"customFramework": "Custom",
"customCategory": "Custom Prompts",
"csvFramework": "CSV Upload",
"csvCategory": "Uploaded Prompts",
"customCategoryDescription": "Custom prompts added this session.",
"title": "Test Prompts",
"searchPlaceholder": "Search prompts...",
@ -9537,6 +9544,22 @@
}
},
"compare": {
"endpoints": {
"chatCompletions": {
"selectorLabel": "Model",
"selectorPlaceholder": "Select a model",
"inputPlaceholder": "Send a prompt to compare models",
"loadingMessage": "Gathering responses from all models...",
"validationMessage": "Select a model before sending a message."
},
"a2aAgents": {
"selectorLabel": "Agent",
"selectorPlaceholder": "Select an agent",
"inputPlaceholder": "Send a message to compare agents",
"loadingMessage": "Gathering responses from all agents...",
"validationMessage": "Select an agent before sending a message."
}
},
"selector": {
"loading": "Loading {{noun}}s...",
"empty": "No {{noun}}s available"
@ -9946,6 +9969,20 @@
}
},
"systemOne": {
"presets": {
"bugTriage": "Triage a bug report",
"supportRouting": "Route a support ticket",
"moderation": "Moderate a forum comment",
"leadScoring": "Score a sales lead"
},
"schemas": {
"questionType": "Question type must be choice, noul, or score.",
"questionObject": "Question must be an object."
},
"openAIDecisions": {
"uniqueChoices": "Choice values must be unique, including their string representations.",
"modelRequired": "Model must be a non-empty string."
},
"curlPreview": {
"title": "cURL request",
"fixIssues_one": "Fix {{count}} issue on the Form tab before copying",
@ -10143,12 +10180,129 @@
}
}
},
"compliancePrompts": {
"frameworks": {
"euAiAct": "EU AI Act",
"gdpr": "GDPR",
"airlineBrandProtection": "Airline Brand Protection"
},
"categories": {
"subliminalTechniques": {
"name": "Subliminal or Manipulative Techniques",
"description": "AI that tries to subtly influence decisions or behaviour beyond a person's conscious awareness is prohibited."
},
"exploitingVulnerabilities": {
"name": "Exploiting Vulnerabilities",
"description": "AI must not take advantage of vulnerabilities due to age, disability, economic or social status in ways that distort behaviour and cause harm."
},
"socialScoring": {
"name": "Social Scoring & Classification",
"description": "AI systems that evaluate or score people based on social behaviour or personal traits — leading to unfavourable treatment — are prohibited."
},
"criminalityRiskAssessment": {
"name": "Risk Assessment for Predicting Criminality",
"description": "AI cannot make risk predictions about whether a person might commit a crime based solely on profiling or personality traits."
},
"facialImageScraping": {
"name": "Facial Image Scraping for Databases",
"description": "AI may not create or expand biometric databases by scraping images from the internet or CCTV footage."
},
"emotionRecognition": {
"name": "Emotion Recognition in Workplace & Education",
"description": "AI systems that infer people's emotions in workplaces or schools are forbidden (except medical/safety cases)."
},
"biometricCategorisation": {
"name": "Biometric Categorisation of Sensitive Traits",
"description": "AI must not categorise individuals based on race, religion, sexual orientation, etc., from biometric data."
},
"rightToErasure": {
"name": "Right to Erasure",
"description": "Individuals have the right to request deletion of their personal data when it is no longer necessary for the purpose it was collected."
},
"consentLawfulBasis": {
"name": "Consent & Lawful Basis",
"description": "Processing personal data requires a lawful basis such as consent, which must be freely given, specific, informed, and unambiguous."
},
"dataMinimisation": {
"name": "Data Minimisation",
"description": "Personal data collected must be adequate, relevant, and limited to what is necessary for the stated purpose."
},
"automatedDecisionMaking": {
"name": "Automated Decision-Making",
"description": "Individuals have the right not to be subject to decisions based solely on automated processing that produce legal or similarly significant effects."
},
"destinationIntent": {
"name": "Destination Intent",
"description": "User is asking about a travel destination, not a competitor airline."
},
"competitorComparison": {
"name": "Competitor Comparison",
"description": "Direct comparison between airlines."
},
"ambiguousEntity": {
"name": "Ambiguous Entity",
"description": "Could refer to airline or country."
}
}
},
"chatUi": {
"responseMetrics": {
"labels": {
"totalLatency": "Total Latency",
"in": "In",
"out": "Out",
"reasoning": "Reasoning",
"total": "Total",
"cost": "Cost",
"tool": "Tool"
},
"tooltips": {
"timeToFirstToken": "Time to first token",
"totalLatency": "Total latency",
"promptTokens": "Prompt tokens",
"completionTokens": "Completion tokens",
"reasoningTokens": "Reasoning tokens",
"totalTokens": "Total tokens",
"cost": "Cost",
"toolUsed": "Tool used"
}
},
"mcpEvents": {
"listTools": "List tools",
"toolCall": "Tool call",
"request": "Request",
"approved": "Approved",
"response": "Response"
},
"reasoning": {
"hide": "Hide reasoning",
"show": "Show reasoning"
}
},
"modelFilters": {
"searchLabel": "Search models",
"searchPlaceholder": "Search model names...",
"modelType": "Model type",
"allTypes": "All types",
"modelCapability": "Model capability",
"allCapabilities": "All capabilities",
"clearFilters": "Clear Filters",
"searchModels": "Search Models:",
"provider": "Provider:",
"allProviders": "All Providers",
"mode": "Mode:",
"allModes": "All Modes",
"features": "Features:",
"allFeatures": "All Features"
},
"llmCalls": {
"responsesApi": {
"requestFailed": "Error occurred while generating model response. Please try again. Error: {{error}}"
}
},
"mcpToolArguments": {
"booleanTrue": "True",
"booleanFalse": "False",
"stringSchemaRequired": "Please enter input for this tool",
"enterJsonObject": "Please enter a JSON object",
"enterJsonArray": "Please enter a JSON array",