mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-01-02 22:32:27 +08:00
* feat: add multi-provider model configuration - Add model config dialog for managing multiple AI providers - Support for OpenAI, Anthropic, Google, Azure, Bedrock, OpenRouter, DeepSeek, SiliconFlow, Ollama, and AI Gateway - Add model selector dropdown in chat panel header - Add API key validation endpoint - Add custom model ID input with keyboard navigation - Fix hover highlight in Command component - Add suggested models for each provider including latest Claude 4.5 series - Store configuration locally in browser * feat: improve model config UI and move selector to chat input - Move model selector from header to chat input (left of send button) - Add per-model validation status (queued, running, valid, invalid) - Filter model selector to only show verified models - Add editable model IDs in config dialog - Add custom model input field alongside suggested models dropdown - Fix hover states on provider buttons and select triggers - Update OpenAI suggested models with GPT-5 series - Add alert-dialog component for delete confirmation * refactor: revert shadcn component changes, apply hover fix at usage site * feat: add AWS credentials support for Bedrock provider - Add AWS Access Key ID, Secret Access Key, Region fields for Bedrock - Show different credential fields based on provider type - Update validation API to handle Bedrock with AWS credentials - Add region selector with common AWS regions * fix: reset Test button after validation completes * fix: reset validation button to Test after success * fix: complete bedrock support and UI/UX improvements - Add bedrock to ALLOWED_CLIENT_PROVIDERS for client credentials - Pass AWS credentials through full chain (headers → API → provider) - Replace non-existent GPT-5 models with real ones (o1, o3-mini) - Add accessibility: aria-labels, focus-visible rings, inline errors - Add more AWS regions (Ohio, London, Paris, Mumbai, Seoul, São Paulo) - Fix setTimeout cleanup with useRef on component unmount - Fix TypeScript type consistency in getSelectedAIConfig fallback * chore: remove unused code - Remove unused setAccessCodeRequired state in chat-panel.tsx - Remove unused getSelectedModel export in model-config.ts * fix: UI/UX improvements for model configuration dialog - Add gradient header styling with icon badge - Change Configuration section icon from Key to Settings2 - Add duplicate model detection with warning banner and inline removal - Filter out already-added models from suggestions dropdown - Add type-to-confirm for deleting providers with 3+ models - Enhance delete confirmation dialog with warning icon - Improve model selector discoverability (show model name + chevron) - Add truncation for long model names with title tooltip - Remove AI provider settings from Settings dialog (now in Model Config) - Extract ValidationButton into reusable component * fix: prevent duplicate model IDs within same provider - Block adding model if ID already exists in provider - Block editing model ID to match existing model in provider * fix: improve duplicate model ID notifications - Add toast notification when trying to add duplicate model - Allow free typing when editing model ID, validate on blur - Show warning toast instead of blocking input * fix: improve duplicate model validation UX in config dialog - Add inline error display for duplicate model IDs - Show red border on input when error exists - Validate on blur with shake animation for edit errors - Prevent saving empty model names - Clear errors when user starts typing - Simplify error styling (small red text, no heavy chips)
217 lines
8.2 KiB
TypeScript
217 lines
8.2 KiB
TypeScript
"use client"
|
|
|
|
import { 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 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
|
|
}
|
|
|
|
// 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,
|
|
}: ModelSelectorProps) {
|
|
const [open, setOpen] = useState(false)
|
|
// Only show validated models in the selector
|
|
const validatedModels = useMemo(
|
|
() => models.filter((m) => m.validated === true),
|
|
[models],
|
|
)
|
|
const groupedModels = useMemo(
|
|
() => groupModelsByProvider(validatedModels),
|
|
[validatedModels],
|
|
)
|
|
|
|
// 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} (click to change)`
|
|
: "Using server default model (click to change)"
|
|
|
|
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 : "Default"}
|
|
</span>
|
|
<ChevronDown className="h-3 w-3 flex-shrink-0 text-muted-foreground" />
|
|
</ButtonWithTooltip>
|
|
</ModelSelectorTrigger>
|
|
<ModelSelectorContent title="Select Model">
|
|
<ModelSelectorInput placeholder="Search models..." />
|
|
<ModelSelectorList>
|
|
<ModelSelectorEmpty>
|
|
{validatedModels.length === 0 && models.length > 0
|
|
? "No verified models. Test your models first."
|
|
: "No models found."}
|
|
</ModelSelectorEmpty>
|
|
|
|
{/* Server Default Option */}
|
|
<ModelSelectorGroup heading="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>
|
|
Server Default
|
|
</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>
|
|
</ModelSelectorItem>
|
|
))}
|
|
</ModelSelectorGroup>
|
|
),
|
|
)}
|
|
|
|
{/* Configure Option */}
|
|
<ModelSelectorSeparator />
|
|
<ModelSelectorGroup>
|
|
<ModelSelectorItem
|
|
value="__configure__"
|
|
onSelect={handleSelect}
|
|
className="cursor-pointer"
|
|
>
|
|
<Settings2 className="mr-2 h-4 w-4" />
|
|
<ModelSelectorName>
|
|
Configure Models...
|
|
</ModelSelectorName>
|
|
</ModelSelectorItem>
|
|
</ModelSelectorGroup>
|
|
{/* Info text */}
|
|
<div className="px-3 py-2 text-xs text-muted-foreground border-t">
|
|
Only verified models are shown
|
|
</div>
|
|
</ModelSelectorList>
|
|
</ModelSelectorContent>
|
|
</ModelSelectorRoot>
|
|
)
|
|
}
|