feat(i18n): complete web UI internationalization for en/vi/zh

Replace ~205 hardcoded English strings across 25 files with i18n keys:
- Shared components: dialog, error-boundary, sticky-save-bar, markdown-renderer, file-viewers, file-browser, file-tree
- Page components: summoning-modal, agent-shares-tab, file-editor, agent-event-cards, channels-section, bindings-section, provider-oauth-section, mcp-form-dialog, channel-instance-form-dialog
- CLI credentials: full i18n for page, form dialog, and hook toasts (new cli-credentials namespace)
- Hook toast messages: use-agents, use-approvals, use-config, use-knowledge-graph, use-memory
- Channel schemas: wizard strings (zalo_personal createLabel/formBanner)
- Dialog scroll padding: extend scroll area into dialog padding for better scrollbar spacing
This commit is contained in:
viettranx committed 2026-03-15 22:39:46 +07:00
1 parent 84b1b07634
commit 473658450f
56 files changed
+618 -162

No files matched your search

@@ -1,6 +1,7 @@
import { Component, type ErrorInfo, type ReactNode } from "react";
import { AlertTriangle, RefreshCw } from "lucide-react";
import { Button } from "@/components/ui/button";
import i18n from "@/i18n";
interface Props {
children: ReactNode;
@@ -34,10 +35,10 @@ export class ErrorBoundary extends Component<Props, State> {
return (
<div className="flex flex-col items-center justify-center gap-3 rounded-lg border bg-card p-8 text-center">
<AlertTriangle className="h-8 w-8 text-muted-foreground" />
<p className="text-sm text-muted-foreground">Something went wrong rendering this section.</p>
<p className="text-sm text-muted-foreground">{i18n.t("common:errorBoundary")}</p>
<Button variant="outline" size="sm" className="gap-1.5" onClick={this.handleRetry}>
<RefreshCw className="h-3.5 w-3.5" />
Retry
{i18n.t("common:retry")}
</Button>
</div>
);
@@ -25,13 +25,14 @@ function FileActions({
size: number;
onDownload?: () => void;
}) {
const { t } = useTranslation("common");
return (
<div className="flex items-center gap-1.5 shrink-0 ml-auto">
<Badge variant={sizeBadgeVariant(size)} className="text-[10px] px-1.5 py-0">
{formatSize(size)}
</Badge>
{onDownload && (
<Button variant="ghost" size="icon" className="h-6 w-6" onClick={onDownload} title="Download">
<Button variant="ghost" size="icon" className="h-6 w-6" onClick={onDownload} title={t("download")}>
<Download className="h-3.5 w-3.5" />
</Button>
)}
+3 -2
View File
@@ -71,6 +71,7 @@ export function TreeItem({
onLoadMore?: (path: string) => void;
showSize?: boolean;
}) {
const { t } = useTranslation("common");
const [expanded, setExpanded] = useState(depth === 0);
const isActive = activePath === node.path;
@@ -87,7 +88,7 @@ export function TreeItem({
<button
type="button"
className="ml-auto shrink-0 opacity-0 group-hover/tree-item:opacity-100 text-destructive hover:text-destructive/80 transition-opacity cursor-pointer p-0.5"
title={`Delete ${node.isDir ? "folder" : "file"}`}
title={node.isDir ? t("deleteFolder") : t("deleteFile")}
onClick={(e) => { e.stopPropagation(); onDelete(node.path, node.isDir); }}
>
<Trash2 className="h-3.5 w-3.5" />
@@ -141,7 +142,7 @@ export function TreeItem({
onClick={() => onLoadMore?.(node.path)}
>
<Loader2 className="h-3 w-3" />
<span>Load more...</span>
<span>{t("loadMore")}</span>
</div>
)}
</div>
@@ -55,6 +55,7 @@ hljs.registerLanguage("lua", lua);
function CodeViewer({ content, language }: { content: string; language: string }) {
const { copied, copy } = useClipboard();
const { t } = useTranslation("common");
const codeRef = useRef<HTMLElement>(null);
const highlighted = useMemo(() => {
@@ -74,7 +75,7 @@ function CodeViewer({ content, language }: { content: string; language: string }
type="button"
onClick={() => copy(content)}
className="cursor-pointer opacity-0 transition-opacity group-hover:opacity-100"
title="Copy"
title={t("copy")}
>
{copied ? <Check className="h-3.5 w-3.5" /> : <Copy className="h-3.5 w-3.5" />}
</button>
@@ -92,6 +93,7 @@ function CodeViewer({ content, language }: { content: string; language: string }
function CsvViewer({ content }: { content: string }) {
const { copied, copy } = useClipboard();
const { t } = useTranslation("common");
const rows = useMemo(() => {
return content.split("\n").filter(Boolean).map((line) => {
const cols: string[] = [];
@@ -115,12 +117,12 @@ function CsvViewer({ content }: { content: string }) {
return (
<div className="group relative rounded-lg border border-border/60 flex flex-col overflow-hidden">
<div className="flex items-center justify-between border-b border-border/40 bg-muted/70 px-3 py-1.5 text-[11px] font-medium tracking-wide text-muted-foreground uppercase shrink-0">
<span>csv ({body.length} rows)</span>
<span>{t("csvRows", { count: body.length })}</span>
<button
type="button"
onClick={() => copy(content)}
className="cursor-pointer opacity-0 transition-opacity group-hover:opacity-100"
title="Copy"
title={t("copy")}
>
{copied ? <Check className="h-3.5 w-3.5" /> : <Copy className="h-3.5 w-3.5" />}
</button>
@@ -154,6 +156,7 @@ function CsvViewer({ content }: { content: string }) {
}
function ImageViewer({ path, fetchBlob }: { path: string; fetchBlob: (path: string) => Promise<Blob> }) {
const { t } = useTranslation("common");
const [src, setSrc] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(false);
@@ -187,7 +190,7 @@ function ImageViewer({ path, fetchBlob }: { path: string; fetchBlob: (path: stri
if (error || !src) {
return (
<div className="flex items-center justify-center py-12 text-sm text-muted-foreground">
Failed to load image
{t("failedToLoadImage")}
</div>
);
}
@@ -1,4 +1,5 @@
import { useState, useCallback } from "react";
import { useTranslation } from "react-i18next";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import rehypeHighlight from "rehype-highlight";
@@ -14,6 +15,7 @@ function CodeBlock({
children?: React.ReactNode;
}) {
const { copied, copy } = useClipboard();
const { t } = useTranslation("common");
const text = String(children).replace(/\n$/, "");
const lang = className?.replace("language-", "") ?? "";
@@ -25,7 +27,7 @@ function CodeBlock({
type="button"
onClick={() => copy(text)}
className="cursor-pointer opacity-0 transition-opacity group-hover:opacity-100"
title="Copy code"
title={t("copyCode")}
>
{copied ? <Check className="h-3.5 w-3.5" /> : <Copy className="h-3.5 w-3.5" />}
</button>
@@ -1,5 +1,6 @@
import { Save, Check, AlertCircle } from "lucide-react";
import { Button } from "@/components/ui/button";
import { useTranslation } from "react-i18next";
interface StickySaveBarProps {
onSave: () => void;
@@ -17,10 +18,14 @@ export function StickySaveBar({
saving,
saved,
error,
label = "Save",
savingLabel = "Saving...",
savedLabel = "Saved",
label,
savingLabel,
savedLabel,
}: StickySaveBarProps) {
const { t } = useTranslation("common");
const resolvedLabel = label ?? t("save");
const resolvedSavingLabel = savingLabel ?? t("saving");
const resolvedSavedLabel = savedLabel ?? t("saved");
return (
<div className="sticky bottom-0 z-10 -mx-3 mt-6 border-t bg-background/80 px-3 py-3 backdrop-blur-sm sm:-mx-4 sm:px-4">
{error && (
@@ -32,12 +37,12 @@ export function StickySaveBar({
<div className="flex items-center justify-end gap-2">
{saved && (
<span className="flex items-center gap-1 text-sm text-success">
<Check className="h-3.5 w-3.5" /> {savedLabel}
<Check className="h-3.5 w-3.5" /> {resolvedSavedLabel}
</span>
)}
<Button onClick={onSave} disabled={saving}>
{!saving && <Save className="h-4 w-4" />}
{saving ? savingLabel : label}
{saving ? resolvedSavingLabel : resolvedLabel}
</Button>
</div>
</div>
+5 -2
View File
@@ -3,6 +3,7 @@
import * as React from "react"
import { XIcon } from "lucide-react"
import { Dialog as DialogPrimitive } from "radix-ui"
import { useTranslation } from "react-i18next"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
@@ -57,6 +58,7 @@ function DialogContent({
showCloseButton?: boolean
overlayTransparent?: boolean
}) {
const { t } = useTranslation("common")
return (
<DialogPortal data-slot="dialog-portal">
<DialogOverlay style={overlayTransparent ? { background: "rgba(0,0,0,0.15)" } : undefined} />
@@ -77,7 +79,7 @@ function DialogContent({
className="cursor-pointer ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
>
<XIcon />
<span className="sr-only">Close</span>
<span className="sr-only">{t("close")}</span>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Content>
@@ -103,6 +105,7 @@ function DialogFooter({
}: React.ComponentProps<"div"> & {
showCloseButton?: boolean
}) {
const { t } = useTranslation("common")
return (
<div
data-slot="dialog-footer"
@@ -115,7 +118,7 @@ function DialogFooter({
{children}
{showCloseButton && (
<DialogPrimitive.Close asChild>
<Button variant="outline">Close</Button>
<Button variant="outline">{t("close")}</Button>
</DialogPrimitive.Close>
)}
</div>
+7
View File
@@ -33,6 +33,7 @@ import enPendingMessages from "./locales/en/pending-messages.json";
import enContacts from "./locales/en/contacts.json";
import enActivity from "./locales/en/activity.json";
import enApiKeys from "./locales/en/api-keys.json";
import enCliCredentials from "./locales/en/cli-credentials.json";
// --- VI namespaces ---
import viCommon from "./locales/vi/common.json";
@@ -66,6 +67,7 @@ import viPendingMessages from "./locales/vi/pending-messages.json";
import viContacts from "./locales/vi/contacts.json";
import viActivity from "./locales/vi/activity.json";
import viApiKeys from "./locales/vi/api-keys.json";
import viCliCredentials from "./locales/vi/cli-credentials.json";
// --- ZH namespaces ---
import zhCommon from "./locales/zh/common.json";
@@ -99,6 +101,7 @@ import zhPendingMessages from "./locales/zh/pending-messages.json";
import zhContacts from "./locales/zh/contacts.json";
import zhActivity from "./locales/zh/activity.json";
import zhApiKeys from "./locales/zh/api-keys.json";
import zhCliCredentials from "./locales/zh/cli-credentials.json";
const STORAGE_KEY = "goclaw:language";
@@ -117,6 +120,7 @@ const ns = [
"channels", "providers", "traces", "events", "delegations",
"usage", "approvals", "nodes", "logs", "tools", "mcp", "tts",
"setup", "memory", "storage", "pending-messages", "contacts", "activity", "api-keys",
"cli-credentials",
] as const;
i18n.use(initReactI18next).init({
@@ -131,6 +135,7 @@ i18n.use(initReactI18next).init({
mcp: enMcp, tts: enTts, setup: enSetup, memory: enMemory, storage: enStorage,
"pending-messages": enPendingMessages,
contacts: enContacts, activity: enActivity, "api-keys": enApiKeys,
"cli-credentials": enCliCredentials,
},
vi: {
common: viCommon, sidebar: viSidebar, topbar: viTopbar, login: viLogin,
@@ -142,6 +147,7 @@ i18n.use(initReactI18next).init({
mcp: viMcp, tts: viTts, setup: viSetup, memory: viMemory, storage: viStorage,
"pending-messages": viPendingMessages,
contacts: viContacts, activity: viActivity, "api-keys": viApiKeys,
"cli-credentials": viCliCredentials,
},
zh: {
common: zhCommon, sidebar: zhSidebar, topbar: zhTopbar, login: zhLogin,
@@ -153,6 +159,7 @@ i18n.use(initReactI18next).init({
mcp: zhMcp, tts: zhTts, setup: zhSetup, memory: zhMemory, storage: zhStorage,
"pending-messages": zhPendingMessages,
contacts: zhContacts, activity: zhActivity, "api-keys": zhApiKeys,
"cli-credentials": zhCliCredentials,
},
},
ns: [...ns],
+20 -3
View File
@@ -172,7 +172,8 @@
"perUser": "per-user",
"emptyFile": "empty",
"estTokens": "Est {{tokens}} tokens",
"insertContact": "Search contact to insert..."
"insertContact": "Search contact to insert...",
"contentPlaceholder": "File content..."
},
"fileEditor": {
"editWithAi": "Edit with AI",
@@ -194,7 +195,11 @@
"user": "User",
"revokeTitle": "Revoke Share",
"revokeDesc": "Revoke access for user \"{{userId}}\"? They will no longer be able to use this agent.",
"revoke": "Revoke"
"revoke": "Revoke",
"role.user": "User",
"role.viewer": "Viewer",
"roleDesc.user": "Can use the agent and chat",
"roleDesc.viewer": "Read-only access"
},
"links": {
"teamsHint": "Agent Links will be removed soon. Please use Agent Teams for multi-agent collaboration — it provides task tracking, shared workspace, and quality control in a simpler setup.",
@@ -263,7 +268,10 @@
"continue": "Continue",
"retry": "Retry",
"retrying": "Retrying...",
"done": "done"
"done": "done",
"fileLabelSOUL": "Soul & Personality",
"fileLabelIDENTITY": "Identity Card",
"fileLabelUSER_PREDEFINED": "Default User Context"
},
"configGroups": {
"core": "Core",
@@ -396,5 +404,14 @@
"formatJson": "Format JSON",
"invalidJson": "Invalid JSON syntax"
}
},
"toast": {
"created": "Agent created",
"createFailed": "Failed to create agent",
"updated": "Agent updated",
"updateFailed": "Failed to update agent",
"deleted": "Agent deleted",
"deleteFailed": "Failed to delete agent",
"unknownError": "Unknown error"
}
}
+10
View File
@@ -19,5 +19,15 @@
"title": "Deny Execution",
"description": "Deny execution request \"{{command}}\" from agent \"{{agentId}}\"?",
"confirmLabel": "Deny"
},
"toast": {
"approved": "Approved",
"approvedAlways": "Command always allowed",
"approvedOnce": "Command approved",
"approveFailed": "Failed to approve",
"denied": "Denied",
"deniedDesc": "Command denied",
"denyFailed": "Failed to deny",
"unknownError": "Unknown error"
}
}
+11 -2
View File
@@ -154,7 +154,7 @@
}
},
"fieldConfig": {
"token": { "label": "Bot Token" },
"token": { "label": "Bot Token", "help": "From @BotFather" },
"bot_token": { "label": "Bot Token" },
"app_token": { "label": "App-Level Token", "help": "App-Level Token with connections:write scope (required for Socket Mode)" },
"user_token": { "label": "User Token (Optional)", "help": "Optional: User OAuth Token for custom bot identity. Leave empty to use default bot identity." },
@@ -190,7 +190,10 @@
"group_allow_from": { "label": "Group Allowed Users", "help": "Separate allowlist for group senders" },
"native_stream": { "label": "Native Streaming (Agents & AI Apps)", "help": "Use Slack's ChatStreamer API for native streaming. Falls back to edit-in-place if unavailable." },
"debounce_delay": { "label": "Debounce Delay (ms)", "help": "Milliseconds to wait before dispatching rapid messages. Set 0 to disable." },
"thread_ttl": { "label": "Thread Participation TTL (hours)", "help": "Hours before bot stops auto-replying in threads it participated in. 0 = always require @mention." }
"thread_ttl": { "label": "Thread Participation TTL (hours)", "help": "Hours before bot stops auto-replying in threads it participated in. 0 = always require @mention." },
"skills": { "label": "Skills Filter", "help": "Limit available skills for this group" },
"tools": { "label": "Tool Allowlist", "help": "Restrict which tools the agent can use in this group" },
"system_prompt": { "label": "System Prompt" }
},
"fieldOptions": {
"block_reply": {
@@ -269,6 +272,12 @@
"no": "No"
}
},
"wizard": {
"zaloPersonal": {
"createLabel": "Create & Authenticate",
"formBanner": "After creating, you'll authenticate via QR code and configure allowed users."
}
},
"toast": {
"created": "Channel created",
"createdDesc": "{{name}} has been added",
@@ -0,0 +1,56 @@
{
"title": "CLI Credentials",
"description": "Manage secure CLI binaries available to agents with injected credentials.",
"addCredential": "Add Credential",
"emptyTitle": "No CLI credentials yet",
"emptyDescription": "Add a CLI credential to allow agents to run authenticated CLI commands.",
"columns": {
"binary": "Binary",
"scope": "Scope",
"timeout": "Timeout"
},
"delete": {
"title": "Delete CLI Credential",
"description": "Remove \"{{name}}\"? Agents that depend on it will lose access.",
"confirm": "Delete"
},
"form": {
"createTitle": "Add CLI Credential",
"editTitle": "Edit CLI Credential",
"preset": "Preset",
"presetPlaceholder": "Select a preset (optional)",
"noPreset": "No preset (manual)",
"presetHint": "Selecting a preset auto-fills fields and shows required env vars.",
"encryptedHint": "Credentials are encrypted and cannot be displayed. Leave environment variables empty to keep existing values, or re-enter to update.",
"envVars": "Environment Variables",
"binaryName": "Binary Name",
"binaryPath": "Binary Path",
"denyArgs": "Deny Args",
"denyVerbose": "Deny Verbose Args",
"timeout": "Timeout (s)",
"tips": "Tips",
"agentId": "Agent ID",
"agentIdHint": "optional — leave blank for global",
"commaSeparated": "comma-separated",
"binaryNameRequired": "Binary name is required.",
"failedToSave": "Failed to save."
},
"placeholders": {
"binaryName": "e.g. gh",
"binaryPath": "/usr/local/bin/gh",
"description": "What this CLI tool does",
"denyArgs": "--admin, --force",
"denyVerbose": "-v, --verbose",
"tips": "Usage tips for the agent",
"agentId": "agent-id"
},
"toast": {
"created": "CLI credential created",
"createdDesc": "\"{{name}}\" added successfully.",
"createFailed": "Failed to create credential",
"updated": "CLI credential updated",
"updateFailed": "Failed to update credential",
"deleted": "CLI credential deleted",
"deleteFailed": "Failed to delete credential"
}
}
+10 -1
View File
@@ -106,5 +106,14 @@
"customTools": "Custom",
"selectOrTypeTools": "Select or type tool names...",
"selectOrTypeSkills": "Select or type skill names...",
"typeAndPressEnter": "Type and press Enter..."
"typeAndPressEnter": "Type and press Enter...",
"copyCode": "Copy code",
"copy": "Copy",
"download": "Download",
"errorBoundary": "Something went wrong rendering this section.",
"csvRows": "csv ({{count}} rows)",
"failedToLoadImage": "Failed to load image",
"deleteFolder": "Delete folder",
"deleteFile": "Delete file",
"loadMore": "Load more..."
}
+13 -1
View File
@@ -291,5 +291,17 @@
"channels.reasoningStream": "Show Reasoning",
"channels.reasoningStreamHint": "Display AI thinking process as a separate message",
"channels.reactionLevel": "Reaction Level",
"channels.connectionMode": "Connection Mode"
"channels.connectionMode": "Connection Mode",
"channels.reactionOff": "Off",
"channels.reactionMinimal": "Minimal",
"channels.reactionFull": "Full",
"channels.connectionWebsocket": "WebSocket",
"channels.connectionWebhook": "Webhook",
"bindings.any": "Any",
"toast": {
"saved": "Config saved",
"saveFailed": "Failed to save config"
}
}
+4 -1
View File
@@ -30,5 +30,8 @@
"detail": {
"copy": "Copy",
"copied": "Copied"
}
},
"toolBadge": { "error": "error", "activated": "activated", "ok": "ok" },
"moreArgs": "...{{count}} more",
"parentLabel": "parent:"
}
+1
View File
@@ -29,6 +29,7 @@
"name": "Name *",
"nameHint": "Lowercase letters, numbers, and hyphens only",
"displayName": "Display Name",
"displayNamePlaceholder": "My MCP Server",
"transport": "Transport *",
"command": "Command *",
"args": "Args (comma-separated)",
+20 -1
View File
@@ -166,6 +166,25 @@
"indexed": "All documents indexed",
"failedCreate": "Failed to create document",
"failedDelete": "Failed to delete document",
"failedIndex": "Failed to index documents"
"failedIndex": "Failed to index documents",
"entitySaved": "Entity saved",
"entitySaveFailed": "Failed to save entity",
"entityDeleted": "Entity deleted",
"entityDeleteFailed": "Failed to delete entity",
"extractionComplete": "Extraction complete",
"extractionFailed": "Extraction failed",
"traversalFailed": "Traversal failed",
"unknownError": "Unknown error",
"docCreated": "Document created",
"docCreateFailed": "Failed to create document",
"docUpdated": "Document updated",
"docUpdateFailed": "Failed to update document",
"docDeleted": "Document deleted",
"docDeleteFailed": "Failed to delete document",
"docIndexed": "Document indexed",
"docIndexFailed": "Failed to index document",
"allIndexed": "All documents indexed",
"allIndexFailed": "Failed to index all",
"searchFailed": "Search failed"
}
}
+2 -1
View File
@@ -108,7 +108,8 @@
"loggedOut": "Logged out",
"loggedOutDesc": "OpenAI OAuth token removed",
"oauthFailed": "OAuth failed",
"exchangeFailed": "Exchange failed"
"exchangeFailed": "Exchange failed",
"logoutFailed": "Logout failed"
},
"cli": {
"description": "Claude CLI uses your local",
+20 -3
View File
@@ -172,7 +172,8 @@
"perUser": "theo người dùng",
"emptyFile": "trống",
"estTokens": "Ước {{tokens}} token",
"insertContact": "Tìm liên hệ để chèn..."
"insertContact": "Tìm liên hệ để chèn...",
"contentPlaceholder": "Nội dung tệp..."
},
"fileEditor": {
"editWithAi": "Chỉnh sửa với AI",
@@ -194,7 +195,11 @@
"user": "Người dùng",
"revokeTitle": "Thu hồi chia sẻ",
"revokeDesc": "Thu hồi quyền truy cập của người dùng \"{{userId}}\"? Họ sẽ không thể sử dụng agent này nữa.",
"revoke": "Thu hồi"
"revoke": "Thu hồi",
"role.user": "Người dùng",
"role.viewer": "Người xem",
"roleDesc.user": "Có thể sử dụng agent và trò chuyện",
"roleDesc.viewer": "Chỉ đọc"
},
"links": {
"teamsHint": "Agent Links sẽ sớm bị gỡ bỏ. Vui lòng sử dụng Agent Teams để cộng tác đa agent — hỗ trợ quản lý nhiệm vụ, workspace chung và kiểm soát chất lượng trong một hệ thống đơn giản hơn.",
@@ -263,7 +268,10 @@
"continue": "Tiếp tục",
"retry": "Thử lại",
"retrying": "Đang thử lại...",
"done": "xong"
"done": "xong",
"fileLabelSOUL": "Linh hồn & Tính cách",
"fileLabelIDENTITY": "Thẻ danh tính",
"fileLabelUSER_PREDEFINED": "Ngữ cảnh người dùng mặc định"
},
"configGroups": {
"core": "Cốt lõi",
@@ -396,5 +404,14 @@
"formatJson": "Định dạng JSON",
"invalidJson": "Cú pháp JSON không hợp lệ"
}
},
"toast": {
"created": "Đã tạo agent",
"createFailed": "Không thể tạo agent",
"updated": "Đã cập nhật agent",
"updateFailed": "Không thể cập nhật agent",
"deleted": "Đã xóa agent",
"deleteFailed": "Không thể xóa agent",
"unknownError": "Lỗi không xác định"
}
}
+10
View File
@@ -19,5 +19,15 @@
"title": "Từ chối thực thi",
"description": "Từ chối yêu cầu thực thi \"{{command}}\" từ agent \"{{agentId}}\"?",
"confirmLabel": "Từ chối"
},
"toast": {
"approved": "Đã phê duyệt",
"approvedAlways": "Lệnh luôn được phép",
"approvedOnce": "Lệnh đã được phê duyệt",
"approveFailed": "Không thể phê duyệt",
"denied": "Đã từ chối",
"deniedDesc": "Lệnh bị từ chối",
"denyFailed": "Không thể từ chối",
"unknownError": "Lỗi không xác định"
}
}
+11 -2
View File
@@ -154,7 +154,7 @@
}
},
"fieldConfig": {
"token": { "label": "Bot Token" },
"token": { "label": "Bot Token", "help": "Từ @BotFather" },
"bot_token": { "label": "Bot Token" },
"app_token": { "label": "Token cấp ứng dụng", "help": "Token cấp ứng dụng với scope connections:write (bắt buộc cho Socket Mode)" },
"user_token": { "label": "Token người dùng (Tùy chọn)", "help": "Tùy chọn: Token OAuth người dùng cho danh tính bot tùy chỉnh. Để trống để dùng danh tính bot mặc định." },
@@ -190,7 +190,10 @@
"group_allow_from": { "label": "Người dùng nhóm được phép", "help": "Danh sách cho phép riêng cho người gửi nhóm" },
"native_stream": { "label": "Stream gốc (Agents & AI Apps)", "help": "Dùng API ChatStreamer của Slack cho stream gốc. Quay về chỉnh sửa tại chỗ nếu không khả dụng." },
"debounce_delay": { "label": "Độ trễ debounce (ms)", "help": "Mili giây chờ trước khi gửi tin nhắn nhanh. Đặt 0 để tắt." },
"thread_ttl": { "label": "TTL tham gia luồng (giờ)", "help": "Số giờ trước khi bot ngừng tự động trả lời trong các luồng đã tham gia. 0 = luôn yêu cầu @đề cập." }
"thread_ttl": { "label": "TTL tham gia luồng (giờ)", "help": "Số giờ trước khi bot ngừng tự động trả lời trong các luồng đã tham gia. 0 = luôn yêu cầu @đề cập." },
"skills": { "label": "Bộ lọc skill", "help": "Giới hạn skill khả dụng cho nhóm này" },
"tools": { "label": "Danh sách công cụ được phép", "help": "Giới hạn công cụ agent có thể dùng trong nhóm này" },
"system_prompt": { "label": "Prompt hệ thống" }
},
"fieldOptions": {
"block_reply": {
@@ -269,6 +272,12 @@
"no": "Không"
}
},
"wizard": {
"zaloPersonal": {
"createLabel": "Tạo & Xác thực",
"formBanner": "Sau khi tạo, bạn sẽ xác thực bằng mã QR và cấu hình người dùng được phép."
}
},
"toast": {
"created": "Đã tạo channel",
"createdDesc": "{{name}} đã được thêm",
@@ -0,0 +1,56 @@
{
"title": "Thông tin CLI",
"description": "Quản lý các CLI binary bảo mật có sẵn cho agent với thông tin xác thực được inject.",
"addCredential": "Thêm thông tin",
"emptyTitle": "Chưa có thông tin CLI nào",
"emptyDescription": "Thêm thông tin CLI để cho phép agent chạy các lệnh CLI đã xác thực.",
"columns": {
"binary": "Binary",
"scope": "Phạm vi",
"timeout": "Thời gian chờ"
},
"delete": {
"title": "Xóa thông tin CLI",
"description": "Xóa \"{{name}}\"? Các agent phụ thuộc vào nó sẽ mất quyền truy cập.",
"confirm": "Xóa"
},
"form": {
"createTitle": "Thêm thông tin CLI",
"editTitle": "Sửa thông tin CLI",
"preset": "Mẫu có sẵn",
"presetPlaceholder": "Chọn mẫu (tùy chọn)",
"noPreset": "Không dùng mẫu (nhập tay)",
"presetHint": "Chọn mẫu sẽ tự động điền các trường và hiển thị biến môi trường cần thiết.",
"encryptedHint": "Thông tin xác thực đã được mã hóa và không thể hiển thị. Để trống biến môi trường để giữ giá trị hiện tại, hoặc nhập lại để cập nhật.",
"envVars": "Biến môi trường",
"binaryName": "Tên Binary",
"binaryPath": "Đường dẫn Binary",
"denyArgs": "Args bị chặn",
"denyVerbose": "Verbose Args bị chặn",
"timeout": "Thời gian chờ (s)",
"tips": "Mẹo sử dụng",
"agentId": "ID Agent",
"agentIdHint": "tùy chọn — để trống cho toàn cục",
"commaSeparated": "phân cách bằng dấu phẩy",
"binaryNameRequired": "Tên binary là bắt buộc.",
"failedToSave": "Không thể lưu."
},
"placeholders": {
"binaryName": "vd: gh",
"binaryPath": "/usr/local/bin/gh",
"description": "CLI tool này dùng để làm gì",
"denyArgs": "--admin, --force",
"denyVerbose": "-v, --verbose",
"tips": "Mẹo sử dụng cho agent",
"agentId": "agent-id"
},
"toast": {
"created": "Đã tạo thông tin CLI",
"createdDesc": "\"{{name}}\" đã được thêm thành công.",
"createFailed": "Không thể tạo thông tin",
"updated": "Đã cập nhật thông tin CLI",
"updateFailed": "Không thể cập nhật thông tin",
"deleted": "Đã xóa thông tin CLI",
"deleteFailed": "Không thể xóa thông tin"
}
}
+10 -1
View File
@@ -106,5 +106,14 @@
"usingGlobalDefaults": "Đang dùng giá trị mặc định toàn cục từ config.json",
"verificationFailed": "Xác minh thất bại",
"view": "Xem",
"yes": "Có"
"yes": "Có",
"copyCode": "Sao chép mã",
"copy": "Sao chép",
"download": "Tải xuống",
"errorBoundary": "Đã xảy ra lỗi khi hiển thị phần này.",
"csvRows": "csv ({{count}} dòng)",
"failedToLoadImage": "Không thể tải hình ảnh",
"deleteFolder": "Xóa thư mục",
"deleteFile": "Xóa tệp",
"loadMore": "Tải thêm..."
}
+13 -1
View File
@@ -291,5 +291,17 @@
"channels.reasoningStream": "Hiện suy luận",
"channels.reasoningStreamHint": "Hiển thị quá trình suy nghĩ của AI thành tin nhắn riêng",
"channels.reactionLevel": "Mức phản ứng",
"channels.connectionMode": "Chế độ kết nối"
"channels.connectionMode": "Chế độ kết nối",
"channels.reactionOff": "Tắt",
"channels.reactionMinimal": "Tối thiểu",
"channels.reactionFull": "Đầy đủ",
"channels.connectionWebsocket": "WebSocket",
"channels.connectionWebhook": "Webhook",
"bindings.any": "Bất kỳ",
"toast": {
"saved": "Đã lưu cấu hình",
"saveFailed": "Không thể lưu cấu hình"
}
}
+4 -1
View File
@@ -30,5 +30,8 @@
"detail": {
"copy": "Sao chép",
"copied": "Đã sao chép"
}
},
"toolBadge": { "error": "lỗi", "activated": "đã kích hoạt", "ok": "ok" },
"moreArgs": "...{{count}} nữa",
"parentLabel": "cha:"
}
+1
View File
@@ -29,6 +29,7 @@
"name": "Tên *",
"nameHint": "Chỉ dùng chữ thường, số và dấu gạch ngang",
"displayName": "Tên hiển thị",
"displayNamePlaceholder": "Máy chủ MCP của tôi",
"transport": "Giao thức *",
"command": "Lệnh *",
"args": "Tham số (phân cách bằng dấu phẩy)",
+20 -1
View File
@@ -166,6 +166,25 @@
"indexed": "Đã lập chỉ mục tất cả tài liệu",
"failedCreate": "Không thể tạo tài liệu",
"failedDelete": "Không thể xóa tài liệu",
"failedIndex": "Không thể lập chỉ mục tài liệu"
"failedIndex": "Không thể lập chỉ mục tài liệu",
"entitySaved": "Đã lưu thực thể",
"entitySaveFailed": "Không thể lưu thực thể",
"entityDeleted": "Đã xóa thực thể",
"entityDeleteFailed": "Không thể xóa thực thể",
"extractionComplete": "Trích xuất hoàn tất",
"extractionFailed": "Trích xuất thất bại",
"traversalFailed": "Duyệt thất bại",
"unknownError": "Lỗi không xác định",
"docCreated": "Đã tạo tài liệu",
"docCreateFailed": "Không thể tạo tài liệu",
"docUpdated": "Đã cập nhật tài liệu",
"docUpdateFailed": "Không thể cập nhật tài liệu",
"docDeleted": "Đã xóa tài liệu",
"docDeleteFailed": "Không thể xóa tài liệu",
"docIndexed": "Đã lập chỉ mục tài liệu",
"docIndexFailed": "Không thể lập chỉ mục tài liệu",
"allIndexed": "Đã lập chỉ mục tất cả tài liệu",
"allIndexFailed": "Không thể lập chỉ mục tất cả",
"searchFailed": "Tìm kiếm thất bại"
}
}
+2 -1
View File
@@ -108,7 +108,8 @@
"loggedOut": "Đã đăng xuất",
"loggedOutDesc": "Đã xóa OAuth token OpenAI",
"oauthFailed": "OAuth thất bại",
"exchangeFailed": "Trao đổi thất bại"
"exchangeFailed": "Trao đổi thất bại",
"logoutFailed": "Đăng xuất thất bại"
},
"cli": {
"description": "Claude CLI sử dụng tệp nhị phân",
+20 -3
View File
@@ -172,7 +172,8 @@
"perUser": "每用户",
"emptyFile": "空",
"estTokens": "估计 {{tokens}} token",
"insertContact": "搜索联系人以插入..."
"insertContact": "搜索联系人以插入...",
"contentPlaceholder": "文件内容..."
},
"fileEditor": {
"editWithAi": "AI 编辑",
@@ -194,7 +195,11 @@
"user": "用户",
"revokeTitle": "撤销共享",
"revokeDesc": "撤销用户「{{userId}}」的访问权限?他们将无法再使用此Agent。",
"revoke": "撤销"
"revoke": "撤销",
"role.user": "用户",
"role.viewer": "查看者",
"roleDesc.user": "可以使用代理和聊天",
"roleDesc.viewer": "只读访问"
},
"links": {
"teamsHint": "Agent Links 即将移除。请使用 Agent Teams 进行多代理协作——它提供任务跟踪、共享工作区和质量控制,且设置更简单。",
@@ -263,7 +268,10 @@
"continue": "继续",
"retry": "重试",
"retrying": "重试中...",
"done": "完成"
"done": "完成",
"fileLabelSOUL": "灵魂与个性",
"fileLabelIDENTITY": "身份卡",
"fileLabelUSER_PREDEFINED": "默认用户上下文"
},
"configGroups": {
"core": "核心",
@@ -396,5 +404,14 @@
"formatJson": "格式化 JSON",
"invalidJson": "JSON 语法无效"
}
},
"toast": {
"created": "代理已创建",
"createFailed": "创建代理失败",
"updated": "代理已更新",
"updateFailed": "更新代理失败",
"deleted": "代理已删除",
"deleteFailed": "删除代理失败",
"unknownError": "未知错误"
}
}
+10
View File
@@ -19,5 +19,15 @@
"title": "拒绝执行",
"description": "拒绝来自Agent 「{{agentId}}」 的 「{{command}}」 执行请求?",
"confirmLabel": "拒绝"
},
"toast": {
"approved": "已批准",
"approvedAlways": "命令始终允许",
"approvedOnce": "命令已批准",
"approveFailed": "批准失败",
"denied": "已拒绝",
"deniedDesc": "命令已拒绝",
"denyFailed": "拒绝失败",
"unknownError": "未知错误"
}
}
+11 -2
View File
@@ -154,7 +154,7 @@
}
},
"fieldConfig": {
"token": { "label": "Bot Token" },
"token": { "label": "Bot Token", "help": "来自 @BotFather" },
"bot_token": { "label": "Bot Token" },
"app_token": { "label": "应用级Token", "help": "具有 connections:write 权限的应用级Token(Socket Mode 必需)" },
"user_token": { "label": "用户Token(可选)", "help": "可选:用于自定义Bot身份的用户OAuth Token。留空使用默认Bot身份。" },
@@ -190,7 +190,10 @@
"group_allow_from": { "label": "群组允许的用户", "help": "群组发送者的单独白名单" },
"native_stream": { "label": "原生流式传输 (Agents & AI Apps)", "help": "使用 Slack 的 ChatStreamer API 进行原生流式传输。不可用时回退到原地编辑。" },
"debounce_delay": { "label": "防抖延迟 (ms)", "help": "发送快速消息前等待的毫秒数。设为 0 禁用。" },
"thread_ttl": { "label": "线程参与 TTL(小时)", "help": "Bot 停止自动回复已参与线程的小时数。0 = 始终需要 @提及。" }
"thread_ttl": { "label": "线程参与 TTL(小时)", "help": "Bot 停止自动回复已参与线程的小时数。0 = 始终需要 @提及。" },
"skills": { "label": "Skill过滤", "help": "限制此群组可用的Skill" },
"tools": { "label": "工具白名单", "help": "限制Agent在此群组中可使用的工具" },
"system_prompt": { "label": "系统提示词" }
},
"fieldOptions": {
"block_reply": {
@@ -269,6 +272,12 @@
"no": "否"
}
},
"wizard": {
"zaloPersonal": {
"createLabel": "创建并认证",
"formBanner": "创建后,您将通过二维码进行认证并配置允许的用户。"
}
},
"toast": {
"created": "Channel已创建",
"createdDesc": "{{name}} 已添加",
@@ -0,0 +1,56 @@
{
"title": "CLI 凭证",
"description": "管理代理可用的安全 CLI 二进制文件及注入的凭证。",
"addCredential": "添加凭证",
"emptyTitle": "暂无 CLI 凭证",
"emptyDescription": "添加 CLI 凭证以允许代理运行已认证的 CLI 命令。",
"columns": {
"binary": "二进制文件",
"scope": "范围",
"timeout": "超时"
},
"delete": {
"title": "删除 CLI 凭证",
"description": "删除 \"{{name}}\"?依赖它的代理将失去访问权限。",
"confirm": "删除"
},
"form": {
"createTitle": "添加 CLI 凭证",
"editTitle": "编辑 CLI 凭证",
"preset": "预设",
"presetPlaceholder": "选择预设(可选)",
"noPreset": "不使用预设(手动)",
"presetHint": "选择预设会自动填充字段并显示所需的环境变量。",
"encryptedHint": "凭证已加密,无法显示。留空环境变量以保留现有值,或重新输入以更新。",
"envVars": "环境变量",
"binaryName": "二进制名称",
"binaryPath": "二进制路径",
"denyArgs": "禁止参数",
"denyVerbose": "禁止详细参数",
"timeout": "超时 (秒)",
"tips": "提示",
"agentId": "代理 ID",
"agentIdHint": "可选 — 留空表示全局",
"commaSeparated": "逗号分隔",
"binaryNameRequired": "二进制名称为必填项。",
"failedToSave": "保存失败。"
},
"placeholders": {
"binaryName": "例如 gh",
"binaryPath": "/usr/local/bin/gh",
"description": "此 CLI 工具的用途",
"denyArgs": "--admin, --force",
"denyVerbose": "-v, --verbose",
"tips": "给代理的使用提示",
"agentId": "agent-id"
},
"toast": {
"created": "CLI 凭证已创建",
"createdDesc": "\"{{name}}\" 已成功添加。",
"createFailed": "创建凭证失败",
"updated": "CLI 凭证已更新",
"updateFailed": "更新凭证失败",
"deleted": "CLI 凭证已删除",
"deleteFailed": "删除凭证失败"
}
}
+10 -1
View File
@@ -106,5 +106,14 @@
"usingGlobalDefaults": "使用 config.json 中的全局默认值",
"verificationFailed": "验证失败",
"view": "查看",
"yes": "是"
"yes": "是",
"copyCode": "复制代码",
"copy": "复制",
"download": "下载",
"errorBoundary": "此部分渲染出错。",
"csvRows": "csv ({{count}} 行)",
"failedToLoadImage": "图片加载失败",
"deleteFolder": "删除文件夹",
"deleteFile": "删除文件",
"loadMore": "加载更多..."
}
+13 -1
View File
@@ -291,5 +291,17 @@
"channels.reasoningStream": "显示推理",
"channels.reasoningStreamHint": "将AI思考过程显示为单独的消息",
"channels.reactionLevel": "反应级别",
"channels.connectionMode": "连接模式"
"channels.connectionMode": "连接模式",
"channels.reactionOff": "关闭",
"channels.reactionMinimal": "最小",
"channels.reactionFull": "完整",
"channels.connectionWebsocket": "WebSocket",
"channels.connectionWebhook": "Webhook",
"bindings.any": "任意",
"toast": {
"saved": "配置已保存",
"saveFailed": "保存配置失败"
}
}
+4 -1
View File
@@ -30,5 +30,8 @@
"detail": {
"copy": "复制",
"copied": "已复制"
}
},
"toolBadge": { "error": "错误", "activated": "已激活", "ok": "成功" },
"moreArgs": "...还有{{count}}个",
"parentLabel": "父级:"
}
+1
View File
@@ -29,6 +29,7 @@
"name": "名称 *",
"nameHint": "仅小写字母、数字和连字符",
"displayName": "显示名称",
"displayNamePlaceholder": "我的 MCP 服务器",
"transport": "传输协议 *",
"command": "命令 *",
"args": "参数(逗号分隔)",
+20 -1
View File
@@ -166,6 +166,25 @@
"indexed": "所有文档已索引",
"failedCreate": "创建文档失败",
"failedDelete": "删除文档失败",
"failedIndex": "索引文档失败"
"failedIndex": "索引文档失败",
"entitySaved": "实体已保存",
"entitySaveFailed": "保存实体失败",
"entityDeleted": "实体已删除",
"entityDeleteFailed": "删除实体失败",
"extractionComplete": "提取完成",
"extractionFailed": "提取失败",
"traversalFailed": "遍历失败",
"unknownError": "未知错误",
"docCreated": "文档已创建",
"docCreateFailed": "创建文档失败",
"docUpdated": "文档已更新",
"docUpdateFailed": "更新文档失败",
"docDeleted": "文档已删除",
"docDeleteFailed": "删除文档失败",
"docIndexed": "文档已索引",
"docIndexFailed": "索引文档失败",
"allIndexed": "所有文档已索引",
"allIndexFailed": "索引所有文档失败",
"searchFailed": "搜索失败"
}
}
+2 -1
View File
@@ -108,7 +108,8 @@
"loggedOut": "已登出",
"loggedOutDesc": "OpenAI OAuth 令牌已移除",
"oauthFailed": "OAuth 失败",
"exchangeFailed": "交换失败"
"exchangeFailed": "交换失败",
"logoutFailed": "登出失败"
},
"cli": {
"description": "Claude CLI 使用本地",
@@ -24,8 +24,8 @@ interface AgentSharesTabProps {
}
const ROLE_OPTIONS = [
{ value: "user", label: "User", description: "Can use the agent and chat" },
{ value: "viewer", label: "Viewer", description: "Read-only access" },
{ value: "user" },
{ value: "viewer" },
] as const;
function roleBadgeVariant(role: string) {
@@ -101,7 +101,7 @@ export function AgentSharesTab({ agentId }: AgentSharesTabProps) {
<SelectContent>
{ROLE_OPTIONS.map((opt) => (
<SelectItem key={opt.value} value={opt.value}>
{opt.label}
{t(`shares.role.${opt.value}`)}
</SelectItem>
))}
</SelectContent>
@@ -115,7 +115,7 @@ export function FileEditor({
}}
readOnly={!canEdit}
className={`flex-1 resize-none font-mono text-sm ${!canEdit ? "opacity-70" : ""}`}
placeholder="File content..."
placeholder={t("files.contentPlaceholder")}
/>
)}
</div>
+7 -6
View File
@@ -5,6 +5,7 @@ import { useAuthStore } from "@/stores/use-auth-store";
import { Methods } from "@/api/protocol";
import { queryKeys } from "@/lib/query-keys";
import { toast } from "@/stores/use-toast-store";
import i18n from "@/i18n";
import type { AgentData } from "@/types/agent";
interface AgentInfoWs {
@@ -65,10 +66,10 @@ export function useAgents() {
try {
const res = await http.post<AgentData>("/v1/agents", data);
await invalidate();
toast.success("Agent created", `${data.display_name || data.agent_key || "Agent"} has been added`);
toast.success(i18n.t("agents:toast.created"), `${data.display_name || data.agent_key || "Agent"} has been added`);
return res;
} catch (err) {
toast.error("Failed to create agent", err instanceof Error ? err.message : "Unknown error");
toast.error(i18n.t("agents:toast.createFailed"), err instanceof Error ? err.message : i18n.t("agents:toast.unknownError"));
throw err;
}
},
@@ -80,9 +81,9 @@ export function useAgents() {
try {
await http.put(`/v1/agents/${id}`, data);
await invalidate();
toast.success("Agent updated", `${data.display_name || data.agent_key || "Agent"} has been updated`);
toast.success(i18n.t("agents:toast.updated"), `${data.display_name || data.agent_key || "Agent"} has been updated`);
} catch (err) {
toast.error("Failed to update agent", err instanceof Error ? err.message : "Unknown error");
toast.error(i18n.t("agents:toast.updateFailed"), err instanceof Error ? err.message : i18n.t("agents:toast.unknownError"));
throw err;
}
},
@@ -94,9 +95,9 @@ export function useAgents() {
try {
await http.delete(`/v1/agents/${id}`);
await invalidate();
toast.success("Agent deleted");
toast.success(i18n.t("agents:toast.deleted"));
} catch (err) {
toast.error("Failed to delete agent", err instanceof Error ? err.message : "Unknown error");
toast.error(i18n.t("agents:toast.deleteFailed"), err instanceof Error ? err.message : i18n.t("agents:toast.unknownError"));
throw err;
}
},
+4 -4
View File
@@ -22,9 +22,9 @@ interface SummoningModalProps {
}
const SUMMONING_FILES = [
{ name: "SOUL.md", label: "Soul & Personality", required: true },
{ name: "IDENTITY.md", label: "Identity Card", required: true },
{ name: "USER_PREDEFINED.md", label: "Default User Context", required: false },
{ name: "SOUL.md", required: true },
{ name: "IDENTITY.md", required: true },
{ name: "USER_PREDEFINED.md", required: false },
];
export function SummoningModal({
@@ -194,7 +194,7 @@ export function SummoningModal({
<span className={`text-sm text-foreground ${done ? "font-medium" : ""}`}>
{file.name}
</span>
<span className="ml-2 text-xs text-muted-foreground">{file.label}</span>
<span className="ml-2 text-xs text-muted-foreground">{t(`summoning.fileLabel${file.name.replace(".md", "")}`)}</span>
</div>
{done && (
<motion.span
@@ -4,6 +4,7 @@ import { useAuthStore } from "@/stores/use-auth-store";
import { useWsEvent } from "@/hooks/use-ws-event";
import { Methods, Events } from "@/api/protocol";
import { toast } from "@/stores/use-toast-store";
import i18n from "@/i18n";
export interface PendingApproval {
id: string;
@@ -48,9 +49,9 @@ export function useApprovals() {
try {
await ws.call(Methods.APPROVALS_APPROVE, { id, always });
setPending((prev) => prev.filter((a) => a.id !== id));
toast.success("Approved", always ? "Command always allowed" : "Command approved");
toast.success(i18n.t("approvals:toast.approved"), always ? i18n.t("approvals:toast.approvedAlways") : i18n.t("approvals:toast.approvedOnce"));
} catch (err) {
toast.error("Failed to approve", err instanceof Error ? err.message : "Unknown error");
toast.error(i18n.t("approvals:toast.approveFailed"), err instanceof Error ? err.message : i18n.t("approvals:toast.unknownError"));
throw err;
}
},
@@ -62,9 +63,9 @@ export function useApprovals() {
try {
await ws.call(Methods.APPROVALS_DENY, { id });
setPending((prev) => prev.filter((a) => a.id !== id));
toast.success("Denied", "Command denied");
toast.success(i18n.t("approvals:toast.denied"), i18n.t("approvals:toast.deniedDesc"));
} catch (err) {
toast.error("Failed to deny", err instanceof Error ? err.message : "Unknown error");
toast.error(i18n.t("approvals:toast.denyFailed"), err instanceof Error ? err.message : i18n.t("approvals:toast.unknownError"));
throw err;
}
},
@@ -327,7 +327,7 @@ export function ChannelInstanceFormDialog({
{/* Wizard info banner (create mode) */}
{hasWizard && wizard?.formBanner && (
<div className="rounded-md border border-blue-200 bg-blue-50 dark:border-blue-900 dark:bg-blue-950 p-3">
<p className="text-sm text-muted-foreground">{wizard.formBanner}</p>
<p className="text-sm text-muted-foreground">{t(wizard.formBanner)}</p>
</div>
)}
@@ -362,7 +362,7 @@ export function ChannelInstanceFormDialog({
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)} disabled={loading}>{t("form.cancel")}</Button>
<Button onClick={handleSubmit} disabled={loading}>
{loading ? t("form.saving") : instance ? t("form.update") : (wizard?.createLabel ?? t("form.create"))}
{loading ? t("form.saving") : instance ? t("form.update") : (wizard?.createLabel ? t(wizard.createLabel) : t("form.create"))}
</Button>
</DialogFooter>
</>
+2 -2
View File
@@ -179,8 +179,8 @@ export interface WizardConfig {
export const wizardConfig: Partial<Record<string, WizardConfig>> = {
zalo_personal: {
steps: ["auth", "config"],
createLabel: "Create & Authenticate",
formBanner: "After creating, you'll authenticate via QR code and configure allowed users.",
createLabel: "wizard.zaloPersonal.createLabel",
formBanner: "wizard.zaloPersonal.formBanner",
excludeConfigFields: ["allow_from"],
},
};
@@ -1,4 +1,5 @@
import { useState, useEffect } from "react";
import { useTranslation } from "react-i18next";
import {
Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle,
} from "@/components/ui/dialog";
@@ -23,6 +24,9 @@ interface Props {
const NONE_PRESET = "__none__";
export function CliCredentialFormDialog({ open, onOpenChange, credential, presets, onSubmit }: Props) {
const { t } = useTranslation("cli-credentials");
const { t: tc } = useTranslation("common");
const [selectedPreset, setSelectedPreset] = useState(NONE_PRESET);
const [binaryName, setBinaryName] = useState("");
const [binaryPath, setBinaryPath] = useState("");
@@ -82,7 +86,7 @@ export function CliCredentialFormDialog({ open, onOpenChange, credential, preset
const handleSubmit = async () => {
if (!binaryName.trim()) {
setError("Binary name is required.");
setError(t("form.binaryNameRequired"));
return;
}
setLoading(true);
@@ -104,7 +108,7 @@ export function CliCredentialFormDialog({ open, onOpenChange, credential, preset
await onSubmit(payload);
onOpenChange(false);
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to save.");
setError(err instanceof Error ? err.message : t("form.failedToSave"));
} finally {
setLoading(false);
}
@@ -112,22 +116,22 @@ export function CliCredentialFormDialog({ open, onOpenChange, credential, preset
return (
<Dialog open={open} onOpenChange={(v) => !loading && onOpenChange(v)}>
<DialogContent className="max-h-[85vh] flex flex-col sm:max-w-lg">
<DialogContent className="max-h-[85vh] flex flex-col sm:max-w-xl">
<DialogHeader>
<DialogTitle>{isEdit ? "Edit CLI Credential" : "Add CLI Credential"}</DialogTitle>
<DialogTitle>{isEdit ? t("form.editTitle") : t("form.createTitle")}</DialogTitle>
</DialogHeader>
<div className="grid gap-4 py-2 px-0.5 -mx-0.5 overflow-y-auto min-h-0">
<div className="grid gap-4 py-2 -mx-4 px-4 sm:-mx-6 sm:px-6 overflow-y-auto min-h-0">
{/* Preset selector — only on create */}
{!isEdit && presetEntries.length > 0 && (
<div className="grid gap-1.5">
<Label>Preset</Label>
<Label>{t("form.preset")}</Label>
<Select value={selectedPreset} onValueChange={applyPreset}>
<SelectTrigger className="text-base md:text-sm">
<SelectValue placeholder="Select a preset (optional)" />
<SelectValue placeholder={t("form.presetPlaceholder")} />
</SelectTrigger>
<SelectContent>
<SelectItem value={NONE_PRESET}>No preset (manual)</SelectItem>
<SelectItem value={NONE_PRESET}>{t("form.noPreset")}</SelectItem>
{presetEntries.map(([k, p]) => (
<SelectItem key={k} value={k}>
{p.binary_name} — {p.description}
@@ -136,27 +140,27 @@ export function CliCredentialFormDialog({ open, onOpenChange, credential, preset
</SelectContent>
</Select>
<p className="text-xs text-muted-foreground">
Selecting a preset auto-fills fields and shows required env vars.
{t("form.presetHint")}
</p>
</div>
)}
{/* Existing credentials indicator (edit mode) */}
{isEdit && (
<p className="text-xs text-muted-foreground rounded-md border border-dashed p-2">
Credentials are encrypted and cannot be displayed. Leave environment variables empty to keep existing values, or re-enter to update.
{t("form.encryptedHint")}
</p>
)}
{/* Env var inputs from preset */}
{activePreset && activePreset.env_vars.length > 0 && (
<div className="grid gap-3 rounded-md border p-3">
<p className="text-sm font-medium">Environment Variables</p>
<p className="text-sm font-medium">{t("form.envVars")}</p>
{activePreset.env_vars.map((ev) => (
<div key={ev.name} className="grid gap-1.5">
<Label htmlFor={`env-${ev.name}`}>
{ev.name}
{ev.optional && <span className="ml-1 text-xs text-muted-foreground">(optional)</span>}
{ev.optional && <span className="ml-1 text-xs text-muted-foreground">({tc("optional")})</span>}
</Label>
<Input
id={`env-${ev.name}`}
@@ -177,34 +181,34 @@ export function CliCredentialFormDialog({ open, onOpenChange, credential, preset
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div className="grid gap-1.5">
<Label htmlFor="cc-name">Binary Name</Label>
<Label htmlFor="cc-name">{t("form.binaryName")}</Label>
<Input
id="cc-name"
value={binaryName}
onChange={(e) => setBinaryName(e.target.value)}
placeholder="e.g. gh"
placeholder={t("placeholders.binaryName")}
className="text-base md:text-sm"
/>
</div>
<div className="grid gap-1.5">
<Label htmlFor="cc-path">Binary Path <span className="text-xs text-muted-foreground">(optional)</span></Label>
<Label htmlFor="cc-path">{t("form.binaryPath")} <span className="text-xs text-muted-foreground">({tc("optional")})</span></Label>
<Input
id="cc-path"
value={binaryPath}
onChange={(e) => setBinaryPath(e.target.value)}
placeholder="/usr/local/bin/gh"
placeholder={t("placeholders.binaryPath")}
className="text-base md:text-sm"
/>
</div>
</div>
<div className="grid gap-1.5">
<Label htmlFor="cc-desc">Description</Label>
<Label htmlFor="cc-desc">{tc("description")}</Label>
<Textarea
id="cc-desc"
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="What this CLI tool does"
placeholder={t("placeholders.description")}
rows={2}
className="text-base md:text-sm"
/>
@@ -212,17 +216,17 @@ export function CliCredentialFormDialog({ open, onOpenChange, credential, preset
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div className="grid gap-1.5">
<Label htmlFor="cc-deny-args">Deny Args <span className="text-xs text-muted-foreground">(comma-separated)</span></Label>
<Label htmlFor="cc-deny-args">{t("form.denyArgs")} <span className="text-xs text-muted-foreground">({t("form.commaSeparated")})</span></Label>
<Input
id="cc-deny-args"
value={denyArgs}
onChange={(e) => setDenyArgs(e.target.value)}
placeholder="--admin, --force"
placeholder={t("placeholders.denyArgs")}
className="text-base md:text-sm"
/>
</div>
<div className="grid gap-1.5">
<Label htmlFor="cc-timeout">Timeout (s)</Label>
<Label htmlFor="cc-timeout">{t("form.timeout")}</Label>
<Input
id="cc-timeout"
type="number"
@@ -235,51 +239,51 @@ export function CliCredentialFormDialog({ open, onOpenChange, credential, preset
</div>
<div className="grid gap-1.5">
<Label htmlFor="cc-deny-verbose">Deny Verbose Args <span className="text-xs text-muted-foreground">(comma-separated)</span></Label>
<Label htmlFor="cc-deny-verbose">{t("form.denyVerbose")} <span className="text-xs text-muted-foreground">({t("form.commaSeparated")})</span></Label>
<Input
id="cc-deny-verbose"
value={denyVerbose}
onChange={(e) => setDenyVerbose(e.target.value)}
placeholder="-v, --verbose"
placeholder={t("placeholders.denyVerbose")}
className="text-base md:text-sm"
/>
</div>
<div className="grid gap-1.5">
<Label htmlFor="cc-tips">Tips</Label>
<Label htmlFor="cc-tips">{t("form.tips")}</Label>
<Textarea
id="cc-tips"
value={tips}
onChange={(e) => setTips(e.target.value)}
placeholder="Usage tips for the agent"
placeholder={t("placeholders.tips")}
rows={2}
className="text-base md:text-sm"
/>
</div>
<div className="grid gap-1.5">
<Label htmlFor="cc-agent">Agent ID <span className="text-xs text-muted-foreground">(optional — leave blank for global)</span></Label>
<Label htmlFor="cc-agent">{t("form.agentId")} <span className="text-xs text-muted-foreground">({t("form.agentIdHint")})</span></Label>
<Input
id="cc-agent"
value={agentId}
onChange={(e) => setAgentId(e.target.value)}
placeholder="agent-id"
placeholder={t("placeholders.agentId")}
className="text-base md:text-sm"
/>
</div>
<div className="flex items-center gap-2">
<Switch id="cc-enabled" checked={enabled} onCheckedChange={setEnabled} />
<Label htmlFor="cc-enabled">Enabled</Label>
<Label htmlFor="cc-enabled">{tc("enabled")}</Label>
</div>
{error && <p className="text-sm text-destructive">{error}</p>}
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)} disabled={loading}>Cancel</Button>
<Button variant="outline" onClick={() => onOpenChange(false)} disabled={loading}>{tc("cancel")}</Button>
<Button onClick={handleSubmit} disabled={loading}>
{loading ? "Saving..." : isEdit ? "Update" : "Create"}
{loading ? tc("saving") : isEdit ? tc("update") : tc("create")}
</Button>
</DialogFooter>
</DialogContent>
@@ -1,4 +1,5 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { KeyRound, Plus, RefreshCw, Pencil, Trash2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
@@ -13,6 +14,9 @@ import { CliCredentialFormDialog } from "./cli-credential-form-dialog";
import type { SecureCLIBinary, CLICredentialInput } from "./hooks/use-cli-credentials";
export function CliCredentialsPage() {
const { t } = useTranslation("cli-credentials");
const { t: tc } = useTranslation("common");
const [formOpen, setFormOpen] = useState(false);
const [editItem, setEditItem] = useState<SecureCLIBinary | null>(null);
const [deleteTarget, setDeleteTarget] = useState<SecureCLIBinary | null>(null);
@@ -58,15 +62,15 @@ export function CliCredentialsPage() {
return (
<div className="p-4 sm:p-6">
<PageHeader
title="CLI Credentials"
description="Manage secure CLI binaries available to agents with injected credentials."
title={t("title")}
description={t("description")}
actions={
<div className="flex gap-2">
<Button size="sm" onClick={openCreate} className="gap-1">
<Plus className="h-3.5 w-3.5" /> Add Credential
<Plus className="h-3.5 w-3.5" /> {t("addCredential")}
</Button>
<Button variant="outline" size="sm" onClick={refresh} disabled={spinning} className="gap-1">
<RefreshCw className={"h-3.5 w-3.5" + (spinning ? " animate-spin" : "")} /> Refresh
<RefreshCw className={"h-3.5 w-3.5" + (spinning ? " animate-spin" : "")} /> {tc("refresh")}
</Button>
</div>
}
@@ -78,20 +82,20 @@ export function CliCredentialsPage() {
) : items.length === 0 ? (
<EmptyState
icon={KeyRound}
title="No CLI credentials yet"
description="Add a CLI credential to allow agents to run authenticated CLI commands."
title={t("emptyTitle")}
description={t("emptyDescription")}
/>
) : (
<div className="overflow-x-auto rounded-md border">
<table className="w-full min-w-[600px] text-sm">
<thead>
<tr className="border-b bg-muted/50">
<th className="px-4 py-3 text-left font-medium">Binary</th>
<th className="px-4 py-3 text-left font-medium">Description</th>
<th className="px-4 py-3 text-left font-medium">Scope</th>
<th className="px-4 py-3 text-left font-medium">Enabled</th>
<th className="px-4 py-3 text-left font-medium">Timeout</th>
<th className="px-4 py-3 text-right font-medium">Actions</th>
<th className="px-4 py-3 text-left font-medium">{t("columns.binary")}</th>
<th className="px-4 py-3 text-left font-medium">{tc("description")}</th>
<th className="px-4 py-3 text-left font-medium">{t("columns.scope")}</th>
<th className="px-4 py-3 text-left font-medium">{tc("enabled")}</th>
<th className="px-4 py-3 text-left font-medium">{t("columns.timeout")}</th>
<th className="px-4 py-3 text-right font-medium">{tc("actions")}</th>
</tr>
</thead>
<tbody>
@@ -113,12 +117,12 @@ export function CliCredentialsPage() {
</td>
<td className="px-4 py-3">
<Badge variant={item.agent_id ? "secondary" : "outline"}>
{item.agent_id ? "Agent" : "Global"}
{item.agent_id ? tc("agent") : tc("global")}
</Badge>
</td>
<td className="px-4 py-3">
<Badge variant={item.enabled ? "default" : "secondary"}>
{item.enabled ? "Enabled" : "Disabled"}
{item.enabled ? tc("enabled") : tc("disabled")}
</Badge>
</td>
<td className="px-4 py-3 text-muted-foreground">{item.timeout_seconds}s</td>
@@ -130,7 +134,7 @@ export function CliCredentialsPage() {
onClick={() => openEdit(item)}
className="gap-1"
>
<Pencil className="h-3.5 w-3.5" /> Edit
<Pencil className="h-3.5 w-3.5" /> {tc("edit")}
</Button>
<Button
variant="ghost"
@@ -161,9 +165,9 @@ export function CliCredentialsPage() {
<ConfirmDialog
open={!!deleteTarget}
onOpenChange={(open) => !open && setDeleteTarget(null)}
title="Delete CLI Credential"
description={`Remove "${deleteTarget?.binary_name}"? Agents that depend on it will lose access.`}
confirmLabel="Delete"
title={t("delete.title")}
description={t("delete.description", { name: deleteTarget?.binary_name })}
confirmLabel={t("delete.confirm")}
variant="destructive"
onConfirm={handleDelete}
loading={deleteLoading}
@@ -2,6 +2,7 @@ import { useCallback } from "react";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useHttp } from "@/hooks/use-ws";
import { toast } from "@/stores/use-toast-store";
import i18n from "@/i18n";
import type { SecureCLIBinary, CLICredentialInput, CLIPreset } from "@/types/cli-credential";
export type { SecureCLIBinary, CLICredentialInput, CLIPreset };
@@ -34,10 +35,13 @@ export function useCliCredentials() {
try {
const res = await http.post<SecureCLIBinary>("/v1/cli-credentials", input);
await invalidate();
toast.success("CLI credential created", `"${input.binary_name}" added successfully.`);
toast.success(
i18n.t("cli-credentials:toast.created"),
i18n.t("cli-credentials:toast.createdDesc", { name: input.binary_name }),
);
return res;
} catch (err) {
toast.error("Failed to create credential", err instanceof Error ? err.message : "");
toast.error(i18n.t("cli-credentials:toast.createFailed"), err instanceof Error ? err.message : "");
throw err;
}
},
@@ -49,9 +53,9 @@ export function useCliCredentials() {
try {
await http.put(`/v1/cli-credentials/${id}`, input);
await invalidate();
toast.success("CLI credential updated");
toast.success(i18n.t("cli-credentials:toast.updated"));
} catch (err) {
toast.error("Failed to update credential", err instanceof Error ? err.message : "");
toast.error(i18n.t("cli-credentials:toast.updateFailed"), err instanceof Error ? err.message : "");
throw err;
}
},
@@ -63,9 +67,9 @@ export function useCliCredentials() {
try {
await http.delete(`/v1/cli-credentials/${id}`);
await invalidate();
toast.success("CLI credential deleted");
toast.success(i18n.t("cli-credentials:toast.deleted"));
} catch (err) {
toast.error("Failed to delete credential", err instanceof Error ? err.message : "");
toast.error(i18n.t("cli-credentials:toast.deleteFailed"), err instanceof Error ? err.message : "");
throw err;
}
},
+7 -6
View File
@@ -5,6 +5,7 @@ import { useAuthStore } from "@/stores/use-auth-store";
import { Methods } from "@/api/protocol";
import { queryKeys } from "@/lib/query-keys";
import { toast } from "@/stores/use-toast-store";
import i18n from "@/i18n";
interface ConfigData {
config: Record<string, unknown>;
@@ -51,11 +52,11 @@ export function useConfig() {
});
hashRef.current = res.hash;
await invalidate();
toast.success("Config saved");
toast.success(i18n.t("config:toast.saved"));
} catch (err) {
const msg = err instanceof Error ? err.message : "Failed to apply config";
const msg = err instanceof Error ? err.message : i18n.t("config:toast.saveFailed");
setError(msg);
toast.error("Failed to save config", msg);
toast.error(i18n.t("config:toast.saveFailed"), msg);
throw err;
} finally {
setSaving(false);
@@ -75,11 +76,11 @@ export function useConfig() {
});
hashRef.current = res.hash;
await invalidate();
toast.success("Config saved");
toast.success(i18n.t("config:toast.saved"));
} catch (err) {
const msg = err instanceof Error ? err.message : "Failed to patch config";
const msg = err instanceof Error ? err.message : i18n.t("config:toast.saveFailed");
setError(msg);
toast.error("Failed to save config", msg);
toast.error(i18n.t("config:toast.saveFailed"), msg);
throw err;
} finally {
setSaving(false);
@@ -29,6 +29,7 @@ const EMPTY_BINDING: Binding = { agentId: "", match: { channel: "", peer: { kind
export function BindingsSection({ data, onSave, saving }: Props) {
const { t } = useTranslation("config");
const { t: tc } = useTranslation("common");
const [draft, setDraft] = useState<Binding[]>(data ?? []);
const [dirty, setDirty] = useState(false);
@@ -97,14 +98,14 @@ export function BindingsSection({ data, onSave, saving }: Props) {
<Input
value={binding.agentId ?? ""}
onChange={(e) => updateBinding(idx, { agentId: e.target.value })}
placeholder="default"
placeholder={tc("default")}
/>
</div>
<div className="grid gap-1">
<Label className="text-xs">{t("bindings.channel")}</Label>
<Select value={binding.match?.channel ?? ""} onValueChange={(v) => updateMatch(idx, { channel: v })}>
<SelectTrigger>
<SelectValue placeholder="Any" />
<SelectValue placeholder={t("bindings.any")} />
</SelectTrigger>
<SelectContent>
<SelectItem value="telegram">Telegram</SelectItem>
@@ -120,7 +121,7 @@ export function BindingsSection({ data, onSave, saving }: Props) {
<Label className="text-xs">{t("bindings.peerKind")}</Label>
<Select value={binding.match?.peer?.kind ?? ""} onValueChange={(v) => updatePeer(idx, { kind: v })}>
<SelectTrigger>
<SelectValue placeholder="Any" />
<SelectValue placeholder={t("bindings.any")} />
</SelectTrigger>
<SelectContent>
<SelectItem value="direct">Direct</SelectItem>
@@ -133,7 +134,7 @@ export function BindingsSection({ data, onSave, saving }: Props) {
<Input
value={binding.match?.peer?.id ?? ""}
onChange={(e) => updatePeer(idx, { id: e.target.value })}
placeholder="Optional"
placeholder={tc("optional")}
/>
</div>
</div>
@@ -245,9 +245,9 @@ export function ChannelsSection({ data, onSave, saving }: Props) {
<Select value={chData.reaction_level ?? "off"} onValueChange={(v) => updateChannel(ch, { reaction_level: v })}>
<SelectTrigger><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="off">Off</SelectItem>
<SelectItem value="minimal">Minimal</SelectItem>
<SelectItem value="full">Full</SelectItem>
<SelectItem value="off">{t("channels.reactionOff")}</SelectItem>
<SelectItem value="minimal">{t("channels.reactionMinimal")}</SelectItem>
<SelectItem value="full">{t("channels.reactionFull")}</SelectItem>
</SelectContent>
</Select>
</div>
@@ -262,8 +262,8 @@ export function ChannelsSection({ data, onSave, saving }: Props) {
<Select value={chData.connection_mode ?? "websocket"} onValueChange={(v) => updateChannel(ch, { connection_mode: v })}>
<SelectTrigger><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="websocket">WebSocket</SelectItem>
<SelectItem value="webhook">Webhook</SelectItem>
<SelectItem value="websocket">{t("channels.connectionWebsocket")}</SelectItem>
<SelectItem value="webhook">{t("channels.connectionWebhook")}</SelectItem>
</SelectContent>
</Select>
</div>
@@ -1,3 +1,4 @@
import { useTranslation } from "react-i18next";
import { Badge } from "@/components/ui/badge";
import type { TeamEventEntry } from "@/stores/use-team-event-store";
import type { EnrichedAgentEventPayload } from "@/types/team-events";
@@ -48,6 +49,7 @@ function RunEventCard({ p, resolveAgent }: { p: EnrichedAgentEventPayload; resol
/** tool.call / tool.result */
function ToolEventCard({ p, resolveAgent }: { p: EnrichedAgentEventPayload; resolveAgent: Props["resolveAgent"] }) {
const { t } = useTranslation("events");
const isResult = p.type === "tool.result";
const toolName = p.payload?.name;
const isError = isResult && p.payload?.is_error;
@@ -72,7 +74,7 @@ function ToolEventCard({ p, resolveAgent }: { p: EnrichedAgentEventPayload; reso
<span className="shrink-0 text-muted-foreground">&rarr;</span>
<span className="truncate font-medium">{agentName}</span>
<Badge variant={isError ? "destructive" : isSkill ? "warning" : "success"} className="shrink-0 text-xs">
{isError ? "error" : isSkill ? "activated" : "ok"}
{isError ? t("toolBadge.error") : isSkill ? t("toolBadge.activated") : t("toolBadge.ok")}
</Badge>
{p.runKind && <RunKindBadge kind={p.runKind} />}
</>
@@ -100,6 +102,7 @@ function ToolEventCard({ p, resolveAgent }: { p: EnrichedAgentEventPayload; reso
/** Render tool arguments as structured key-value pairs */
function ToolArgs({ args }: { args: Record<string, unknown> }) {
const { t } = useTranslation("events");
const entries = Object.entries(args);
if (entries.length === 0) return null;
@@ -118,7 +121,7 @@ function ToolArgs({ args }: { args: Record<string, unknown> }) {
</div>
))}
{remaining > 0 && (
<span className="text-muted-foreground">...{remaining} more</span>
<span className="text-muted-foreground">{t("moreArgs", { count: remaining })}</span>
)}
</div>
);
@@ -134,6 +137,7 @@ function ContextRow({
resolveAgent: Props["resolveAgent"];
showCallId?: boolean;
}) {
const { t } = useTranslation("events");
const hasContext = p.runId || p.delegationId || p.parentAgentId || p.teamTaskId || (showCallId && p.payload?.id);
if (!hasContext) return null;
@@ -142,7 +146,7 @@ function ContextRow({
{p.delegationId && <PillId label="deleg" id={p.delegationId} />}
{p.parentAgentId && (
<span className="inline-flex items-center gap-1 rounded bg-muted px-1.5 py-0.5">
parent: <span className="font-medium text-foreground">{resolveAgent(p.parentAgentId)}</span>
{t("parentLabel")} <span className="font-medium text-foreground">{resolveAgent(p.parentAgentId)}</span>
</span>
)}
{p.teamTaskId && <PillId label="task" id={p.teamTaskId} />}
+1 -1
View File
@@ -170,7 +170,7 @@ export function MCPFormDialog({ open, onOpenChange, server, onSubmit, onTest }:
<div className="grid gap-1.5">
<Label htmlFor="mcp-display">{t("form.displayName")}</Label>
<Input id="mcp-display" value={displayName} onChange={(e) => setDisplayName(e.target.value)} placeholder="My MCP Server" />
<Input id="mcp-display" value={displayName} onChange={(e) => setDisplayName(e.target.value)} placeholder={t("form.displayNamePlaceholder")} />
</div>
<div className="grid gap-1.5">
@@ -3,6 +3,7 @@ import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useHttp } from "@/hooks/use-ws";
import { queryKeys } from "@/lib/query-keys";
import { toast } from "@/stores/use-toast-store";
import i18n from "@/i18n";
import type { KGEntity, KGRelation, KGStats, KGTraversalResult } from "@/types/knowledge-graph";
export interface KGFilters {
@@ -57,9 +58,9 @@ export function useKnowledgeGraph(filters: KGFilters) {
try {
await http.post(`/v1/agents/${filters.agentId}/kg/entities`, entity);
await invalidate();
toast.success("Entity saved", entity.name || "");
toast.success(i18n.t("memory:toast.entitySaved"), entity.name || "");
} catch (err) {
toast.error("Failed to save entity", err instanceof Error ? err.message : "Unknown error");
toast.error(i18n.t("memory:toast.entitySaveFailed"), err instanceof Error ? err.message : i18n.t("memory:toast.unknownError"));
throw err;
}
},
@@ -72,9 +73,9 @@ export function useKnowledgeGraph(filters: KGFilters) {
const qs = userId ? `?user_id=${encodeURIComponent(userId)}` : "";
await http.delete(`/v1/agents/${filters.agentId}/kg/entities/${entityId}${qs}`);
await invalidate();
toast.success("Entity deleted");
toast.success(i18n.t("memory:toast.entityDeleted"));
} catch (err) {
toast.error("Failed to delete entity", err instanceof Error ? err.message : "Unknown error");
toast.error(i18n.t("memory:toast.entityDeleteFailed"), err instanceof Error ? err.message : i18n.t("memory:toast.unknownError"));
throw err;
}
},
@@ -89,10 +90,10 @@ export function useKnowledgeGraph(filters: KGFilters) {
{ text, provider, model, user_id: userId || "" },
);
await invalidate();
toast.success("Extraction complete", `${res.entities} entities, ${res.relations} relations`);
toast.success(i18n.t("memory:toast.extractionComplete"), `${res.entities} entities, ${res.relations} relations`);
return res;
} catch (err) {
toast.error("Extraction failed", err instanceof Error ? err.message : "Unknown error");
toast.error(i18n.t("memory:toast.extractionFailed"), err instanceof Error ? err.message : i18n.t("memory:toast.unknownError"));
throw err;
}
},
@@ -173,7 +174,7 @@ export function useKGTraversal(agentId: string) {
setResults(res ?? []);
return res ?? [];
} catch (err) {
toast.error("Traversal failed", err instanceof Error ? err.message : "Unknown error");
toast.error(i18n.t("memory:toast.traversalFailed"), err instanceof Error ? err.message : i18n.t("memory:toast.unknownError"));
setResults([]);
return [];
} finally {
+12 -11
View File
@@ -3,6 +3,7 @@ import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useHttp } from "@/hooks/use-ws";
import { queryKeys } from "@/lib/query-keys";
import { toast } from "@/stores/use-toast-store";
import i18n from "@/i18n";
import type {
MemoryDocument,
MemoryDocumentDetail,
@@ -67,9 +68,9 @@ export function useMemoryDocuments(filters: MemoryDocFilters) {
user_id: userId || "",
});
await invalidate();
toast.success("Document created", path);
toast.success(i18n.t("memory:toast.docCreated"), path);
} catch (err) {
toast.error("Failed to create document", err instanceof Error ? err.message : "Unknown error");
toast.error(i18n.t("memory:toast.docCreateFailed"), err instanceof Error ? err.message : i18n.t("memory:toast.unknownError"));
throw err;
}
},
@@ -84,9 +85,9 @@ export function useMemoryDocuments(filters: MemoryDocFilters) {
user_id: userId || "",
});
await invalidate();
toast.success("Document updated", path);
toast.success(i18n.t("memory:toast.docUpdated"), path);
} catch (err) {
toast.error("Failed to update document", err instanceof Error ? err.message : "Unknown error");
toast.error(i18n.t("memory:toast.docUpdateFailed"), err instanceof Error ? err.message : i18n.t("memory:toast.unknownError"));
throw err;
}
},
@@ -99,9 +100,9 @@ export function useMemoryDocuments(filters: MemoryDocFilters) {
const qs = userId ? `?user_id=${encodeURIComponent(userId)}` : "";
await http.delete(`/v1/agents/${filters.agentId}/memory/documents/${path}${qs}`);
await invalidate();
toast.success("Document deleted", path);
toast.success(i18n.t("memory:toast.docDeleted"), path);
} catch (err) {
toast.error("Failed to delete document", err instanceof Error ? err.message : "Unknown error");
toast.error(i18n.t("memory:toast.docDeleteFailed"), err instanceof Error ? err.message : i18n.t("memory:toast.unknownError"));
throw err;
}
},
@@ -127,9 +128,9 @@ export function useMemoryDocuments(filters: MemoryDocFilters) {
path,
user_id: userId || "",
});
toast.success("Document indexed", path);
toast.success(i18n.t("memory:toast.docIndexed"), path);
} catch (err) {
toast.error("Failed to index document", err instanceof Error ? err.message : "Unknown error");
toast.error(i18n.t("memory:toast.docIndexFailed"), err instanceof Error ? err.message : i18n.t("memory:toast.unknownError"));
throw err;
}
},
@@ -142,9 +143,9 @@ export function useMemoryDocuments(filters: MemoryDocFilters) {
await http.post(`/v1/agents/${filters.agentId}/memory/index-all`, {
user_id: userId || "",
});
toast.success("All documents indexed");
toast.success(i18n.t("memory:toast.allIndexed"));
} catch (err) {
toast.error("Failed to index all", err instanceof Error ? err.message : "Unknown error");
toast.error(i18n.t("memory:toast.allIndexFailed"), err instanceof Error ? err.message : i18n.t("memory:toast.unknownError"));
throw err;
}
},
@@ -187,7 +188,7 @@ export function useMemorySearch(agentId: string) {
setResults(res.results ?? []);
return res.results ?? [];
} catch (err) {
toast.error("Search failed", err instanceof Error ? err.message : "Unknown error");
toast.error(i18n.t("memory:toast.searchFailed"), err instanceof Error ? err.message : i18n.t("memory:toast.unknownError"));
setResults([]);
return [];
} finally {
@@ -132,7 +132,7 @@ export function OAuthSection({ onSuccess }: { onSuccess: () => void }) {
setStatus({ authenticated: false });
toast.success(i18next.t("providers:oauth.loggedOut"), i18next.t("providers:oauth.loggedOutDesc"));
} catch (err) {
toast.error("Logout failed", err instanceof Error ? err.message : "");
toast.error(i18next.t("providers:oauth.logoutFailed"), err instanceof Error ? err.message : "");
}
};