web-search-settings.tsx 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116
  1. 'use client';
  2. import { useState } from 'react';
  3. import { Label } from '@/components/ui/label';
  4. import { Input } from '@/components/ui/input';
  5. import { useI18n } from '@/lib/hooks/use-i18n';
  6. import { useSettingsStore } from '@/lib/store/settings';
  7. import { WEB_SEARCH_PROVIDERS } from '@/lib/web-search/constants';
  8. import type { WebSearchProviderId } from '@/lib/web-search/types';
  9. import { Eye, EyeOff } from 'lucide-react';
  10. interface WebSearchSettingsProps {
  11. selectedProviderId: WebSearchProviderId;
  12. }
  13. export function WebSearchSettings({ selectedProviderId }: WebSearchSettingsProps) {
  14. const { t } = useI18n();
  15. const [showApiKey, setShowApiKey] = useState(false);
  16. const webSearchProvidersConfig = useSettingsStore((state) => state.webSearchProvidersConfig);
  17. const setWebSearchProviderConfig = useSettingsStore((state) => state.setWebSearchProviderConfig);
  18. const provider = WEB_SEARCH_PROVIDERS[selectedProviderId];
  19. const isServerConfigured = !!webSearchProvidersConfig[selectedProviderId]?.isServerConfigured;
  20. // Reset showApiKey when provider changes (derived state pattern)
  21. const [prevSelectedProviderId, setPrevSelectedProviderId] = useState(selectedProviderId);
  22. if (selectedProviderId !== prevSelectedProviderId) {
  23. setPrevSelectedProviderId(selectedProviderId);
  24. setShowApiKey(false);
  25. }
  26. return (
  27. <div className="space-y-6 max-w-3xl">
  28. {/* Server-configured notice */}
  29. {isServerConfigured && (
  30. <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">
  31. {t('settings.serverConfiguredNotice')}
  32. </div>
  33. )}
  34. {/* API Key + Base URL Configuration */}
  35. {(provider.requiresApiKey || isServerConfigured) && (
  36. <>
  37. <div className="grid grid-cols-2 gap-4">
  38. <div className="space-y-2">
  39. <Label className="text-sm">{t('settings.webSearchApiKey')}</Label>
  40. <div className="relative">
  41. <Input
  42. name={`web-search-api-key-${selectedProviderId}`}
  43. type={showApiKey ? 'text' : 'password'}
  44. autoComplete="new-password"
  45. autoCapitalize="none"
  46. autoCorrect="off"
  47. spellCheck={false}
  48. placeholder={
  49. isServerConfigured ? t('settings.optionalOverride') : t('settings.enterApiKey')
  50. }
  51. value={webSearchProvidersConfig[selectedProviderId]?.apiKey || ''}
  52. onChange={(e) =>
  53. setWebSearchProviderConfig(selectedProviderId, {
  54. apiKey: e.target.value,
  55. })
  56. }
  57. className="font-mono text-sm pr-10"
  58. />
  59. <button
  60. type="button"
  61. onClick={() => setShowApiKey(!showApiKey)}
  62. className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
  63. >
  64. {showApiKey ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
  65. </button>
  66. </div>
  67. <p className="text-xs text-muted-foreground">{t('settings.webSearchApiKeyHint')}</p>
  68. </div>
  69. <div className="space-y-2">
  70. <Label className="text-sm">{t('settings.webSearchBaseUrl')}</Label>
  71. <Input
  72. name={`web-search-base-url-${selectedProviderId}`}
  73. autoComplete="off"
  74. autoCapitalize="none"
  75. autoCorrect="off"
  76. spellCheck={false}
  77. placeholder={provider.defaultBaseUrl || 'https://api.tavily.com'}
  78. value={webSearchProvidersConfig[selectedProviderId]?.baseUrl || ''}
  79. onChange={(e) =>
  80. setWebSearchProviderConfig(selectedProviderId, {
  81. baseUrl: e.target.value,
  82. })
  83. }
  84. className="text-sm"
  85. />
  86. </div>
  87. </div>
  88. {/* Request URL Preview */}
  89. {(() => {
  90. const effectiveBaseUrl =
  91. webSearchProvidersConfig[selectedProviderId]?.baseUrl ||
  92. provider.defaultBaseUrl ||
  93. '';
  94. if (!effectiveBaseUrl) return null;
  95. const fullUrl = effectiveBaseUrl + '/search';
  96. return (
  97. <p className="text-xs text-muted-foreground break-all">
  98. {t('settings.requestUrl')}: {fullUrl}
  99. </p>
  100. );
  101. })()}
  102. </>
  103. )}
  104. </div>
  105. );
  106. }