'use client'; import { useState, useRef, useMemo } from 'react'; import { Bot, Check, ChevronLeft, Globe, Paperclip, FileText, X, Globe2 } from 'lucide-react'; import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; import { cn } from '@/lib/utils'; 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 { WEB_SEARCH_PROVIDERS } from '@/lib/web-search/constants'; import type { WebSearchProviderId } from '@/lib/web-search/types'; import type { ProviderId } from '@/lib/ai/providers'; import { MONO_LOGO_PROVIDERS } from '@/lib/ai/providers'; import type { SettingsSection } from '@/lib/types/settings'; import { MediaPopover } from '@/components/generation/media-popover'; // ─── Constants ─────────────────────────────────────────────── const MAX_PDF_SIZE_MB = 50; const MAX_PDF_SIZE_BYTES = MAX_PDF_SIZE_MB * 1024 * 1024; // ─── Types ─────────────────────────────────────────────────── export interface GenerationToolbarProps { language: 'zh-CN' | 'en-US'; onLanguageChange: (lang: 'zh-CN' | 'en-US') => void; webSearch: boolean; onWebSearchChange: (v: boolean) => void; onSettingsOpen: (section?: SettingsSection) => void; // PDF pdfFile: File | null; onPdfFileChange: (file: File | null) => void; onPdfError: (error: string | null) => void; } // ─── Component ─────────────────────────────────────────────── export function GenerationToolbar({ language, onLanguageChange, webSearch, onWebSearchChange, onSettingsOpen, pdfFile, onPdfFileChange, onPdfError, }: GenerationToolbarProps) { const { t } = useI18n(); const currentProviderId = useSettingsStore((s) => s.providerId); const currentModelId = useSettingsStore((s) => s.modelId); const providersConfig = useSettingsStore((s) => s.providersConfig); const setModel = useSettingsStore((s) => s.setModel); const pdfProviderId = useSettingsStore((s) => s.pdfProviderId); const pdfProvidersConfig = useSettingsStore((s) => s.pdfProvidersConfig); const setPDFProvider = useSettingsStore((s) => s.setPDFProvider); const webSearchProviderId = useSettingsStore((s) => s.webSearchProviderId); const webSearchProvidersConfig = useSettingsStore((s) => s.webSearchProvidersConfig); const setWebSearchProvider = useSettingsStore((s) => s.setWebSearchProvider); const fileInputRef = useRef(null); const [isDragging, setIsDragging] = useState(false); // Check if the selected web search provider has a valid config (API key or server-configured) const webSearchProvider = WEB_SEARCH_PROVIDERS[webSearchProviderId]; const webSearchConfig = webSearchProvidersConfig[webSearchProviderId]; const webSearchAvailable = webSearchProvider ? !webSearchProvider.requiresApiKey || !!webSearchConfig?.apiKey || !!webSearchConfig?.isServerConfigured : false; // Configured LLM providers (only those with valid credentials + models + endpoint) const configuredProviders = providersConfig ? Object.entries(providersConfig) .filter( ([, config]) => (!config.requiresApiKey || config.apiKey || config.isServerConfigured) && config.models.length >= 1 && (config.baseUrl || config.defaultBaseUrl || config.serverBaseUrl), ) .map(([id, config]) => ({ id: id as ProviderId, name: config.name, icon: config.icon, isServerConfigured: config.isServerConfigured, models: config.isServerConfigured && !config.apiKey && config.serverModels?.length ? config.models.filter((m) => new Set(config.serverModels).has(m.id)) : config.models, })) : []; const currentProviderConfig = providersConfig?.[currentProviderId]; // PDF handler const handleFileSelect = (file: File) => { if (file.type !== 'application/pdf') return; if (file.size > MAX_PDF_SIZE_BYTES) { onPdfError(t('upload.fileTooLarge')); return; } onPdfError(null); onPdfFileChange(file); }; // ─── Pill button helper ───────────────────────────── const pillCls = 'inline-flex items-center gap-1.5 rounded-full px-2.5 py-1 text-xs font-medium transition-all cursor-pointer select-none whitespace-nowrap border'; const pillMuted = `${pillCls} border-border/50 text-muted-foreground/70 hover:text-foreground hover:bg-muted/60`; const pillActive = `${pillCls} border-violet-200/60 dark:border-violet-700/50 bg-violet-100 dark:bg-violet-900/30 text-violet-700 dark:text-violet-300`; return (
{/* ── Model selector ── */} {configuredProviders.length > 0 ? ( ) : ( {t('toolbar.configureProviderHint')} )} {/* ── Separator ── */}
{/* ── PDF (parser + upload) combined Popover ── */} {pdfFile ? ( ) : ( )} {/* Parser selector */}
{t('toolbar.pdfParser')}
{/* Upload area / file info */}
{ const f = e.target.files?.[0]; if (f) handleFileSelect(f); e.target.value = ''; }} /> {pdfFile ? (

{pdfFile.name}

{(pdfFile.size / 1024 / 1024).toFixed(2)} MB

) : (
fileInputRef.current?.click()} onDragOver={(e) => { e.preventDefault(); setIsDragging(true); }} onDragLeave={() => setIsDragging(false)} onDrop={(e) => { e.preventDefault(); setIsDragging(false); const f = e.dataTransfer.files?.[0]; if (f) handleFileSelect(f); }} >

{t('toolbar.pdfUpload')}

{t('upload.pdfSizeLimit')}

)}
{/* ── Web Search ── */} {webSearchAvailable ? ( {/* Toggle */} {/* Provider selector */}
{t('toolbar.webSearchProvider')}
) : ( {t('toolbar.webSearchNoProvider')} )} {/* ── Language pill ── */} {t('toolbar.languageHint')} {/* ── Separator ── */}
{/* ── Media popover ── */}
); } // ─── ModelSelectorPopover (two-level: provider → model) ───── interface ConfiguredProvider { id: ProviderId; name: string; icon?: string; isServerConfigured?: boolean; models: { id: string; name: string }[]; } function ModelSelectorPopover({ configuredProviders, currentProviderId, currentModelId, currentProviderConfig, setModel, t, }: { configuredProviders: ConfiguredProvider[]; currentProviderId: ProviderId; currentModelId: string; currentProviderConfig: { name: string; icon?: string } | undefined; setModel: (providerId: ProviderId, modelId: string) => void; t: (key: string) => string; }) { const [popoverOpen, setPopoverOpen] = useState(false); // null = provider list, ProviderId = model list for that provider const [drillProvider, setDrillProvider] = useState(null); const activeProvider = useMemo( () => configuredProviders.find((p) => p.id === drillProvider), [configuredProviders, drillProvider], ); return ( { setPopoverOpen(open); if (open) setDrillProvider(null); }} > {currentModelId ? `${currentProviderConfig?.name || currentProviderId} / ${currentModelId}` : t('settings.selectModel')} {/* Level 1: Provider list */} {!drillProvider && (
{t('toolbar.selectProvider')}
{configuredProviders.map((provider) => { const isActive = currentProviderId === provider.id; return ( ); })}
)} {/* Level 2: Model list for selected provider */} {drillProvider && activeProvider && (
{/* Back header */} {/* Models */} {activeProvider.models.map((model) => { const isSelected = currentProviderId === drillProvider && currentModelId === model.id; return ( ); })}
)}
); }