feat: add toggle to show unvalidated models in model selector (#413)

* feat: add toggle to show unvalidated models in model selector

Add a toggle switch in the model configuration dialog to allow users to
display models that haven't been validated. This helps users who work with
model providers that have disabled their verification endpoints.

Changes:
- Add showUnvalidatedModels field to MultiModelConfig type
- Add setShowUnvalidatedModels method to useModelConfig hook
- Add Switch toggle in model-config-dialog footer
- Update model-selector to filter based on showUnvalidatedModels setting
- Add warning icon for unvalidated models in the selector
- Add i18n translations for en/zh/ja

Closes #410

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: wrap AlertTriangle in span for title attribute

The AlertTriangle icon from lucide-react doesn't support the title prop directly.
Wrapped it in a span element to properly display the tooltip.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
xunc lee
2025-12-26 11:19:59 +08:00
committed by GitHub
parent 067d309927
commit 31644dbcd8
9 changed files with 79 additions and 17 deletions

View File

@@ -162,6 +162,7 @@ interface ChatInputProps {
models?: FlattenedModel[] models?: FlattenedModel[]
selectedModelId?: string selectedModelId?: string
onModelSelect?: (modelId: string | undefined) => void onModelSelect?: (modelId: string | undefined) => void
showUnvalidatedModels?: boolean
onConfigureModels?: () => void onConfigureModels?: () => void
} }
@@ -183,6 +184,7 @@ export function ChatInput({
models = [], models = [],
selectedModelId, selectedModelId,
onModelSelect = () => {}, onModelSelect = () => {},
showUnvalidatedModels = false,
onConfigureModels = () => {}, onConfigureModels = () => {},
}: ChatInputProps) { }: ChatInputProps) {
const dict = useDictionary() const dict = useDictionary()
@@ -482,6 +484,7 @@ export function ChatInput({
onSelect={onModelSelect} onSelect={onModelSelect}
onConfigure={onConfigureModels} onConfigure={onConfigureModels}
disabled={isDisabled} disabled={isDisabled}
showUnvalidatedModels={showUnvalidatedModels}
/> />
<div className="w-px h-5 bg-border mx-1" /> <div className="w-px h-5 bg-border mx-1" />

View File

@@ -1071,6 +1071,7 @@ export default function ChatPanel({
models={modelConfig.models} models={modelConfig.models}
selectedModelId={modelConfig.selectedModelId} selectedModelId={modelConfig.selectedModelId}
onModelSelect={modelConfig.setSelectedModelId} onModelSelect={modelConfig.setSelectedModelId}
showUnvalidatedModels={modelConfig.showUnvalidatedModels}
onConfigureModels={() => setShowModelConfigDialog(true)} onConfigureModels={() => setShowModelConfigDialog(true)}
/> />
</footer> </footer>

View File

@@ -50,6 +50,7 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select" } from "@/components/ui/select"
import { Switch } from "@/components/ui/switch"
import { useDictionary } from "@/hooks/use-dictionary" import { useDictionary } from "@/hooks/use-dictionary"
import type { UseModelConfigReturn } from "@/hooks/use-model-config" import type { UseModelConfigReturn } from "@/hooks/use-model-config"
import { formatMessage } from "@/lib/i18n/utils" import { formatMessage } from "@/lib/i18n/utils"
@@ -1447,11 +1448,24 @@ export function ModelConfigDialog({
{/* Footer */} {/* Footer */}
<div className="px-6 py-3 border-t border-border-subtle bg-surface-1/30 shrink-0"> <div className="px-6 py-3 border-t border-border-subtle bg-surface-1/30 shrink-0">
<p className="text-xs text-muted-foreground text-center flex items-center justify-center gap-1.5"> <div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Switch
checked={modelConfig.showUnvalidatedModels}
onCheckedChange={
modelConfig.setShowUnvalidatedModels
}
/>
<Label className="text-xs text-muted-foreground cursor-pointer">
{dict.modelConfig.showUnvalidatedModels}
</Label>
</div>
<p className="text-xs text-muted-foreground flex items-center gap-1.5">
<Key className="h-3 w-3" /> <Key className="h-3 w-3" />
{dict.modelConfig.apiKeyStored} {dict.modelConfig.apiKeyStored}
</p> </p>
</div> </div>
</div>
</DialogContent> </DialogContent>
{/* Delete Confirmation Dialog */} {/* Delete Confirmation Dialog */}

View File

@@ -1,6 +1,13 @@
"use client" "use client"
import { Bot, Check, ChevronDown, Server, Settings2 } from "lucide-react" import {
AlertTriangle,
Bot,
Check,
ChevronDown,
Server,
Settings2,
} from "lucide-react"
import { useMemo, useState } from "react" import { useMemo, useState } from "react"
import { import {
ModelSelectorContent, ModelSelectorContent,
@@ -26,6 +33,7 @@ interface ModelSelectorProps {
onSelect: (modelId: string | undefined) => void onSelect: (modelId: string | undefined) => void
onConfigure: () => void onConfigure: () => void
disabled?: boolean disabled?: boolean
showUnvalidatedModels?: boolean
} }
// Map our provider names to models.dev logo names // Map our provider names to models.dev logo names
@@ -67,17 +75,20 @@ export function ModelSelector({
onSelect, onSelect,
onConfigure, onConfigure,
disabled = false, disabled = false,
showUnvalidatedModels = false,
}: ModelSelectorProps) { }: ModelSelectorProps) {
const dict = useDictionary() const dict = useDictionary()
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
// Only show validated models in the selector // Filter models based on showUnvalidatedModels setting
const validatedModels = useMemo( const displayModels = useMemo(() => {
() => models.filter((m) => m.validated === true), if (showUnvalidatedModels) {
[models], return models
) }
return models.filter((m) => m.validated === true)
}, [models, showUnvalidatedModels])
const groupedModels = useMemo( const groupedModels = useMemo(
() => groupModelsByProvider(validatedModels), () => groupModelsByProvider(displayModels),
[validatedModels], [displayModels],
) )
// Find selected model for display // Find selected model for display
@@ -126,7 +137,7 @@ export function ModelSelector({
/> />
<ModelSelectorList className="[&::-webkit-scrollbar]:hidden [-ms-overflow-style:none] [scrollbar-width:none]"> <ModelSelectorList className="[&::-webkit-scrollbar]:hidden [-ms-overflow-style:none] [scrollbar-width:none]">
<ModelSelectorEmpty> <ModelSelectorEmpty>
{validatedModels.length === 0 && models.length > 0 {displayModels.length === 0 && models.length > 0
? dict.modelConfig.noVerifiedModels ? dict.modelConfig.noVerifiedModels
: dict.modelConfig.noModelsFound} : dict.modelConfig.noModelsFound}
</ModelSelectorEmpty> </ModelSelectorEmpty>
@@ -191,6 +202,16 @@ export function ModelSelector({
<ModelSelectorName> <ModelSelectorName>
{model.modelId} {model.modelId}
</ModelSelectorName> </ModelSelectorName>
{model.validated !== true && (
<span
title={
dict.modelConfig
.unvalidatedModelWarning
}
>
<AlertTriangle className="ml-auto h-3 w-3 text-warning" />
</span>
)}
</ModelSelectorItem> </ModelSelectorItem>
))} ))}
</ModelSelectorGroup> </ModelSelectorGroup>
@@ -213,7 +234,9 @@ export function ModelSelector({
</ModelSelectorGroup> </ModelSelectorGroup>
{/* Info text */} {/* Info text */}
<div className="px-3 py-2 text-xs text-muted-foreground border-t"> <div className="px-3 py-2 text-xs text-muted-foreground border-t">
{dict.modelConfig.onlyVerifiedShown} {showUnvalidatedModels
? dict.modelConfig.allModelsShown
: dict.modelConfig.onlyVerifiedShown}
</div> </div>
</ModelSelectorList> </ModelSelectorList>
</ModelSelectorContent> </ModelSelectorContent>

View File

@@ -109,9 +109,11 @@ export interface UseModelConfigReturn {
models: FlattenedModel[] models: FlattenedModel[]
selectedModel: FlattenedModel | undefined selectedModel: FlattenedModel | undefined
selectedModelId: string | undefined selectedModelId: string | undefined
showUnvalidatedModels: boolean
// Actions // Actions
setSelectedModelId: (modelId: string | undefined) => void setSelectedModelId: (modelId: string | undefined) => void
setShowUnvalidatedModels: (show: boolean) => void
addProvider: (provider: ProviderName) => ProviderConfig addProvider: (provider: ProviderName) => ProviderConfig
updateProvider: ( updateProvider: (
providerId: string, providerId: string,
@@ -160,6 +162,13 @@ export function useModelConfig(): UseModelConfigReturn {
})) }))
}, []) }, [])
const setShowUnvalidatedModels = useCallback((show: boolean) => {
setConfig((prev) => ({
...prev,
showUnvalidatedModels: show,
}))
}, [])
const addProvider = useCallback( const addProvider = useCallback(
(provider: ProviderName): ProviderConfig => { (provider: ProviderName): ProviderConfig => {
const newProvider = createProviderConfig(provider) const newProvider = createProviderConfig(provider)
@@ -278,7 +287,9 @@ export function useModelConfig(): UseModelConfigReturn {
models, models,
selectedModel, selectedModel,
selectedModelId: config.selectedModelId, selectedModelId: config.selectedModelId,
showUnvalidatedModels: config.showUnvalidatedModels ?? false,
setSelectedModelId, setSelectedModelId,
setShowUnvalidatedModels,
addProvider, addProvider,
updateProvider, updateProvider,
deleteProvider, deleteProvider,

View File

@@ -243,6 +243,9 @@
"default": "Default", "default": "Default",
"serverDefault": "Server Default", "serverDefault": "Server Default",
"configureModels": "Configure Models...", "configureModels": "Configure Models...",
"onlyVerifiedShown": "Only verified models are shown" "onlyVerifiedShown": "Only verified models are shown",
"showUnvalidatedModels": "Show unvalidated models",
"allModelsShown": "All models are shown (including unvalidated)",
"unvalidatedModelWarning": "This model has not been validated"
} }
} }

View File

@@ -243,6 +243,9 @@
"default": "デフォルト", "default": "デフォルト",
"serverDefault": "サーバーデフォルト", "serverDefault": "サーバーデフォルト",
"configureModels": "モデルを設定...", "configureModels": "モデルを設定...",
"onlyVerifiedShown": "検証済みのモデルのみ表示" "onlyVerifiedShown": "検証済みのモデルのみ表示",
"showUnvalidatedModels": "未検証のモデルを表示",
"allModelsShown": "すべてのモデルを表示(未検証を含む)",
"unvalidatedModelWarning": "このモデルは検証されていません"
} }
} }

View File

@@ -243,6 +243,9 @@
"default": "默认", "default": "默认",
"serverDefault": "服务器默认", "serverDefault": "服务器默认",
"configureModels": "配置模型...", "configureModels": "配置模型...",
"onlyVerifiedShown": "仅显示已验证的模型" "onlyVerifiedShown": "仅显示已验证的模型",
"showUnvalidatedModels": "显示未验证的模型",
"allModelsShown": "显示所有模型(包括未验证的)",
"unvalidatedModelWarning": "此模型尚未验证"
} }
} }

View File

@@ -40,6 +40,7 @@ export interface MultiModelConfig {
version: 1 version: 1
providers: ProviderConfig[] providers: ProviderConfig[]
selectedModelId?: string // Currently selected model's UUID selectedModelId?: string // Currently selected model's UUID
showUnvalidatedModels?: boolean // Show models that haven't been validated
} }
// Flattened model for dropdown display // Flattened model for dropdown display