| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224 |
- 'use client';
- import { useState } from 'react';
- import { Label } from '@/components/ui/label';
- import { Input } from '@/components/ui/input';
- import { Badge } from '@/components/ui/badge';
- import { Button } from '@/components/ui/button';
- import { useI18n } from '@/lib/hooks/use-i18n';
- import { useSettingsStore } from '@/lib/store/settings';
- import { PDF_PROVIDERS } from '@/lib/pdf/constants';
- import type { PDFProviderId } from '@/lib/pdf/types';
- import { CheckCircle2, Eye, EyeOff, Loader2, Zap, XCircle } from 'lucide-react';
- import { cn } from '@/lib/utils';
- /**
- * Get display label for feature
- */
- function getFeatureLabel(feature: string, t: (key: string) => string): string {
- const labels: Record<string, string> = {
- text: t('settings.featureText'),
- images: t('settings.featureImages'),
- tables: t('settings.featureTables'),
- formulas: t('settings.featureFormulas'),
- 'layout-analysis': t('settings.featureLayoutAnalysis'),
- metadata: t('settings.featureMetadata'),
- };
- return labels[feature] || feature;
- }
- interface PDFSettingsProps {
- selectedProviderId: PDFProviderId;
- }
- export function PDFSettings({ selectedProviderId }: PDFSettingsProps) {
- const { t } = useI18n();
- const [showApiKey, setShowApiKey] = useState(false);
- const [testStatus, setTestStatus] = useState<'idle' | 'testing' | 'success' | 'error'>('idle');
- const [testMessage, setTestMessage] = useState('');
- const pdfProvidersConfig = useSettingsStore((state) => state.pdfProvidersConfig);
- const setPDFProviderConfig = useSettingsStore((state) => state.setPDFProviderConfig);
- const pdfProvider = PDF_PROVIDERS[selectedProviderId];
- const isServerConfigured = !!pdfProvidersConfig[selectedProviderId]?.isServerConfigured;
- const providerConfig = pdfProvidersConfig[selectedProviderId];
- const hasBaseUrl = !!providerConfig?.baseUrl;
- const needsRemoteConfig = selectedProviderId === 'mineru';
- // Reset state when provider changes
- const [prevSelectedProviderId, setPrevSelectedProviderId] = useState(selectedProviderId);
- if (selectedProviderId !== prevSelectedProviderId) {
- setPrevSelectedProviderId(selectedProviderId);
- setShowApiKey(false);
- setTestStatus('idle');
- setTestMessage('');
- }
- const handleTestConnection = async () => {
- const baseUrl = providerConfig?.baseUrl;
- if (!baseUrl) return;
- setTestStatus('testing');
- setTestMessage('');
- try {
- const response = await fetch('/api/verify-pdf-provider', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({
- providerId: selectedProviderId,
- apiKey: providerConfig?.apiKey || '',
- baseUrl,
- }),
- });
- const data = await response.json();
- if (data.success) {
- setTestStatus('success');
- setTestMessage(t('settings.connectionSuccess'));
- } else {
- setTestStatus('error');
- setTestMessage(`${t('settings.connectionFailed')}: ${data.error}`);
- }
- } catch (err) {
- setTestStatus('error');
- const message = err instanceof Error ? err.message : String(err);
- setTestMessage(`${t('settings.connectionFailed')}: ${message}`);
- }
- };
- 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>
- )}
- {/* Base URL + API Key Configuration (for remote providers like MinerU) */}
- {(needsRemoteConfig || isServerConfigured) && (
- <>
- <div className="grid grid-cols-2 gap-4">
- <div className="space-y-2">
- <Label className="text-sm">{t('settings.pdfBaseUrl')}</Label>
- <div className="flex gap-2">
- <Input
- name={`pdf-base-url-${selectedProviderId}`}
- autoComplete="off"
- autoCapitalize="none"
- autoCorrect="off"
- spellCheck={false}
- placeholder="http://localhost:8080"
- value={providerConfig?.baseUrl || ''}
- onChange={(e) =>
- setPDFProviderConfig(selectedProviderId, { baseUrl: e.target.value })
- }
- className="text-sm"
- />
- <Button
- variant="outline"
- size="sm"
- onClick={handleTestConnection}
- disabled={testStatus === 'testing' || !hasBaseUrl}
- className="gap-1.5 shrink-0"
- >
- {testStatus === 'testing' ? (
- <Loader2 className="h-3.5 w-3.5 animate-spin" />
- ) : (
- <>
- <Zap className="h-3.5 w-3.5" />
- {t('settings.testConnection')}
- </>
- )}
- </Button>
- </div>
- </div>
- <div className="space-y-2">
- <Label className="text-sm">
- {t('settings.pdfApiKey')}
- <span className="text-muted-foreground ml-1 font-normal">
- ({t('settings.optional')})
- </span>
- </Label>
- <div className="relative">
- <Input
- name={`pdf-api-key-${selectedProviderId}`}
- type={showApiKey ? 'text' : 'password'}
- autoComplete="new-password"
- autoCapitalize="none"
- autoCorrect="off"
- spellCheck={false}
- placeholder={
- isServerConfigured ? t('settings.optionalOverride') : t('settings.enterApiKey')
- }
- value={providerConfig?.apiKey || ''}
- onChange={(e) =>
- setPDFProviderConfig(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>
- </div>
- </div>
- {/* Test result message */}
- {testMessage && (
- <div
- className={cn(
- 'rounded-lg p-3 text-sm',
- testStatus === 'success' &&
- 'bg-green-50 text-green-700 border border-green-200 dark:bg-green-950/30 dark:text-green-300 dark:border-green-800',
- testStatus === 'error' &&
- 'bg-red-50 text-red-700 border border-red-200 dark:bg-red-950/30 dark:text-red-300 dark:border-red-800',
- )}
- >
- <div className="flex items-center gap-2">
- {testStatus === 'success' && <CheckCircle2 className="h-4 w-4 shrink-0" />}
- {testStatus === 'error' && <XCircle className="h-4 w-4 shrink-0" />}
- <span className="break-all">{testMessage}</span>
- </div>
- </div>
- )}
- {/* Request URL Preview */}
- {(() => {
- const effectiveBaseUrl = providerConfig?.baseUrl || '';
- if (!effectiveBaseUrl) return null;
- const fullUrl = effectiveBaseUrl + '/file_parse';
- return (
- <p className="text-xs text-muted-foreground break-all">
- {t('settings.requestUrl')}: {fullUrl}
- </p>
- );
- })()}
- </>
- )}
- {/* Features List */}
- <div className="space-y-2">
- <Label className="text-sm">{t('settings.pdfFeatures')}</Label>
- <div className="flex flex-wrap gap-2">
- {pdfProvider.features.map((feature) => (
- <Badge key={feature} variant="secondary" className="font-normal">
- <CheckCircle2 className="h-3 w-3 mr-1" />
- {getFeatureLabel(feature, t)}
- </Badge>
- ))}
- </div>
- </div>
- </div>
- );
- }
|