-
Notifications
You must be signed in to change notification settings - Fork 17
feat: 为 Provider 和 Routes 页面添加搜索功能 #118
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -3,14 +3,20 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||
| * 全局路由配置页面 - 显示当前 ClientType 的路由 | ||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useState } from 'react'; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useParams } from 'react-router-dom'; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useTranslation } from 'react-i18next'; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| import { Search } from 'lucide-react'; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| import { ClientIcon, getClientName } from '@/components/icons/client-icons'; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| import type { ClientType } from '@/lib/transport'; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| import { ClientTypeRoutesContent } from '@/components/routes/ClientTypeRoutesContent'; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| import { Input } from '@/components/ui/input'; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| export function ClientRoutesPage() { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const { t } = useTranslation(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const { clientType } = useParams<{ clientType: string }>(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const activeClientType = (clientType as ClientType) || 'claude'; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const [searchQuery, setSearchQuery] = useState(''); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="flex flex-col h-full bg-background"> | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -29,11 +35,27 @@ export function ClientRoutesPage() { | |||||||||||||||||||||||||||||||||||||||||||||||||
| </p> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="relative"> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| <Search | ||||||||||||||||||||||||||||||||||||||||||||||||||
| size={14} | ||||||||||||||||||||||||||||||||||||||||||||||||||
| className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground" | ||||||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| <Input | ||||||||||||||||||||||||||||||||||||||||||||||||||
| placeholder={t('common.searchProviders')} | ||||||||||||||||||||||||||||||||||||||||||||||||||
| value={searchQuery} | ||||||||||||||||||||||||||||||||||||||||||||||||||
| onChange={(e) => setSearchQuery(e.target.value)} | ||||||||||||||||||||||||||||||||||||||||||||||||||
| className="pl-9 w-48" | ||||||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+38
to
+48
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 为搜索输入补充可访问性标签。 🧩 建议修复- <Input
- placeholder={t('common.searchProviders')}
- value={searchQuery}
- onChange={(e) => setSearchQuery(e.target.value)}
- className="pl-9 w-48"
- />
+ <Input
+ type="search"
+ aria-label={t('common.searchProviders')}
+ placeholder={t('common.searchProviders')}
+ value={searchQuery}
+ onChange={(e) => setSearchQuery(e.target.value)}
+ className="pl-9 w-48"
+ />📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| {/* Content */} | ||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="flex-1 min-w-0 overflow-hidden"> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| <ClientTypeRoutesContent clientType={activeClientType} projectID={0} /> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| <ClientTypeRoutesContent | ||||||||||||||||||||||||||||||||||||||||||||||||||
| clientType={activeClientType} | ||||||||||||||||||||||||||||||||||||||||||||||||||
| projectID={0} | ||||||||||||||||||||||||||||||||||||||||||||||||||
| searchQuery={searchQuery} | ||||||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,10 +1,11 @@ | ||
| import { Globe, ChevronLeft, Key, Check } from 'lucide-react'; | ||
| import { Globe, ChevronLeft, Key, Check, Plus, Trash2, ArrowRight } from 'lucide-react'; | ||
| import { useTranslation } from 'react-i18next'; | ||
| import { useCreateProvider } from '@/hooks/queries'; | ||
| import { useCreateProvider, useCreateModelMapping } from '@/hooks/queries'; | ||
| import type { ClientType, CreateProviderData } from '@/lib/transport'; | ||
| import { ClientsConfigSection } from './clients-config-section'; | ||
| import { Button } from '@/components/ui/button'; | ||
| import { Input } from '@/components/ui/input'; | ||
| import { ModelInput } from '@/components/ui/model-input'; | ||
| import { useProviderForm } from '../context/provider-form-context'; | ||
| import { useProviderNavigation } from '../hooks/use-provider-navigation'; | ||
|
|
||
|
|
@@ -13,6 +14,7 @@ export function CustomConfigStep() { | |
| const { formData, updateFormData, updateClient, isValid, isSaving, setSaving, saveStatus, setSaveStatus } = useProviderForm(); | ||
| const { goToSelectType, goToProviders } = useProviderNavigation(); | ||
| const createProvider = useCreateProvider(); | ||
| const createModelMapping = useCreateModelMapping(); | ||
|
|
||
| const handleSave = async () => { | ||
| if (!isValid()) return; | ||
|
|
@@ -32,6 +34,7 @@ export function CustomConfigStep() { | |
| const data: CreateProviderData = { | ||
| type: 'custom', | ||
| name: formData.name, | ||
| logo: formData.logo, | ||
| config: { | ||
| custom: { | ||
| baseURL: formData.baseURL, | ||
|
|
@@ -42,7 +45,20 @@ export function CustomConfigStep() { | |
| supportedClientTypes, | ||
| }; | ||
|
|
||
| await createProvider.mutateAsync(data); | ||
| const provider = await createProvider.mutateAsync(data); | ||
|
|
||
| // Create model mappings if template has any | ||
| if (formData.modelMappings && formData.modelMappings.length > 0) { | ||
| for (const mapping of formData.modelMappings) { | ||
| await createModelMapping.mutateAsync({ | ||
| scope: 'provider', | ||
| providerID: provider.id, | ||
| pattern: mapping.pattern, | ||
| target: mapping.target, | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| setSaveStatus('success'); | ||
| setTimeout(() => goToProviders(), 500); | ||
| } catch (error) { | ||
|
|
@@ -153,6 +169,81 @@ export function CustomConfigStep() { | |
| <ClientsConfigSection clients={formData.clients} onUpdateClient={updateClient} /> | ||
| </div> | ||
|
|
||
| {/* Model Mapping Section */} | ||
| <div className="space-y-6"> | ||
| <div className="flex items-center justify-between border-b border-border pb-2"> | ||
| <h3 className="text-lg font-semibold text-text-primary"> | ||
| {t('modelMappings.title')} | ||
| </h3> | ||
| <Button | ||
| variant="outline" | ||
| size="sm" | ||
| onClick={() => { | ||
| const newMappings = [...(formData.modelMappings || []), { pattern: '', target: '' }]; | ||
| updateFormData({ modelMappings: newMappings }); | ||
| }} | ||
| > | ||
| <Plus size={14} /> | ||
| {t('routes.modelMapping.addMapping')} | ||
| </Button> | ||
| </div> | ||
|
|
||
| {formData.modelMappings && formData.modelMappings.length > 0 ? ( | ||
| <div className="space-y-3"> | ||
| {formData.modelMappings.map((mapping, index) => ( | ||
| <div key={index} className="flex items-center gap-3 p-3 bg-muted/50 rounded-lg"> | ||
| <div className="flex-1"> | ||
| <label className="text-xs text-muted-foreground mb-1 block"> | ||
| {t('settings.matchPattern')} | ||
| </label> | ||
| <Input | ||
| type="text" | ||
| value={mapping.pattern} | ||
| onChange={(e) => { | ||
| const newMappings = [...(formData.modelMappings || [])]; | ||
| newMappings[index] = { ...newMappings[index], pattern: e.target.value }; | ||
| updateFormData({ modelMappings: newMappings }); | ||
| }} | ||
| placeholder="*claude*, *sonnet*, *" | ||
| className="font-mono text-sm" | ||
| /> | ||
| </div> | ||
| <ArrowRight size={16} className="text-muted-foreground shrink-0 mt-5" /> | ||
| <div className="flex-1"> | ||
| <label className="text-xs text-muted-foreground mb-1 block"> | ||
| {t('settings.targetModel')} | ||
| </label> | ||
| <ModelInput | ||
| value={mapping.target} | ||
| onChange={(value) => { | ||
| const newMappings = [...(formData.modelMappings || [])]; | ||
| newMappings[index] = { ...newMappings[index], target: value }; | ||
| updateFormData({ modelMappings: newMappings }); | ||
| }} | ||
| placeholder={t('modelInput.selectOrEnter')} | ||
| /> | ||
| </div> | ||
| <Button | ||
| variant="ghost" | ||
| size="icon" | ||
| className="shrink-0 mt-5 text-muted-foreground hover:text-destructive" | ||
| onClick={() => { | ||
| const newMappings = (formData.modelMappings || []).filter((_, i) => i !== index); | ||
| updateFormData({ modelMappings: newMappings }); | ||
| }} | ||
| > | ||
| <Trash2 size={14} /> | ||
| </Button> | ||
| </div> | ||
| ))} | ||
| </div> | ||
| ) : ( | ||
| <div className="text-sm text-muted-foreground p-4 bg-muted/30 rounded-lg text-center"> | ||
| {t('modelMappings.noMappings')} | ||
| </div> | ||
| )} | ||
| </div> | ||
|
Comment on lines
+172
to
+245
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 删除按钮为纯图标,建议补充可访问性文本。 🧩 建议修复- <Button
+ <Button
variant="ghost"
size="icon"
className="shrink-0 mt-5 text-muted-foreground hover:text-destructive"
+ aria-label={t('common.delete')}
+ title={t('common.delete')}
onClick={() => {
const newMappings = (formData.modelMappings || []).filter((_, i) => i !== index);
updateFormData({ modelMappings: newMappings });
}}
>🤖 Prompt for AI Agents |
||
|
|
||
| {saveStatus === 'error' && ( | ||
| <div className="p-4 bg-error/10 border border-error/30 rounded-lg text-sm text-error flex items-center gap-2"> | ||
| <div className="w-1.5 h-1.5 rounded-full bg-error" /> | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -41,6 +41,8 @@ export function SelectTypeStep() { | |||||||||
| selectedTemplate: templateId, | ||||||||||
| name: template.name, | ||||||||||
| clients: updatedClients, | ||||||||||
| modelMappings: template.modelMappings, | ||||||||||
| logo: template.logoUrl, | ||||||||||
|
Comment on lines
+44
to
+45
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 避免直接复用模板的 modelMappings 引用 🛠 建议修复- modelMappings: template.modelMappings,
+ modelMappings: template.modelMappings?.map((m) => ({ ...m })),📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||
| }); | ||||||||||
|
|
||||||||||
| goToCustomConfig(); | ||||||||||
|
|
||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: awsl-project/maxx
Length of output: 188
🏁 Script executed:
Repository: awsl-project/maxx
Length of output: 171
🏁 Script executed:
Repository: awsl-project/maxx
Length of output: 181
🏁 Script executed:
rg -n "type Provider struct" -A 50 internal/domain/model.goRepository: awsl-project/maxx
Length of output: 1409
🏁 Script executed:
Repository: awsl-project/maxx
Length of output: 5457
🏁 Script executed:
cat -n internal/repository/sqlite/models.go | head -100Repository: awsl-project/maxx
Length of output: 2771
🏁 Script executed:
rg -n "validate|Validate" internal/domain/model.goRepository: awsl-project/maxx
Length of output: 43
🏁 Script executed:
Repository: awsl-project/maxx
Length of output: 1135
🏁 Script executed:
rg -n "Logo" internal/repository/sqlite/models.goRepository: awsl-project/maxx
Length of output: 43
🏁 Script executed:
Repository: awsl-project/maxx
Length of output: 6890
Logo 字段缺失 GORM 模型定义,导致数据无法持久化
Logo 字段已添加到域模型 (internal/domain/model.go:91-92),但在 GORM 模型 (internal/repository/sqlite/models.go:115-121) 中缺失。由于 AutoMigrate 仅处理 GORM 模型中定义的字段,缺少该定义会导致数据库列不被创建,Logo 数据将被丢弃。
必须在 Provider 结构体中补充 Logo 字段定义,建议添加长度限制以防止超长 data URI 导致存储膨胀。
🤖 Prompt for AI Agents