diff --git a/ui/web/src/i18n/locales/en/config.json b/ui/web/src/i18n/locales/en/config.json index fb510c0c..3d5a4b17 100644 --- a/ui/web/src/i18n/locales/en/config.json +++ b/ui/web/src/i18n/locales/en/config.json @@ -211,6 +211,10 @@ "tools.webSearch": "Web Search", "tools.webFetch": "Web fetch and browser automation", "tools.maxResults": "Max Results", + "tools.braveApiKey": "API key", + "tools.braveApiKeyTip": "Required to use Brave Search. Create a key at https://brave.com/search/api/", + "tools.braveApiKeyPlaceholder": "BSA…", + "tools.braveApiKeyManaged": "A key is already stored on the server and cannot be shown here.", "tools.webFetchPolicy": "Web Fetch Policy", "tools.webFetchPolicyTip": "Controls which URLs the agent can fetch. 'Allowlist' restricts to approved domains only.", "tools.allowedDomains": "Allowed Domains", diff --git a/ui/web/src/i18n/locales/vi/config.json b/ui/web/src/i18n/locales/vi/config.json index 3a4b82e8..2dcab871 100644 --- a/ui/web/src/i18n/locales/vi/config.json +++ b/ui/web/src/i18n/locales/vi/config.json @@ -211,6 +211,10 @@ "tools.webSearch": "Tìm kiếm web", "tools.webFetch": "Tải web và tự động hóa trình duyệt", "tools.maxResults": "Kết quả tối đa", + "tools.braveApiKey": "API Key", + "tools.braveApiKeyTip": "Cần thiết để dùng Brave Search. Tạo khóa tại https://brave.com/search/api/", + "tools.braveApiKeyPlaceholder": "BSA…", + "tools.braveApiKeyManaged": "Khóa đã được lưu trên máy chủ và không thể hiển thị tại đây.", "tools.webFetchPolicy": "Chính sách tải web", "tools.webFetchPolicyTip": "Kiểm soát URL nào agent có thể tải. 'Danh sách cho phép' giới hạn chỉ các domain được duyệt.", "tools.allowedDomains": "Domain được phép", diff --git a/ui/web/src/i18n/locales/zh/config.json b/ui/web/src/i18n/locales/zh/config.json index fb8da047..561b629b 100644 --- a/ui/web/src/i18n/locales/zh/config.json +++ b/ui/web/src/i18n/locales/zh/config.json @@ -211,6 +211,10 @@ "tools.webSearch": "网页搜索", "tools.webFetch": "网页抓取和浏览器自动化", "tools.maxResults": "最大结果数", + "tools.braveApiKey": "API 密钥", + "tools.braveApiKeyTip": "使用 Brave 搜索时需要。可在 https://brave.com/search/api/ 创建密钥", + "tools.braveApiKeyPlaceholder": "BSA…", + "tools.braveApiKeyManaged": "密钥已保存在服务器上,无法在此显示。", "tools.webFetchPolicy": "网页抓取策略", "tools.webFetchPolicyTip": "控制 Agent 可以抓取哪些 URL。\"允许列表\"仅限已批准的域名。", "tools.allowedDomains": "允许的域名", diff --git a/ui/web/src/lib/secret.ts b/ui/web/src/lib/secret.ts new file mode 100644 index 00000000..5d1c4408 --- /dev/null +++ b/ui/web/src/lib/secret.ts @@ -0,0 +1,4 @@ +/** Returns true if the value is a masked secret (contains "***"). */ +export function isSecret(val: unknown): boolean { + return typeof val === "string" && val.includes("***"); +} diff --git a/ui/web/src/pages/config/sections/providers-section.tsx b/ui/web/src/pages/config/sections/providers-section.tsx index 72b0d5b1..372ccc43 100644 --- a/ui/web/src/pages/config/sections/providers-section.tsx +++ b/ui/web/src/pages/config/sections/providers-section.tsx @@ -5,6 +5,7 @@ import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { isSecret } from "@/lib/secret"; type ProviderEntry = { api_key?: string; @@ -28,10 +29,6 @@ const KNOWN_PROVIDERS = [ { key: "ollama_cloud", label: "Ollama Cloud", envKey: "GOCLAW_OLLAMA_CLOUD_API_KEY" }, ]; -function isSecret(val: unknown): boolean { - return typeof val === "string" && val.includes("***"); -} - interface Props { data: ProvidersData | undefined; onSave: (value: ProvidersData) => Promise; diff --git a/ui/web/src/pages/config/sections/tools-web-section.tsx b/ui/web/src/pages/config/sections/tools-web-section.tsx index 0544cd81..d6cba568 100644 --- a/ui/web/src/pages/config/sections/tools-web-section.tsx +++ b/ui/web/src/pages/config/sections/tools-web-section.tsx @@ -22,6 +22,7 @@ import { } from "@/components/ui/select"; import { Separator } from "@/components/ui/separator"; import { InfoLabel } from "@/components/shared/info-label"; +import { isSecret } from "@/lib/secret"; /* eslint-disable @typescript-eslint/no-explicit-any */ type ToolsData = Record; @@ -50,6 +51,18 @@ export function ToolsWebSection({ data, onSave, saving }: Props) { setDirty(true); }; + const handleSave = () => { + const toSave: ToolsData = { ...draft }; + const web = { ...(toSave.web ?? {}) }; + const brave = { ...(web.brave ?? {}) }; + if (isSecret(brave.api_key)) { + delete brave.api_key; + } + web.brave = brave; + toSave.web = web; + onSave(toSave); + }; + if (!data) return null; const web = draft.web ?? {}; @@ -79,6 +92,7 @@ export function ToolsWebSection({ data, onSave, saving }: Props) { updateNested("web", { duckduckgo: { ...ddg, max_results: Number(e.target.value) } })} placeholder="5" @@ -98,12 +112,33 @@ export function ToolsWebSection({ data, onSave, saving }: Props) { updateNested("web", { brave: { ...brave, max_results: Number(e.target.value) } })} placeholder="5" min={1} /> + {brave.enabled && ( +
+ {t("tools.braveApiKey")} + + updateNested("web", { brave: { ...brave, api_key: e.target.value } }) + } + placeholder={t("tools.braveApiKeyPlaceholder")} + autoComplete="off" + /> + {isSecret(brave.api_key) && ( +

{t("tools.braveApiKeyManaged")}

+ )} +
+ )} @@ -181,7 +216,7 @@ export function ToolsWebSection({ data, onSave, saving }: Props) { {dirty && (
-