/* Copyright (C) 2025 QuantumNous This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ import React, { useState, useRef, useEffect, useCallback } from 'react'; import { Input, Typography } from '@douyinfe/semi-ui'; import { IconSearch } from '@douyinfe/semi-icons'; import { API, isAdmin, showSuccess } from '../../../helpers'; import { useNavigate } from 'react-router-dom'; const { Text } = Typography; const SEARCH_DEBOUNCE_MS = 300; function debounce(fn, ms) { let timer; return (...args) => { clearTimeout(timer); timer = setTimeout(() => fn(...args), ms); }; } const SearchDropdown = ({ isMobile }) => { const [searchValue, setSearchValue] = useState(''); const [visible, setVisible] = useState(false); const [models, setModels] = useState([]); const [results, setResults] = useState([]); const [loading, setLoading] = useState(false); const dropdownRef = useRef(null); const inputRef = useRef(null); const navigate = useNavigate(); // 预加载模型数据 useEffect(() => { (async () => { try { const res = await API.get('/api/pricing', { disableDuplicate: true }); if (res?.data?.success && Array.isArray(res.data.data)) { setModels(res.data.data); } } catch {} })(); }, []); const doSearch = useCallback( (q) => { const trimmed = q.trim(); if (!trimmed) { setResults([]); return; } setLoading(true); const lower = trimmed.toLowerCase(); const matched = []; for (const m of models) { const name = (m.model_name || '').toLowerCase(); const desc = (m.description || '').toLowerCase(); const tags = (m.tags || '').toLowerCase(); if (name.includes(lower) || desc.includes(lower) || tags.includes(lower)) { matched.push({ type: 'model', label: m.model_name, desc: m.description || '', navigate: `/pricing?search=${encodeURIComponent(trimmed)}`, }); } if (matched.length >= 8) break; } setResults(matched); setLoading(false); }, [models], ); const debouncedSearch = useCallback( debounce(doSearch, SEARCH_DEBOUNCE_MS), [doSearch], ); useEffect(() => { debouncedSearch(searchValue); }, [searchValue, debouncedSearch]); useEffect(() => { const handleKeyDown = (event) => { if (event.key === '/' && document.activeElement === document.body) { event.preventDefault(); inputRef.current?.focus(); setVisible(true); } if (event.key === 'Escape') { setVisible(false); inputRef.current?.blur(); } }; document.addEventListener('keydown', handleKeyDown); return () => document.removeEventListener('keydown', handleKeyDown); }, []); const handleItemClick = (item) => { setVisible(false); setSearchValue(''); setResults([]); if (item.navigate) { if (item.navigate.startsWith('/')) { navigate(item.navigate); } else { window.open(item.navigate, '_blank'); } } }; const typeLabel = (type) => { switch (type) { case 'model': return '模型'; default: return ''; } }; const renderDropdownContent = () => { if (!searchValue.trim()) { return (
输入关键词搜索模型
); } if (loading) { return (
搜索中...
); } if (results.length === 0) { return (
未找到匹配结果
); } return (
{results.map((item, i) => (
handleItemClick(item)} className='px-4 py-2.5 flex items-center gap-3 cursor-pointer hover:bg-semi-color-fill-1 dark:hover:bg-gray-700 transition-colors' > {typeLabel(item.type)}
{item.label} {item.desc && ( {item.desc} )}
))}
); }; return (
} suffix={ / } value={searchValue} onChange={setSearchValue} onFocus={() => { if (searchValue.trim()) setVisible(true); }} onBlur={() => setTimeout(() => setVisible(false), 150)} className='!w-40 lg:!w-56 !h-9 !text-sm !bg-semi-color-fill-0 dark:!bg-gray-800/50 !border-semi-color-border dark:!border-gray-700 hover:!border-semi-color-primary dark:hover:!border-blue-400 focus:!border-semi-color-primary dark:focus:!border-blue-400' style={{ borderRadius: '6px' }} />
{visible && (results.length > 0 || searchValue.trim()) && (
{renderDropdownContent()}
)}
); }; export default SearchDropdown;