Skip to content

Commit b919360

Browse files
authored
feat: 为 Provider 和 Routes 页面添加搜索功能 (#118)
- Provider 列表页添加搜索框,支持按名称和显示信息过滤 - Client Routes 页面 (/routes/claude 等) 添加搜索框 - 搜索同时过滤已配置路由和可用 Provider - 添加 Custom Provider 模板的 Model Mapping 支持 - 添加 NVIDIA 模板 (OpenAI 兼容,默认模型映射) - Provider 创建页面增加 Model Mapping 编辑功能 - Provider 新增 Logo 字段,支持模板自动设置 - 添加 88code、aicodemirror、nvidia 图标资源 - 补充 i18n 翻译
1 parent 79754aa commit b919360

File tree

14 files changed

+238
-15
lines changed

14 files changed

+238
-15
lines changed

internal/domain/model.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,9 @@ type Provider struct {
8888
// 展示的名称
8989
Name string `json:"name"`
9090

91+
// Logo URL 或 data URI
92+
Logo string `json:"logo,omitempty"`
93+
9194
// 配置
9295
Config *ProviderConfig `json:"config"`
9396

web/src/assets/icons/88code.svg

Lines changed: 4 additions & 0 deletions
Loading
6.01 KB
Loading

web/src/assets/icons/nvidia.svg

Lines changed: 1 addition & 0 deletions
Loading

web/src/components/routes/ClientTypeRoutesContent.tsx

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,13 @@ import { AntigravityQuotasProvider } from '@/contexts/antigravity-quotas-context
4848
interface ClientTypeRoutesContentProps {
4949
clientType: ClientType;
5050
projectID: number; // 0 for global routes
51+
searchQuery?: string; // Optional search query from parent
5152
}
5253

5354
export function ClientTypeRoutesContent({
5455
clientType,
5556
projectID,
57+
searchQuery = '',
5658
}: ClientTypeRoutesContentProps) {
5759
const [activeId, setActiveId] = useState<string | null>(null)
5860
const { data: providerStats = {} } = useProviderStats(clientType, projectID || undefined)
@@ -100,7 +102,16 @@ export function ClientTypeRoutesContent({
100102
});
101103

102104
// Only show providers that have routes
103-
const filteredItems = allItems.filter((item) => item.route);
105+
let filteredItems = allItems.filter((item) => item.route);
106+
107+
// Apply search filter
108+
if (searchQuery.trim()) {
109+
const query = searchQuery.toLowerCase();
110+
filteredItems = filteredItems.filter((item) =>
111+
item.provider.name.toLowerCase().includes(query) ||
112+
item.provider.type.toLowerCase().includes(query)
113+
);
114+
}
104115

105116
return filteredItems.sort((a, b) => {
106117
if (a.route && b.route) return a.route.position - b.route.position;
@@ -110,15 +121,26 @@ export function ClientTypeRoutesContent({
110121
if (!a.isNative && b.isNative) return 1;
111122
return a.provider.name.localeCompare(b.provider.name);
112123
});
113-
}, [providers, clientRoutes, clientType]);
124+
}, [providers, clientRoutes, clientType, searchQuery]);
114125

115126
// Get available providers (without routes yet)
116127
const availableProviders = useMemo((): Provider[] => {
117-
return providers.filter((p) => {
128+
let available = providers.filter((p) => {
118129
const hasRoute = clientRoutes.some((r) => Number(r.providerID) === Number(p.id));
119130
return !hasRoute;
120131
});
121-
}, [providers, clientRoutes]);
132+
133+
// Apply search filter
134+
if (searchQuery.trim()) {
135+
const query = searchQuery.toLowerCase();
136+
available = available.filter((p) =>
137+
p.name.toLowerCase().includes(query) ||
138+
p.type.toLowerCase().includes(query)
139+
);
140+
}
141+
142+
return available;
143+
}, [providers, clientRoutes, searchQuery]);
122144

123145
const activeItem = activeId ? items.find((item) => item.id === activeId) : null;
124146

web/src/components/ui/model-input.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ const COMMON_MODELS = [
4848
{ id: '*gpt*', name: 'All GPT models', provider: 'OpenAI' },
4949
{ id: '*o1*', name: 'All o1 models', provider: 'OpenAI' },
5050
{ id: '*o3*', name: 'All o3 models', provider: 'OpenAI' },
51+
// NVIDIA/Meta wildcards
52+
{ id: '*llama*', name: 'All Llama models', provider: 'NVIDIA' },
53+
{ id: 'meta/*', name: 'All Meta models', provider: 'NVIDIA' },
5154
// OpenAI models
5255
{ id: 'gpt-4o', name: 'GPT-4o', provider: 'OpenAI' },
5356
{ id: 'gpt-4o-mini', name: 'GPT-4o Mini', provider: 'OpenAI' },
@@ -58,6 +61,15 @@ const COMMON_MODELS = [
5861
{ id: 'o1-mini', name: 'o1 Mini', provider: 'OpenAI' },
5962
{ id: 'o1-pro', name: 'o1 Pro', provider: 'OpenAI' },
6063
{ id: 'o3-mini', name: 'o3 Mini', provider: 'OpenAI' },
64+
// NVIDIA models
65+
{ id: 'minimaxai/minimax-m2.1', name: 'MiniMax M2.1', provider: 'NVIDIA' },
66+
{ id: 'z-ai/glm4.7', name: 'GLM 4.7', provider: 'NVIDIA' },
67+
{ id: 'deepseek-ai/deepseek-rl', name: 'DeepSeek RL', provider: 'NVIDIA' },
68+
{ id: 'qwen/qwen2.5-coder-32b-instruct', name: 'Qwen 2.5 Coder 32B', provider: 'NVIDIA' },
69+
{ id: 'openai/gpt-oss-120b', name: 'GPT OSS 120B', provider: 'NVIDIA' },
70+
{ id: 'google/gemma-3-27b-it', name: 'Gemma 3 27B', provider: 'NVIDIA' },
71+
{ id: 'meta/llama-4-maverick-17b-128e-instruct', name: 'Llama 4 Maverick 17B', provider: 'NVIDIA' },
72+
{ id: 'mistralai/devstral-2-123b-instruct-2512', name: 'Devstral 2 123B', provider: 'NVIDIA' },
6173
// Antigravity supported target models (use these as mapping targets)
6274
{
6375
id: 'claude-opus-4-5-thinking',

web/src/lib/transport/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export interface Provider {
4646
updatedAt: string;
4747
type: string;
4848
name: string;
49+
logo?: string; // Logo URL or data URI
4950
config: ProviderConfig | null;
5051
supportedClientTypes: ClientType[];
5152
}

web/src/locales/en.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,9 @@
3636
"days": "days",
3737
"hours": "hours",
3838
"global": "Global",
39-
"initFailed": "Failed to Initialize"
39+
"initFailed": "Failed to Initialize",
40+
"search": "Search",
41+
"searchProviders": "Search providers..."
4042
},
4143
"nav": {
4244
"dashboard": "Dashboard",
@@ -568,6 +570,10 @@
568570
"freeduck": {
569571
"name": "Free Duck",
570572
"description": "Free site · Claude Code only"
573+
},
574+
"nvidia": {
575+
"name": "NVIDIA NIM",
576+
"description": "NVIDIA NIM · OpenAI Compatible"
571577
}
572578
}
573579
}

web/src/locales/zh.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,9 @@
3636
"days": "",
3737
"hours": "小时",
3838
"global": "全局",
39-
"initFailed": "初始化失败"
39+
"initFailed": "初始化失败",
40+
"search": "搜索",
41+
"searchProviders": "搜索 Provider..."
4042
},
4143
"nav": {
4244
"dashboard": "仪表板",
@@ -567,6 +569,10 @@
567569
"freeduck": {
568570
"name": "Free Duck",
569571
"description": "免费站点 · 只有 Claude Code"
572+
},
573+
"nvidia": {
574+
"name": "NVIDIA NIM",
575+
"description": "NVIDIA NIM · OpenAI 兼容"
570576
}
571577
}
572578
}

web/src/pages/client-routes/index.tsx

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,20 @@
33
* 全局路由配置页面 - 显示当前 ClientType 的路由
44
*/
55

6+
import { useState } from 'react';
67
import { useParams } from 'react-router-dom';
8+
import { useTranslation } from 'react-i18next';
9+
import { Search } from 'lucide-react';
710
import { ClientIcon, getClientName } from '@/components/icons/client-icons';
811
import type { ClientType } from '@/lib/transport';
912
import { ClientTypeRoutesContent } from '@/components/routes/ClientTypeRoutesContent';
13+
import { Input } from '@/components/ui/input';
1014

1115
export function ClientRoutesPage() {
16+
const { t } = useTranslation();
1217
const { clientType } = useParams<{ clientType: string }>();
1318
const activeClientType = (clientType as ClientType) || 'claude';
19+
const [searchQuery, setSearchQuery] = useState('');
1420

1521
return (
1622
<div className="flex flex-col h-full bg-background">
@@ -29,11 +35,27 @@ export function ClientRoutesPage() {
2935
</p>
3036
</div>
3137
</div>
38+
<div className="relative">
39+
<Search
40+
size={14}
41+
className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground"
42+
/>
43+
<Input
44+
placeholder={t('common.searchProviders')}
45+
value={searchQuery}
46+
onChange={(e) => setSearchQuery(e.target.value)}
47+
className="pl-9 w-48"
48+
/>
49+
</div>
3250
</div>
3351

3452
{/* Content */}
3553
<div className="flex-1 min-w-0 overflow-hidden">
36-
<ClientTypeRoutesContent clientType={activeClientType} projectID={0} />
54+
<ClientTypeRoutesContent
55+
clientType={activeClientType}
56+
projectID={0}
57+
searchQuery={searchQuery}
58+
/>
3759
</div>
3860
</div>
3961
);

0 commit comments

Comments
 (0)