pdf-settings.tsx 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  1. 'use client';
  2. import { useState } from 'react';
  3. import { Label } from '@/components/ui/label';
  4. import { Input } from '@/components/ui/input';
  5. import { Badge } from '@/components/ui/badge';
  6. import { Button } from '@/components/ui/button';
  7. import { useI18n } from '@/lib/hooks/use-i18n';
  8. import { useSettingsStore } from '@/lib/store/settings';
  9. import { PDF_PROVIDERS } from '@/lib/pdf/constants';
  10. import type { PDFProviderId } from '@/lib/pdf/types';
  11. import { CheckCircle2, Eye, EyeOff, Loader2, Zap, XCircle } from 'lucide-react';
  12. import { cn } from '@/lib/utils';
  13. /**
  14. * Get display label for feature
  15. */
  16. function getFeatureLabel(feature: string, t: (key: string) => string): string {
  17. const labels: Record<string, string> = {
  18. text: t('settings.featureText'),
  19. images: t('settings.featureImages'),
  20. tables: t('settings.featureTables'),
  21. formulas: t('settings.featureFormulas'),
  22. 'layout-analysis': t('settings.featureLayoutAnalysis'),
  23. metadata: t('settings.featureMetadata'),
  24. };
  25. return labels[feature] || feature;
  26. }
  27. interface PDFSettingsProps {
  28. selectedProviderId: PDFProviderId;
  29. }
  30. export function PDFSettings({ selectedProviderId }: PDFSettingsProps) {
  31. const { t } = useI18n();
  32. const [showApiKey, setShowApiKey] = useState(false);
  33. const [testStatus, setTestStatus] = useState<'idle' | 'testing' | 'success' | 'error'>('idle');
  34. const [testMessage, setTestMessage] = useState('');
  35. const pdfProvidersConfig = useSettingsStore((state) => state.pdfProvidersConfig);
  36. const setPDFProviderConfig = useSettingsStore((state) => state.setPDFProviderConfig);
  37. const pdfProvider = PDF_PROVIDERS[selectedProviderId];
  38. const isServerConfigured = !!pdfProvidersConfig[selectedProviderId]?.isServerConfigured;
  39. const providerConfig = pdfProvidersConfig[selectedProviderId];
  40. const hasBaseUrl = !!providerConfig?.baseUrl;
  41. const needsRemoteConfig = selectedProviderId === 'mineru';
  42. // Reset state when provider changes
  43. const [prevSelectedProviderId, setPrevSelectedProviderId] = useState(selectedProviderId);
  44. if (selectedProviderId !== prevSelectedProviderId) {
  45. setPrevSelectedProviderId(selectedProviderId);
  46. setShowApiKey(false);
  47. setTestStatus('idle');
  48. setTestMessage('');
  49. }
  50. const handleTestConnection = async () => {
  51. const baseUrl = providerConfig?.baseUrl;
  52. if (!baseUrl) return;
  53. setTestStatus('testing');
  54. setTestMessage('');
  55. try {
  56. const response = await fetch('/api/verify-pdf-provider', {
  57. method: 'POST',
  58. headers: { 'Content-Type': 'application/json' },
  59. body: JSON.stringify({
  60. providerId: selectedProviderId,
  61. apiKey: providerConfig?.apiKey || '',
  62. baseUrl,
  63. }),
  64. });
  65. const data = await response.json();
  66. if (data.success) {
  67. setTestStatus('success');
  68. setTestMessage(t('settings.connectionSuccess'));
  69. } else {
  70. setTestStatus('error');
  71. setTestMessage(`${t('settings.connectionFailed')}: ${data.error}`);
  72. }
  73. } catch (err) {
  74. setTestStatus('error');
  75. const message = err instanceof Error ? err.message : String(err);
  76. setTestMessage(`${t('settings.connectionFailed')}: ${message}`);
  77. }
  78. };
  79. return (
  80. <div className="space-y-6 max-w-3xl">
  81. {/* Server-configured notice */}
  82. {isServerConfigured && (
  83. <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">
  84. {t('settings.serverConfiguredNotice')}
  85. </div>
  86. )}
  87. {/* Base URL + API Key Configuration (for remote providers like MinerU) */}
  88. {(needsRemoteConfig || isServerConfigured) && (
  89. <>
  90. <div className="grid grid-cols-2 gap-4">
  91. <div className="space-y-2">
  92. <Label className="text-sm">{t('settings.pdfBaseUrl')}</Label>
  93. <div className="flex gap-2">
  94. <Input
  95. name={`pdf-base-url-${selectedProviderId}`}
  96. autoComplete="off"
  97. autoCapitalize="none"
  98. autoCorrect="off"
  99. spellCheck={false}
  100. placeholder="http://localhost:8080"
  101. value={providerConfig?.baseUrl || ''}
  102. onChange={(e) =>
  103. setPDFProviderConfig(selectedProviderId, { baseUrl: e.target.value })
  104. }
  105. className="text-sm"
  106. />
  107. <Button
  108. variant="outline"
  109. size="sm"
  110. onClick={handleTestConnection}
  111. disabled={testStatus === 'testing' || !hasBaseUrl}
  112. className="gap-1.5 shrink-0"
  113. >
  114. {testStatus === 'testing' ? (
  115. <Loader2 className="h-3.5 w-3.5 animate-spin" />
  116. ) : (
  117. <>
  118. <Zap className="h-3.5 w-3.5" />
  119. {t('settings.testConnection')}
  120. </>
  121. )}
  122. </Button>
  123. </div>
  124. </div>
  125. <div className="space-y-2">
  126. <Label className="text-sm">
  127. {t('settings.pdfApiKey')}
  128. <span className="text-muted-foreground ml-1 font-normal">
  129. ({t('settings.optional')})
  130. </span>
  131. </Label>
  132. <div className="relative">
  133. <Input
  134. name={`pdf-api-key-${selectedProviderId}`}
  135. type={showApiKey ? 'text' : 'password'}
  136. autoComplete="new-password"
  137. autoCapitalize="none"
  138. autoCorrect="off"
  139. spellCheck={false}
  140. placeholder={
  141. isServerConfigured ? t('settings.optionalOverride') : t('settings.enterApiKey')
  142. }
  143. value={providerConfig?.apiKey || ''}
  144. onChange={(e) =>
  145. setPDFProviderConfig(selectedProviderId, {
  146. apiKey: e.target.value,
  147. })
  148. }
  149. className="font-mono text-sm pr-10"
  150. />
  151. <button
  152. type="button"
  153. onClick={() => setShowApiKey(!showApiKey)}
  154. className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
  155. >
  156. {showApiKey ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
  157. </button>
  158. </div>
  159. </div>
  160. </div>
  161. {/* Test result message */}
  162. {testMessage && (
  163. <div
  164. className={cn(
  165. 'rounded-lg p-3 text-sm',
  166. testStatus === 'success' &&
  167. 'bg-green-50 text-green-700 border border-green-200 dark:bg-green-950/30 dark:text-green-300 dark:border-green-800',
  168. testStatus === 'error' &&
  169. 'bg-red-50 text-red-700 border border-red-200 dark:bg-red-950/30 dark:text-red-300 dark:border-red-800',
  170. )}
  171. >
  172. <div className="flex items-center gap-2">
  173. {testStatus === 'success' && <CheckCircle2 className="h-4 w-4 shrink-0" />}
  174. {testStatus === 'error' && <XCircle className="h-4 w-4 shrink-0" />}
  175. <span className="break-all">{testMessage}</span>
  176. </div>
  177. </div>
  178. )}
  179. {/* Request URL Preview */}
  180. {(() => {
  181. const effectiveBaseUrl = providerConfig?.baseUrl || '';
  182. if (!effectiveBaseUrl) return null;
  183. const fullUrl = effectiveBaseUrl + '/file_parse';
  184. return (
  185. <p className="text-xs text-muted-foreground break-all">
  186. {t('settings.requestUrl')}: {fullUrl}
  187. </p>
  188. );
  189. })()}
  190. </>
  191. )}
  192. {/* Features List */}
  193. <div className="space-y-2">
  194. <Label className="text-sm">{t('settings.pdfFeatures')}</Label>
  195. <div className="flex flex-wrap gap-2">
  196. {pdfProvider.features.map((feature) => (
  197. <Badge key={feature} variant="secondary" className="font-normal">
  198. <CheckCircle2 className="h-3 w-3 mr-1" />
  199. {getFeatureLabel(feature, t)}
  200. </Badge>
  201. ))}
  202. </div>
  203. </div>
  204. </div>
  205. );
  206. }