mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-15 00:17:27 +00:00
feat(web): add Brave Search API key to tools config UI (#627)
* feat(web): add Brave Search API key to tools config UI Expose `web.brave.api_key` in the Web Search settings with password input, strip masked values on save, and show the field only when Brave Search is enabled. Add en/vi/zh strings for label, tooltip, placeholder, and managed key. * refactor(web): extract isSecret to shared utility and fix mobile input font-size DRY: move duplicated isSecret() from providers-section and tools-web-section into lib/secret.ts. Add text-base md:text-sm to number inputs for iOS Safari auto-zoom prevention. --------- Co-authored-by: viettranx <viettranx@gmail.com>
This commit is contained in:
1 parent
6baef0fb9c
commit
040ac31156
6 files changed
+53
-5
No files matched your search
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "允许的域名",
|
||||
|
||||
@@ -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("***");
|
||||
}
|
||||
@@ -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<void>;
|
||||
|
||||
@@ -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<string, any>;
|
||||
@@ -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) {
|
||||
<Label className="text-xs text-muted-foreground">{t("tools.maxResults")}</Label>
|
||||
<Input
|
||||
type="number"
|
||||
className="text-base md:text-sm"
|
||||
value={ddg.max_results ?? ""}
|
||||
onChange={(e) => updateNested("web", { duckduckgo: { ...ddg, max_results: Number(e.target.value) } })}
|
||||
placeholder="5"
|
||||
@@ -98,12 +112,33 @@ export function ToolsWebSection({ data, onSave, saving }: Props) {
|
||||
<Label className="text-xs text-muted-foreground">{t("tools.maxResults")}</Label>
|
||||
<Input
|
||||
type="number"
|
||||
className="text-base md:text-sm"
|
||||
value={brave.max_results ?? ""}
|
||||
onChange={(e) => updateNested("web", { brave: { ...brave, max_results: Number(e.target.value) } })}
|
||||
placeholder="5"
|
||||
min={1}
|
||||
/>
|
||||
</div>
|
||||
{brave.enabled && (
|
||||
<div className="grid gap-1.5">
|
||||
<InfoLabel tip={t("tools.braveApiKeyTip")}>{t("tools.braveApiKey")}</InfoLabel>
|
||||
<Input
|
||||
type="password"
|
||||
className="text-base md:text-sm"
|
||||
value={brave.api_key ?? ""}
|
||||
disabled={isSecret(brave.api_key)}
|
||||
readOnly={isSecret(brave.api_key)}
|
||||
onChange={(e) =>
|
||||
updateNested("web", { brave: { ...brave, api_key: e.target.value } })
|
||||
}
|
||||
placeholder={t("tools.braveApiKeyPlaceholder")}
|
||||
autoComplete="off"
|
||||
/>
|
||||
{isSecret(brave.api_key) && (
|
||||
<p className="text-xs text-muted-foreground">{t("tools.braveApiKeyManaged")}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -181,7 +216,7 @@ export function ToolsWebSection({ data, onSave, saving }: Props) {
|
||||
|
||||
{dirty && (
|
||||
<div className="flex justify-end pt-2">
|
||||
<Button size="sm" onClick={() => onSave(draft)} disabled={saving} className="gap-1.5">
|
||||
<Button size="sm" onClick={handleSave} disabled={saving} className="gap-1.5">
|
||||
<Save className="h-3.5 w-3.5" /> {saving ? t("saving") : t("save")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user