mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-14 18:18:37 +00:00
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:
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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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..."
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -30,5 +30,8 @@
|
||||
"detail": {
|
||||
"copy": "Copy",
|
||||
"copied": "Copied"
|
||||
}
|
||||
},
|
||||
"toolBadge": { "error": "error", "activated": "activated", "ok": "ok" },
|
||||
"moreArgs": "...{{count}} more",
|
||||
"parentLabel": "parent:"
|
||||
}
|
||||
@@ -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)",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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..."
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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:"
|
||||
}
|
||||
@@ -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)",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "未知错误"
|
||||
}
|
||||
}
|
||||
@@ -19,5 +19,15 @@
|
||||
"title": "拒绝执行",
|
||||
"description": "拒绝来自Agent 「{{agentId}}」 的 「{{command}}」 执行请求?",
|
||||
"confirmLabel": "拒绝"
|
||||
},
|
||||
"toast": {
|
||||
"approved": "已批准",
|
||||
"approvedAlways": "命令始终允许",
|
||||
"approvedOnce": "命令已批准",
|
||||
"approveFailed": "批准失败",
|
||||
"denied": "已拒绝",
|
||||
"deniedDesc": "命令已拒绝",
|
||||
"denyFailed": "拒绝失败",
|
||||
"unknownError": "未知错误"
|
||||
}
|
||||
}
|
||||
@@ -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": "删除凭证失败"
|
||||
}
|
||||
}
|
||||
@@ -106,5 +106,14 @@
|
||||
"usingGlobalDefaults": "使用 config.json 中的全局默认值",
|
||||
"verificationFailed": "验证失败",
|
||||
"view": "查看",
|
||||
"yes": "是"
|
||||
"yes": "是",
|
||||
"copyCode": "复制代码",
|
||||
"copy": "复制",
|
||||
"download": "下载",
|
||||
"errorBoundary": "此部分渲染出错。",
|
||||
"csvRows": "csv ({{count}} 行)",
|
||||
"failedToLoadImage": "图片加载失败",
|
||||
"deleteFolder": "删除文件夹",
|
||||
"deleteFile": "删除文件",
|
||||
"loadMore": "加载更多..."
|
||||
}
|
||||
@@ -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": "保存配置失败"
|
||||
}
|
||||
}
|
||||
@@ -30,5 +30,8 @@
|
||||
"detail": {
|
||||
"copy": "复制",
|
||||
"copied": "已复制"
|
||||
}
|
||||
},
|
||||
"toolBadge": { "error": "错误", "activated": "已激活", "ok": "成功" },
|
||||
"moreArgs": "...还有{{count}}个",
|
||||
"parentLabel": "父级:"
|
||||
}
|
||||
@@ -29,6 +29,7 @@
|
||||
"name": "名称 *",
|
||||
"nameHint": "仅小写字母、数字和连字符",
|
||||
"displayName": "显示名称",
|
||||
"displayNamePlaceholder": "我的 MCP 服务器",
|
||||
"transport": "传输协议 *",
|
||||
"command": "命令 *",
|
||||
"args": "参数(逗号分隔)",
|
||||
|
||||
@@ -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": "搜索失败"
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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">→</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} />}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 : "");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in new issue
Block a user