项目文件夹

文件
wehub-resource-sync c48612c494
CI / E2E Tests (push) Has been cancelled
CI / Lint, Typecheck & Unit Tests (push) Has been cancelled
Docs Build / Build docs site (push) Has been cancelled
Publish @openmaic packages / Build, validate & publish (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:03:23 +08:00

187 行
7.5 KiB
TypeScript

'use client';
import { useState } from 'react';
import { Label } from '@/components/ui/label';
import { Input } from '@/components/ui/input';
import { Switch } from '@/components/ui/switch';
import { useI18n } from '@/lib/hooks/use-i18n';
import { useSettingsStore } from '@/lib/store/settings';
import { BAIDU_SUB_SOURCES, WEB_SEARCH_PROVIDERS } from '@/lib/web-search/constants';
import type { BaiduSubSources, WebSearchProviderId } from '@/lib/web-search/types';
import { ExternalLink, Eye, EyeOff } from 'lucide-react';
interface WebSearchSettingsProps {
selectedProviderId: WebSearchProviderId;
}
export function WebSearchSettings({ selectedProviderId }: WebSearchSettingsProps) {
const { t } = useI18n();
const [showApiKey, setShowApiKey] = useState(false);
const webSearchProvidersConfig = useSettingsStore((state) => state.webSearchProvidersConfig);
const setWebSearchProviderConfig = useSettingsStore((state) => state.setWebSearchProviderConfig);
const baiduSubSources = useSettingsStore((state) => state.baiduSubSources);
const setBaiduSubSources = useSettingsStore((state) => state.setBaiduSubSources);
const provider = WEB_SEARCH_PROVIDERS[selectedProviderId];
const isServerConfigured = !!webSearchProvidersConfig[selectedProviderId]?.isServerConfigured;
// Managed providers are admin-owned: hide the key/base-URL override inputs.
const showCredentialFields = !isServerConfigured;
const buildRequestUrl = (baseUrl: string) => {
const trimmed = baseUrl.replace(/\/$/, '');
if (!provider.endpointPath) return trimmed;
if (trimmed.endsWith(provider.endpointPath)) return trimmed;
return `${trimmed}${provider.endpointPath}`;
};
// Reset showApiKey when provider changes (derived state pattern)
const [prevSelectedProviderId, setPrevSelectedProviderId] = useState(selectedProviderId);
if (selectedProviderId !== prevSelectedProviderId) {
setPrevSelectedProviderId(selectedProviderId);
setShowApiKey(false);
}
return (
<div className="space-y-6 max-w-3xl">
{/* Server-configured notice */}
{isServerConfigured && (
<div className="rounded-lg border border-blue-200 bg-blue-50 dark:border-blue-800 dark:bg-blue-950/30 p-3 text-sm text-blue-700 dark:text-blue-300">
{t('settings.serverConfiguredNotice')}
</div>
)}
{!provider.requiresApiKey && !isServerConfigured && (
<div className="rounded-lg border border-amber-200 bg-amber-50 dark:border-amber-800 dark:bg-amber-950/30 p-3 text-sm text-amber-700 dark:text-amber-300">
{t('settings.webSearchApiKeyOptional')}
</div>
)}
{/* API Key + Base URL Configuration */}
{showCredentialFields && (
<>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label className="text-sm">{t('settings.webSearchApiKey')}</Label>
<div className="relative">
<Input
name={`web-search-api-key-${selectedProviderId}`}
type={showApiKey ? 'text' : 'password'}
autoComplete="new-password"
autoCapitalize="none"
autoCorrect="off"
spellCheck={false}
placeholder={
!provider.requiresApiKey
? t('settings.optionalOverride')
: t('settings.enterApiKey')
}
value={webSearchProvidersConfig[selectedProviderId]?.apiKey || ''}
onChange={(e) =>
setWebSearchProviderConfig(selectedProviderId, {
apiKey: e.target.value,
})
}
className="font-mono text-sm pr-10"
/>
<button
type="button"
onClick={() => setShowApiKey(!showApiKey)}
className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
>
{showApiKey ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</button>
</div>
<p className="text-xs text-muted-foreground">{t('settings.webSearchApiKeyHint')}</p>
</div>
<div className="space-y-2">
<Label className="text-sm">{t('settings.webSearchBaseUrl')}</Label>
<Input
name={`web-search-base-url-${selectedProviderId}`}
autoComplete="off"
autoCapitalize="none"
autoCorrect="off"
spellCheck={false}
placeholder={provider.defaultBaseUrl || ''}
value={webSearchProvidersConfig[selectedProviderId]?.baseUrl || ''}
onChange={(e) =>
setWebSearchProviderConfig(selectedProviderId, {
baseUrl: e.target.value,
})
}
className="text-sm"
/>
</div>
</div>
{/* Request URL Preview */}
{(() => {
const effectiveBaseUrl =
webSearchProvidersConfig[selectedProviderId]?.baseUrl ||
provider.defaultBaseUrl ||
'';
if (!effectiveBaseUrl) return null;
const fullUrl = buildRequestUrl(effectiveBaseUrl);
return (
<p className="text-xs text-muted-foreground break-all">
{t('settings.requestUrl')}: {fullUrl}
</p>
);
})()}
</>
)}
{selectedProviderId === 'baidu' && (
<div className="space-y-3">
<div className="space-y-1">
<Label className="text-sm">{t('settings.baiduSubSources')}</Label>
<p className="text-xs text-muted-foreground">
{t('settings.baiduSubSourcesDescription')}
</p>
</div>
<div className="space-y-2">
{(
Object.entries(BAIDU_SUB_SOURCES) as Array<
[keyof BaiduSubSources, (typeof BAIDU_SUB_SOURCES)[keyof typeof BAIDU_SUB_SOURCES]]
>
).map(([key, meta]) => {
const enabled = baiduSubSources[key];
return (
<div
key={key}
className="flex items-center justify-between gap-4 rounded-md border px-3 py-2"
>
<div className="min-w-0 space-y-0.5">
<div className="text-sm font-medium">{t(meta.labelKey)}</div>
<div className="text-xs text-muted-foreground">
{t(meta.descriptionKey)}
{meta.docsUrl && (
<a
href={meta.docsUrl}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-0.5 ml-1.5 text-blue-500 hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-300 hover:underline"
>
{t('settings.viewDocs')}
<ExternalLink className="h-3 w-3" />
</a>
)}
</div>
</div>
<Switch
checked={enabled}
onCheckedChange={(checked) => setBaiduSubSources({ [key]: checked })}
aria-label={t(meta.labelKey)}
/>
</div>
);
})}
</div>
</div>
)}
</div>
);
}