mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-01-02 14:22:28 +08:00
* 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>
246 lines
9.6 KiB
TypeScript
246 lines
9.6 KiB
TypeScript
"use client"
|
|
|
|
import {
|
|
AlertTriangle,
|
|
Bot,
|
|
Check,
|
|
ChevronDown,
|
|
Server,
|
|
Settings2,
|
|
} from "lucide-react"
|
|
import { useMemo, useState } from "react"
|
|
import {
|
|
ModelSelectorContent,
|
|
ModelSelectorEmpty,
|
|
ModelSelectorGroup,
|
|
ModelSelectorInput,
|
|
ModelSelectorItem,
|
|
ModelSelectorList,
|
|
ModelSelectorLogo,
|
|
ModelSelectorName,
|
|
ModelSelector as ModelSelectorRoot,
|
|
ModelSelectorSeparator,
|
|
ModelSelectorTrigger,
|
|
} from "@/components/ai-elements/model-selector"
|
|
import { ButtonWithTooltip } from "@/components/button-with-tooltip"
|
|
import { useDictionary } from "@/hooks/use-dictionary"
|
|
import type { FlattenedModel } from "@/lib/types/model-config"
|
|
import { cn } from "@/lib/utils"
|
|
|
|
interface ModelSelectorProps {
|
|
models: FlattenedModel[]
|
|
selectedModelId: string | undefined
|
|
onSelect: (modelId: string | undefined) => void
|
|
onConfigure: () => void
|
|
disabled?: boolean
|
|
showUnvalidatedModels?: boolean
|
|
}
|
|
|
|
// Map our provider names to models.dev logo names
|
|
const PROVIDER_LOGO_MAP: Record<string, string> = {
|
|
openai: "openai",
|
|
anthropic: "anthropic",
|
|
google: "google",
|
|
azure: "azure",
|
|
bedrock: "amazon-bedrock",
|
|
openrouter: "openrouter",
|
|
deepseek: "deepseek",
|
|
siliconflow: "siliconflow",
|
|
gateway: "vercel",
|
|
}
|
|
|
|
// Group models by providerLabel (handles duplicate providers)
|
|
function groupModelsByProvider(
|
|
models: FlattenedModel[],
|
|
): Map<string, { provider: string; models: FlattenedModel[] }> {
|
|
const groups = new Map<
|
|
string,
|
|
{ provider: string; models: FlattenedModel[] }
|
|
>()
|
|
for (const model of models) {
|
|
const key = model.providerLabel
|
|
const existing = groups.get(key)
|
|
if (existing) {
|
|
existing.models.push(model)
|
|
} else {
|
|
groups.set(key, { provider: model.provider, models: [model] })
|
|
}
|
|
}
|
|
return groups
|
|
}
|
|
|
|
export function ModelSelector({
|
|
models,
|
|
selectedModelId,
|
|
onSelect,
|
|
onConfigure,
|
|
disabled = false,
|
|
showUnvalidatedModels = false,
|
|
}: ModelSelectorProps) {
|
|
const dict = useDictionary()
|
|
const [open, setOpen] = useState(false)
|
|
// Filter models based on showUnvalidatedModels setting
|
|
const displayModels = useMemo(() => {
|
|
if (showUnvalidatedModels) {
|
|
return models
|
|
}
|
|
return models.filter((m) => m.validated === true)
|
|
}, [models, showUnvalidatedModels])
|
|
const groupedModels = useMemo(
|
|
() => groupModelsByProvider(displayModels),
|
|
[displayModels],
|
|
)
|
|
|
|
// Find selected model for display
|
|
const selectedModel = useMemo(
|
|
() => models.find((m) => m.id === selectedModelId),
|
|
[models, selectedModelId],
|
|
)
|
|
|
|
const handleSelect = (value: string) => {
|
|
if (value === "__configure__") {
|
|
onConfigure()
|
|
} else if (value === "__server_default__") {
|
|
onSelect(undefined)
|
|
} else {
|
|
onSelect(value)
|
|
}
|
|
setOpen(false)
|
|
}
|
|
|
|
const tooltipContent = selectedModel
|
|
? `${selectedModel.modelId} ${dict.modelConfig.clickToChange}`
|
|
: `${dict.modelConfig.usingServerDefault} ${dict.modelConfig.clickToChange}`
|
|
|
|
return (
|
|
<ModelSelectorRoot open={open} onOpenChange={setOpen}>
|
|
<ModelSelectorTrigger asChild>
|
|
<ButtonWithTooltip
|
|
tooltipContent={tooltipContent}
|
|
variant="ghost"
|
|
size="sm"
|
|
disabled={disabled}
|
|
className="hover:bg-accent gap-1.5 h-8 max-w-[180px] px-2"
|
|
>
|
|
<Bot className="h-4 w-4 flex-shrink-0 text-muted-foreground" />
|
|
<span className="text-xs truncate">
|
|
{selectedModel
|
|
? selectedModel.modelId
|
|
: dict.modelConfig.default}
|
|
</span>
|
|
<ChevronDown className="h-3 w-3 flex-shrink-0 text-muted-foreground" />
|
|
</ButtonWithTooltip>
|
|
</ModelSelectorTrigger>
|
|
<ModelSelectorContent title={dict.modelConfig.selectModel}>
|
|
<ModelSelectorInput
|
|
placeholder={dict.modelConfig.searchModels}
|
|
/>
|
|
<ModelSelectorList className="[&::-webkit-scrollbar]:hidden [-ms-overflow-style:none] [scrollbar-width:none]">
|
|
<ModelSelectorEmpty>
|
|
{displayModels.length === 0 && models.length > 0
|
|
? dict.modelConfig.noVerifiedModels
|
|
: dict.modelConfig.noModelsFound}
|
|
</ModelSelectorEmpty>
|
|
|
|
{/* Server Default Option */}
|
|
<ModelSelectorGroup heading={dict.modelConfig.default}>
|
|
<ModelSelectorItem
|
|
value="__server_default__"
|
|
onSelect={handleSelect}
|
|
className={cn(
|
|
"cursor-pointer",
|
|
!selectedModelId && "bg-accent",
|
|
)}
|
|
>
|
|
<Check
|
|
className={cn(
|
|
"mr-2 h-4 w-4",
|
|
!selectedModelId
|
|
? "opacity-100"
|
|
: "opacity-0",
|
|
)}
|
|
/>
|
|
<Server className="mr-2 h-4 w-4 text-muted-foreground" />
|
|
<ModelSelectorName>
|
|
{dict.modelConfig.serverDefault}
|
|
</ModelSelectorName>
|
|
</ModelSelectorItem>
|
|
</ModelSelectorGroup>
|
|
|
|
{/* Configured Models by Provider */}
|
|
{Array.from(groupedModels.entries()).map(
|
|
([
|
|
providerLabel,
|
|
{ provider, models: providerModels },
|
|
]) => (
|
|
<ModelSelectorGroup
|
|
key={providerLabel}
|
|
heading={providerLabel}
|
|
>
|
|
{providerModels.map((model) => (
|
|
<ModelSelectorItem
|
|
key={model.id}
|
|
value={model.modelId}
|
|
onSelect={() => handleSelect(model.id)}
|
|
className="cursor-pointer"
|
|
>
|
|
<Check
|
|
className={cn(
|
|
"mr-2 h-4 w-4",
|
|
selectedModelId === model.id
|
|
? "opacity-100"
|
|
: "opacity-0",
|
|
)}
|
|
/>
|
|
<ModelSelectorLogo
|
|
provider={
|
|
PROVIDER_LOGO_MAP[provider] ||
|
|
provider
|
|
}
|
|
className="mr-2"
|
|
/>
|
|
<ModelSelectorName>
|
|
{model.modelId}
|
|
</ModelSelectorName>
|
|
{model.validated !== true && (
|
|
<span
|
|
title={
|
|
dict.modelConfig
|
|
.unvalidatedModelWarning
|
|
}
|
|
>
|
|
<AlertTriangle className="ml-auto h-3 w-3 text-warning" />
|
|
</span>
|
|
)}
|
|
</ModelSelectorItem>
|
|
))}
|
|
</ModelSelectorGroup>
|
|
),
|
|
)}
|
|
|
|
{/* Configure Option */}
|
|
<ModelSelectorSeparator />
|
|
<ModelSelectorGroup>
|
|
<ModelSelectorItem
|
|
value="__configure__"
|
|
onSelect={handleSelect}
|
|
className="cursor-pointer"
|
|
>
|
|
<Settings2 className="mr-2 h-4 w-4" />
|
|
<ModelSelectorName>
|
|
{dict.modelConfig.configureModels}
|
|
</ModelSelectorName>
|
|
</ModelSelectorItem>
|
|
</ModelSelectorGroup>
|
|
{/* Info text */}
|
|
<div className="px-3 py-2 text-xs text-muted-foreground border-t">
|
|
{showUnvalidatedModels
|
|
? dict.modelConfig.allModelsShown
|
|
: dict.modelConfig.onlyVerifiedShown}
|
|
</div>
|
|
</ModelSelectorList>
|
|
</ModelSelectorContent>
|
|
</ModelSelectorRoot>
|
|
)
|
|
}
|