thu-maic--openmaic
187 行
7.5 KiB
TypeScript
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>
|
|
);
|
|
}
|