/*
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 (