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
This commit is contained in:
180
app/api/validate-model/route.ts
Normal file
180
app/api/validate-model/route.ts
Normal file
@@ -0,0 +1,180 @@
|
||||
import { createAnthropic } from "@ai-sdk/anthropic"
|
||||
import { createDeepSeek, deepseek } from "@ai-sdk/deepseek"
|
||||
import { createGateway } from "@ai-sdk/gateway"
|
||||
import { createGoogleGenerativeAI } from "@ai-sdk/google"
|
||||
import { createOpenAI } from "@ai-sdk/openai"
|
||||
import { createOpenRouter } from "@openrouter/ai-sdk-provider"
|
||||
import { generateText } from "ai"
|
||||
import { NextResponse } from "next/server"
|
||||
import { createOllama } from "ollama-ai-provider-v2"
|
||||
|
||||
export const runtime = "nodejs"
|
||||
|
||||
interface ValidateRequest {
|
||||
provider: string
|
||||
apiKey: string
|
||||
baseUrl?: string
|
||||
modelId: string
|
||||
}
|
||||
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
const body: ValidateRequest = await req.json()
|
||||
const { provider, apiKey, baseUrl, modelId } = body
|
||||
|
||||
if (!provider || !modelId) {
|
||||
return NextResponse.json(
|
||||
{ valid: false, error: "Provider and model ID are required" },
|
||||
{ status: 400 },
|
||||
)
|
||||
}
|
||||
|
||||
// Ollama doesn't require API key
|
||||
if (provider !== "ollama" && !apiKey) {
|
||||
return NextResponse.json(
|
||||
{ valid: false, error: "API key is required" },
|
||||
{ status: 400 },
|
||||
)
|
||||
}
|
||||
|
||||
let model: any
|
||||
|
||||
switch (provider) {
|
||||
case "openai": {
|
||||
const openai = createOpenAI({
|
||||
apiKey,
|
||||
...(baseUrl && { baseURL: baseUrl }),
|
||||
})
|
||||
model = openai.chat(modelId)
|
||||
break
|
||||
}
|
||||
|
||||
case "anthropic": {
|
||||
const anthropic = createAnthropic({
|
||||
apiKey,
|
||||
baseURL: baseUrl || "https://api.anthropic.com/v1",
|
||||
})
|
||||
model = anthropic(modelId)
|
||||
break
|
||||
}
|
||||
|
||||
case "google": {
|
||||
const google = createGoogleGenerativeAI({
|
||||
apiKey,
|
||||
...(baseUrl && { baseURL: baseUrl }),
|
||||
})
|
||||
model = google(modelId)
|
||||
break
|
||||
}
|
||||
|
||||
case "azure": {
|
||||
const azure = createOpenAI({
|
||||
apiKey,
|
||||
baseURL: baseUrl,
|
||||
})
|
||||
model = azure.chat(modelId)
|
||||
break
|
||||
}
|
||||
|
||||
case "openrouter": {
|
||||
const openrouter = createOpenRouter({
|
||||
apiKey,
|
||||
...(baseUrl && { baseURL: baseUrl }),
|
||||
})
|
||||
model = openrouter(modelId)
|
||||
break
|
||||
}
|
||||
|
||||
case "deepseek": {
|
||||
if (baseUrl || apiKey) {
|
||||
const ds = createDeepSeek({
|
||||
apiKey,
|
||||
...(baseUrl && { baseURL: baseUrl }),
|
||||
})
|
||||
model = ds(modelId)
|
||||
} else {
|
||||
model = deepseek(modelId)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case "siliconflow": {
|
||||
const sf = createOpenAI({
|
||||
apiKey,
|
||||
baseURL: baseUrl || "https://api.siliconflow.com/v1",
|
||||
})
|
||||
model = sf.chat(modelId)
|
||||
break
|
||||
}
|
||||
|
||||
case "ollama": {
|
||||
const ollama = createOllama({
|
||||
baseURL: baseUrl || "http://localhost:11434",
|
||||
})
|
||||
model = ollama(modelId)
|
||||
break
|
||||
}
|
||||
|
||||
case "gateway": {
|
||||
const gw = createGateway({
|
||||
apiKey,
|
||||
...(baseUrl && { baseURL: baseUrl }),
|
||||
})
|
||||
model = gw(modelId)
|
||||
break
|
||||
}
|
||||
|
||||
default:
|
||||
return NextResponse.json(
|
||||
{ valid: false, error: `Unknown provider: ${provider}` },
|
||||
{ status: 400 },
|
||||
)
|
||||
}
|
||||
|
||||
// Make a minimal test request
|
||||
const startTime = Date.now()
|
||||
await generateText({
|
||||
model,
|
||||
prompt: "Say 'OK'",
|
||||
maxOutputTokens: 20,
|
||||
})
|
||||
const responseTime = Date.now() - startTime
|
||||
|
||||
return NextResponse.json({
|
||||
valid: true,
|
||||
responseTime,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("[validate-model] Error:", error)
|
||||
|
||||
let errorMessage = "Validation failed"
|
||||
if (error instanceof Error) {
|
||||
// Extract meaningful error message
|
||||
if (
|
||||
error.message.includes("401") ||
|
||||
error.message.includes("Unauthorized")
|
||||
) {
|
||||
errorMessage = "Invalid API key"
|
||||
} else if (
|
||||
error.message.includes("404") ||
|
||||
error.message.includes("not found")
|
||||
) {
|
||||
errorMessage = "Model not found"
|
||||
} else if (
|
||||
error.message.includes("429") ||
|
||||
error.message.includes("rate limit")
|
||||
) {
|
||||
errorMessage = "Rate limited - try again later"
|
||||
} else if (error.message.includes("ECONNREFUSED")) {
|
||||
errorMessage = "Cannot connect to server"
|
||||
} else {
|
||||
errorMessage = error.message.slice(0, 100)
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{ valid: false, error: errorMessage },
|
||||
{ status: 200 }, // Return 200 so client can read error message
|
||||
)
|
||||
}
|
||||
}
|
||||
148
components/ai-elements/model-selector.tsx
Normal file
148
components/ai-elements/model-selector.tsx
Normal file
@@ -0,0 +1,148 @@
|
||||
import type { ComponentProps, ReactNode } from "react"
|
||||
import {
|
||||
Command,
|
||||
CommandDialog,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
CommandSeparator,
|
||||
CommandShortcut,
|
||||
} from "@/components/ui/command"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
export type ModelSelectorProps = ComponentProps<typeof Dialog>
|
||||
|
||||
export const ModelSelector = (props: ModelSelectorProps) => (
|
||||
<Dialog {...props} />
|
||||
)
|
||||
|
||||
export type ModelSelectorTriggerProps = ComponentProps<typeof DialogTrigger>
|
||||
|
||||
export const ModelSelectorTrigger = (props: ModelSelectorTriggerProps) => (
|
||||
<DialogTrigger {...props} />
|
||||
)
|
||||
|
||||
export type ModelSelectorContentProps = ComponentProps<typeof DialogContent> & {
|
||||
title?: ReactNode
|
||||
}
|
||||
|
||||
export const ModelSelectorContent = ({
|
||||
className,
|
||||
children,
|
||||
title = "Model Selector",
|
||||
...props
|
||||
}: ModelSelectorContentProps) => (
|
||||
<DialogContent className={cn("p-0", className)} {...props}>
|
||||
<DialogTitle className="sr-only">{title}</DialogTitle>
|
||||
<Command className="**:data-[slot=command-input-wrapper]:h-auto">
|
||||
{children}
|
||||
</Command>
|
||||
</DialogContent>
|
||||
)
|
||||
|
||||
export type ModelSelectorDialogProps = ComponentProps<typeof CommandDialog>
|
||||
|
||||
export const ModelSelectorDialog = (props: ModelSelectorDialogProps) => (
|
||||
<CommandDialog {...props} />
|
||||
)
|
||||
|
||||
export type ModelSelectorInputProps = ComponentProps<typeof CommandInput>
|
||||
|
||||
export const ModelSelectorInput = ({
|
||||
className,
|
||||
...props
|
||||
}: ModelSelectorInputProps) => (
|
||||
<CommandInput className={cn("h-auto py-3.5", className)} {...props} />
|
||||
)
|
||||
|
||||
export type ModelSelectorListProps = ComponentProps<typeof CommandList>
|
||||
|
||||
export const ModelSelectorList = (props: ModelSelectorListProps) => (
|
||||
<CommandList {...props} />
|
||||
)
|
||||
|
||||
export type ModelSelectorEmptyProps = ComponentProps<typeof CommandEmpty>
|
||||
|
||||
export const ModelSelectorEmpty = (props: ModelSelectorEmptyProps) => (
|
||||
<CommandEmpty {...props} />
|
||||
)
|
||||
|
||||
export type ModelSelectorGroupProps = ComponentProps<typeof CommandGroup>
|
||||
|
||||
export const ModelSelectorGroup = (props: ModelSelectorGroupProps) => (
|
||||
<CommandGroup {...props} />
|
||||
)
|
||||
|
||||
export type ModelSelectorItemProps = ComponentProps<typeof CommandItem>
|
||||
|
||||
export const ModelSelectorItem = (props: ModelSelectorItemProps) => (
|
||||
<CommandItem {...props} />
|
||||
)
|
||||
|
||||
export type ModelSelectorShortcutProps = ComponentProps<typeof CommandShortcut>
|
||||
|
||||
export const ModelSelectorShortcut = (props: ModelSelectorShortcutProps) => (
|
||||
<CommandShortcut {...props} />
|
||||
)
|
||||
|
||||
export type ModelSelectorSeparatorProps = ComponentProps<
|
||||
typeof CommandSeparator
|
||||
>
|
||||
|
||||
export const ModelSelectorSeparator = (props: ModelSelectorSeparatorProps) => (
|
||||
<CommandSeparator {...props} />
|
||||
)
|
||||
|
||||
export type ModelSelectorLogoProps = Omit<
|
||||
ComponentProps<"img">,
|
||||
"src" | "alt"
|
||||
> & {
|
||||
provider: string
|
||||
}
|
||||
|
||||
export const ModelSelectorLogo = ({
|
||||
provider,
|
||||
className,
|
||||
...props
|
||||
}: ModelSelectorLogoProps) => (
|
||||
<img
|
||||
{...props}
|
||||
alt={`${provider} logo`}
|
||||
className={cn("size-4 dark:invert", className)}
|
||||
height={16}
|
||||
src={`https://models.dev/logos/${provider}.svg`}
|
||||
width={16}
|
||||
/>
|
||||
)
|
||||
|
||||
export type ModelSelectorLogoGroupProps = ComponentProps<"div">
|
||||
|
||||
export const ModelSelectorLogoGroup = ({
|
||||
className,
|
||||
...props
|
||||
}: ModelSelectorLogoGroupProps) => (
|
||||
<div
|
||||
className={cn(
|
||||
"-space-x-1 flex shrink-0 items-center [&>img]:rounded-full [&>img]:bg-background [&>img]:p-px [&>img]:ring-1 dark:[&>img]:bg-foreground",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
||||
export type ModelSelectorNameProps = ComponentProps<"span">
|
||||
|
||||
export const ModelSelectorName = ({
|
||||
className,
|
||||
...props
|
||||
}: ModelSelectorNameProps) => (
|
||||
<span className={cn("flex-1 truncate text-left", className)} {...props} />
|
||||
)
|
||||
@@ -18,11 +18,13 @@ import { FaGithub } from "react-icons/fa"
|
||||
import { Toaster, toast } from "sonner"
|
||||
import { ButtonWithTooltip } from "@/components/button-with-tooltip"
|
||||
import { ChatInput } from "@/components/chat-input"
|
||||
import { ModelConfigDialog } from "@/components/model-config-dialog"
|
||||
import { ModelSelector } from "@/components/model-selector"
|
||||
import { ResetWarningModal } from "@/components/reset-warning-modal"
|
||||
import { SettingsDialog } from "@/components/settings-dialog"
|
||||
import { useDiagram } from "@/contexts/diagram-context"
|
||||
import { useDictionary } from "@/hooks/use-dictionary"
|
||||
import { getAIConfig } from "@/lib/ai-config"
|
||||
import { getSelectedAIConfig, useModelConfig } from "@/hooks/use-model-config"
|
||||
import { findCachedResponse } from "@/lib/cached-responses"
|
||||
import { isPdfFile, isTextFile } from "@/lib/pdf-utils"
|
||||
import { type FileData, useFileProcessor } from "@/lib/use-file-processor"
|
||||
@@ -146,7 +148,11 @@ export default function ChatPanel({
|
||||
|
||||
const [showHistory, setShowHistory] = useState(false)
|
||||
const [showSettingsDialog, setShowSettingsDialog] = useState(false)
|
||||
const [showModelConfigDialog, setShowModelConfigDialog] = useState(false)
|
||||
const [, setAccessCodeRequired] = useState(false)
|
||||
|
||||
// Model configuration hook
|
||||
const modelConfig = useModelConfig()
|
||||
const [input, setInput] = useState("")
|
||||
const [dailyRequestLimit, setDailyRequestLimit] = useState(0)
|
||||
const [dailyTokenLimit, setDailyTokenLimit] = useState(0)
|
||||
@@ -1019,7 +1025,7 @@ Continue from EXACTLY where you stopped.`,
|
||||
autoRetryCountRef.current = 0
|
||||
partialXmlRef.current = ""
|
||||
|
||||
const config = getAIConfig()
|
||||
const config = getSelectedAIConfig()
|
||||
|
||||
sendMessage(
|
||||
{ parts },
|
||||
@@ -1298,6 +1304,16 @@ Continue from EXACTLY where you stopped.`,
|
||||
className={`${isMobile ? "w-4 h-4" : "w-5 h-5"}`}
|
||||
/>
|
||||
</a>
|
||||
{!isMobile && modelConfig.isLoaded && (
|
||||
<ModelSelector
|
||||
models={modelConfig.models}
|
||||
selectedModelId={modelConfig.selectedModelId}
|
||||
onSelect={modelConfig.setSelectedModelId}
|
||||
onConfigure={() =>
|
||||
setShowModelConfigDialog(true)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<ButtonWithTooltip
|
||||
tooltipContent={dict.nav.settings}
|
||||
variant="ghost"
|
||||
@@ -1368,12 +1384,19 @@ Continue from EXACTLY where you stopped.`,
|
||||
open={showSettingsDialog}
|
||||
onOpenChange={setShowSettingsDialog}
|
||||
onCloseProtectionChange={onCloseProtectionChange}
|
||||
onOpenModelConfig={() => setShowModelConfigDialog(true)}
|
||||
drawioUi={drawioUi}
|
||||
onToggleDrawioUi={onToggleDrawioUi}
|
||||
darkMode={darkMode}
|
||||
onToggleDarkMode={onToggleDarkMode}
|
||||
/>
|
||||
|
||||
<ModelConfigDialog
|
||||
open={showModelConfigDialog}
|
||||
onOpenChange={setShowModelConfigDialog}
|
||||
modelConfig={modelConfig}
|
||||
/>
|
||||
|
||||
<ResetWarningModal
|
||||
open={showNewChatDialog}
|
||||
onOpenChange={setShowNewChatDialog}
|
||||
|
||||
607
components/model-config-dialog.tsx
Normal file
607
components/model-config-dialog.tsx
Normal file
@@ -0,0 +1,607 @@
|
||||
"use client"
|
||||
|
||||
import { Check, Eye, EyeOff, Loader2, Plus, Trash2, X } from "lucide-react"
|
||||
import { useCallback, useState } from "react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/components/ui/command"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover"
|
||||
import { ScrollArea } from "@/components/ui/scroll-area"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select"
|
||||
import { Switch } from "@/components/ui/switch"
|
||||
import { useDictionary } from "@/hooks/use-dictionary"
|
||||
import type { UseModelConfigReturn } from "@/hooks/use-model-config"
|
||||
import type {
|
||||
ModelConfig,
|
||||
ProviderConfig,
|
||||
ProviderName,
|
||||
} from "@/lib/types/model-config"
|
||||
import { PROVIDER_INFO, SUGGESTED_MODELS } from "@/lib/types/model-config"
|
||||
|
||||
interface ModelConfigDialogProps {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
modelConfig: UseModelConfigReturn
|
||||
}
|
||||
|
||||
type ValidationStatus = "idle" | "validating" | "success" | "error"
|
||||
|
||||
export function ModelConfigDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
modelConfig,
|
||||
}: ModelConfigDialogProps) {
|
||||
const dict = useDictionary()
|
||||
const [selectedProviderId, setSelectedProviderId] = useState<string | null>(
|
||||
null,
|
||||
)
|
||||
const [showApiKey, setShowApiKey] = useState(false)
|
||||
const [validationStatus, setValidationStatus] =
|
||||
useState<ValidationStatus>("idle")
|
||||
const [validationError, setValidationError] = useState<string>("")
|
||||
const [modelPopoverOpen, setModelPopoverOpen] = useState(false)
|
||||
const [modelSearchValue, setModelSearchValue] = useState("")
|
||||
|
||||
const {
|
||||
config,
|
||||
addProvider,
|
||||
updateProvider,
|
||||
deleteProvider,
|
||||
addModel,
|
||||
updateModel,
|
||||
deleteModel,
|
||||
} = modelConfig
|
||||
|
||||
// Get selected provider
|
||||
const selectedProvider = config.providers.find(
|
||||
(p) => p.id === selectedProviderId,
|
||||
)
|
||||
|
||||
// Get suggested models for current provider
|
||||
const suggestedModels = selectedProvider
|
||||
? SUGGESTED_MODELS[selectedProvider.provider] || []
|
||||
: []
|
||||
|
||||
// Handle adding a new provider
|
||||
const handleAddProvider = (providerType: ProviderName) => {
|
||||
const newProvider = addProvider(providerType)
|
||||
setSelectedProviderId(newProvider.id)
|
||||
setValidationStatus("idle")
|
||||
}
|
||||
|
||||
// Handle provider field updates
|
||||
const handleProviderUpdate = (
|
||||
field: keyof ProviderConfig,
|
||||
value: string | boolean,
|
||||
) => {
|
||||
if (!selectedProviderId) return
|
||||
updateProvider(selectedProviderId, { [field]: value })
|
||||
// Reset validation when API key or base URL changes
|
||||
if (field === "apiKey" || field === "baseUrl") {
|
||||
setValidationStatus("idle")
|
||||
updateProvider(selectedProviderId, { validated: false })
|
||||
}
|
||||
}
|
||||
|
||||
// Handle adding a model to current provider
|
||||
const handleAddModel = (modelId: string) => {
|
||||
if (!selectedProviderId) return
|
||||
addModel(selectedProviderId, modelId)
|
||||
}
|
||||
|
||||
// Handle model field updates
|
||||
const handleModelUpdate = (
|
||||
modelConfigId: string,
|
||||
field: keyof ModelConfig,
|
||||
value: string | boolean,
|
||||
) => {
|
||||
if (!selectedProviderId) return
|
||||
updateModel(selectedProviderId, modelConfigId, { [field]: value })
|
||||
}
|
||||
|
||||
// Handle deleting a model
|
||||
const handleDeleteModel = (modelConfigId: string) => {
|
||||
if (!selectedProviderId) return
|
||||
deleteModel(selectedProviderId, modelConfigId)
|
||||
}
|
||||
|
||||
// Handle deleting the provider
|
||||
const handleDeleteProvider = () => {
|
||||
if (!selectedProviderId) return
|
||||
deleteProvider(selectedProviderId)
|
||||
setSelectedProviderId(null)
|
||||
setValidationStatus("idle")
|
||||
}
|
||||
|
||||
// Validate API key
|
||||
const handleValidate = useCallback(async () => {
|
||||
if (!selectedProvider || !selectedProvider.apiKey) return
|
||||
|
||||
// Need at least one model to validate
|
||||
if (selectedProvider.models.length === 0) {
|
||||
setValidationError("Add at least one model to validate")
|
||||
setValidationStatus("error")
|
||||
return
|
||||
}
|
||||
|
||||
setValidationStatus("validating")
|
||||
setValidationError("")
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/validate-model", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
provider: selectedProvider.provider,
|
||||
apiKey: selectedProvider.apiKey,
|
||||
baseUrl: selectedProvider.baseUrl,
|
||||
modelId: selectedProvider.models[0].modelId,
|
||||
}),
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (data.valid) {
|
||||
setValidationStatus("success")
|
||||
updateProvider(selectedProviderId!, { validated: true })
|
||||
} else {
|
||||
setValidationStatus("error")
|
||||
setValidationError(data.error || "Validation failed")
|
||||
}
|
||||
} catch {
|
||||
setValidationStatus("error")
|
||||
setValidationError("Network error")
|
||||
}
|
||||
}, [selectedProvider, selectedProviderId, updateProvider])
|
||||
|
||||
// Get all available provider types (allow duplicates for different base URLs)
|
||||
const availableProviders = Object.keys(PROVIDER_INFO) as ProviderName[]
|
||||
|
||||
// Get display name for provider (use custom name if set)
|
||||
const getProviderDisplayName = (provider: ProviderConfig) => {
|
||||
return provider.name || PROVIDER_INFO[provider.provider].label
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-2xl max-h-[85vh] overflow-hidden flex flex-col">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{dict.modelConfig?.title || "AI Model Configuration"}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{dict.modelConfig?.description ||
|
||||
"Configure multiple AI providers and models"}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex flex-1 gap-4 min-h-0 overflow-hidden">
|
||||
{/* Provider List (Left Sidebar) */}
|
||||
<div className="w-48 flex-shrink-0 flex flex-col gap-2">
|
||||
<Label className="text-xs text-muted-foreground uppercase tracking-wide">
|
||||
Providers
|
||||
</Label>
|
||||
|
||||
<ScrollArea className="flex-1">
|
||||
<div className="flex flex-col gap-1 pr-2">
|
||||
{config.providers.map((provider) => (
|
||||
<button
|
||||
key={provider.id}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setSelectedProviderId(provider.id)
|
||||
setValidationStatus(
|
||||
provider.validated
|
||||
? "success"
|
||||
: "idle",
|
||||
)
|
||||
setShowApiKey(false)
|
||||
}}
|
||||
className={`flex items-center gap-2 px-3 py-2 rounded-md text-left text-sm transition-colors ${
|
||||
selectedProviderId === provider.id
|
||||
? "bg-accent text-accent-foreground"
|
||||
: "hover:bg-accent/50"
|
||||
}`}
|
||||
>
|
||||
<span className="flex-1 truncate">
|
||||
{getProviderDisplayName(provider)}
|
||||
</span>
|
||||
{provider.validated && (
|
||||
<Check className="h-3.5 w-3.5 text-green-500" />
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
{/* Add Provider */}
|
||||
{availableProviders.length > 0 && (
|
||||
<Select
|
||||
onValueChange={(v) =>
|
||||
handleAddProvider(v as ProviderName)
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="h-8 text-xs">
|
||||
<Plus className="h-3.5 w-3.5 mr-1" />
|
||||
<SelectValue placeholder="Add Provider" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{availableProviders.map((p) => (
|
||||
<SelectItem key={p} value={p}>
|
||||
{PROVIDER_INFO[p].label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Provider Details (Right Panel) */}
|
||||
<ScrollArea className="flex-1">
|
||||
{selectedProvider ? (
|
||||
<div className="space-y-4 pr-3">
|
||||
{/* Provider Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="font-medium">
|
||||
{
|
||||
PROVIDER_INFO[
|
||||
selectedProvider.provider
|
||||
].label
|
||||
}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
{/* Provider Name */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="provider-name">
|
||||
Display Name
|
||||
</Label>
|
||||
<Input
|
||||
id="provider-name"
|
||||
value={selectedProvider.name || ""}
|
||||
onChange={(e) =>
|
||||
handleProviderUpdate(
|
||||
"name",
|
||||
e.target.value,
|
||||
)
|
||||
}
|
||||
placeholder={
|
||||
PROVIDER_INFO[
|
||||
selectedProvider.provider
|
||||
].label
|
||||
}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Custom name to identify this provider
|
||||
(e.g., "OpenAI Production")
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* API Key */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="api-key">API Key</Label>
|
||||
<div className="flex gap-2">
|
||||
<div className="relative flex-1">
|
||||
<Input
|
||||
id="api-key"
|
||||
type={
|
||||
showApiKey
|
||||
? "text"
|
||||
: "password"
|
||||
}
|
||||
value={selectedProvider.apiKey}
|
||||
onChange={(e) =>
|
||||
handleProviderUpdate(
|
||||
"apiKey",
|
||||
e.target.value,
|
||||
)
|
||||
}
|
||||
placeholder="Enter API key"
|
||||
className="pr-10"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
setShowApiKey(!showApiKey)
|
||||
}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
{showApiKey ? (
|
||||
<EyeOff className="h-4 w-4" />
|
||||
) : (
|
||||
<Eye className="h-4 w-4" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleValidate}
|
||||
disabled={
|
||||
!selectedProvider.apiKey ||
|
||||
validationStatus ===
|
||||
"validating"
|
||||
}
|
||||
>
|
||||
{validationStatus ===
|
||||
"validating" ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : validationStatus ===
|
||||
"success" ? (
|
||||
<Check className="h-4 w-4 text-green-500" />
|
||||
) : (
|
||||
"Test"
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
{validationStatus === "error" &&
|
||||
validationError && (
|
||||
<p className="text-xs text-destructive">
|
||||
{validationError}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Base URL */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="base-url">
|
||||
Base URL (optional)
|
||||
</Label>
|
||||
<Input
|
||||
id="base-url"
|
||||
value={selectedProvider.baseUrl || ""}
|
||||
onChange={(e) =>
|
||||
handleProviderUpdate(
|
||||
"baseUrl",
|
||||
e.target.value,
|
||||
)
|
||||
}
|
||||
placeholder={
|
||||
PROVIDER_INFO[
|
||||
selectedProvider.provider
|
||||
].defaultBaseUrl ||
|
||||
"Custom endpoint URL"
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Models Section */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label>Models</Label>
|
||||
<Popover
|
||||
open={modelPopoverOpen}
|
||||
onOpenChange={(open) => {
|
||||
setModelPopoverOpen(open)
|
||||
if (!open)
|
||||
setModelSearchValue("")
|
||||
}}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-7 text-xs"
|
||||
>
|
||||
<Plus className="h-3 w-3 mr-1" />
|
||||
Add Model
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="w-72 p-0 z-[60]"
|
||||
align="end"
|
||||
>
|
||||
<Command shouldFilter={true}>
|
||||
<CommandInput
|
||||
placeholder="Search or type custom model..."
|
||||
value={modelSearchValue}
|
||||
onValueChange={
|
||||
setModelSearchValue
|
||||
}
|
||||
/>
|
||||
<CommandList>
|
||||
<CommandEmpty>
|
||||
<span className="text-muted-foreground">
|
||||
{modelSearchValue.trim()
|
||||
? "Press Enter to add custom model"
|
||||
: "Type a model ID..."}
|
||||
</span>
|
||||
</CommandEmpty>
|
||||
{/* Custom model option - appears when search doesn't match suggestions */}
|
||||
{modelSearchValue.trim() &&
|
||||
!suggestedModels.some(
|
||||
(m) =>
|
||||
m
|
||||
.toLowerCase()
|
||||
.includes(
|
||||
modelSearchValue.toLowerCase(),
|
||||
),
|
||||
) && (
|
||||
<CommandGroup heading="Custom">
|
||||
<CommandItem
|
||||
value={`custom-${modelSearchValue.trim()}`}
|
||||
onSelect={() => {
|
||||
handleAddModel(
|
||||
modelSearchValue.trim(),
|
||||
)
|
||||
setModelSearchValue(
|
||||
"",
|
||||
)
|
||||
setModelPopoverOpen(
|
||||
false,
|
||||
)
|
||||
}}
|
||||
className="text-xs cursor-pointer"
|
||||
>
|
||||
Add
|
||||
"
|
||||
{modelSearchValue.trim()}
|
||||
"
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
)}
|
||||
<CommandGroup heading="Suggested">
|
||||
{suggestedModels.map(
|
||||
(modelId) => (
|
||||
<CommandItem
|
||||
key={
|
||||
modelId
|
||||
}
|
||||
value={
|
||||
modelId
|
||||
}
|
||||
onSelect={() => {
|
||||
handleAddModel(
|
||||
modelId,
|
||||
)
|
||||
setModelSearchValue(
|
||||
"",
|
||||
)
|
||||
setModelPopoverOpen(
|
||||
false,
|
||||
)
|
||||
}}
|
||||
className="text-xs cursor-pointer"
|
||||
>
|
||||
{
|
||||
modelId
|
||||
}
|
||||
</CommandItem>
|
||||
),
|
||||
)}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
{/* Model List */}
|
||||
<div className="space-y-2">
|
||||
{selectedProvider.models.length ===
|
||||
0 ? (
|
||||
<p className="text-sm text-muted-foreground py-4 text-center">
|
||||
No models configured. Add a
|
||||
model to get started.
|
||||
</p>
|
||||
) : (
|
||||
selectedProvider.models.map(
|
||||
(model) => (
|
||||
<div
|
||||
key={model.id}
|
||||
className="flex items-center gap-2 p-2 rounded-md border bg-card"
|
||||
>
|
||||
<Input
|
||||
value={
|
||||
model.modelId
|
||||
}
|
||||
onChange={(e) =>
|
||||
handleModelUpdate(
|
||||
model.id,
|
||||
"modelId",
|
||||
e.target
|
||||
.value,
|
||||
)
|
||||
}
|
||||
placeholder="Model ID (e.g., gpt-4o)"
|
||||
className="h-8 text-xs flex-1"
|
||||
/>
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch
|
||||
checked={
|
||||
model.streaming !==
|
||||
false
|
||||
}
|
||||
onCheckedChange={(
|
||||
checked,
|
||||
) =>
|
||||
handleModelUpdate(
|
||||
model.id,
|
||||
"streaming",
|
||||
checked,
|
||||
)
|
||||
}
|
||||
/>
|
||||
<span className="text-xs text-muted-foreground w-12">
|
||||
Stream
|
||||
</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7"
|
||||
onClick={() =>
|
||||
handleDeleteModel(
|
||||
model.id,
|
||||
)
|
||||
}
|
||||
>
|
||||
<X className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Delete Provider */}
|
||||
<div className="pt-4 border-t">
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
onClick={handleDeleteProvider}
|
||||
className="w-full"
|
||||
>
|
||||
<Trash2 className="h-4 w-4 mr-2" />
|
||||
Delete Provider
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center h-full text-center text-muted-foreground">
|
||||
<p className="mb-2">
|
||||
Select a provider or add a new one
|
||||
</p>
|
||||
<p className="text-xs">
|
||||
Configure multiple AI providers and switch
|
||||
between them easily
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</ScrollArea>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="pt-4 border-t text-xs text-muted-foreground text-center">
|
||||
API keys are stored locally in your browser
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
193
components/model-selector.tsx
Normal file
193
components/model-selector.tsx
Normal file
@@ -0,0 +1,193 @@
|
||||
"use client"
|
||||
|
||||
import { Bot, Check, 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",
|
||||
ollama: "ollama",
|
||||
gateway: "openai", // fallback
|
||||
}
|
||||
|
||||
// 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)
|
||||
const groupedModels = useMemo(() => groupModelsByProvider(models), [models])
|
||||
|
||||
// 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
|
||||
? `Model: ${selectedModel.modelId}`
|
||||
: "Model: Server Default"
|
||||
|
||||
return (
|
||||
<ModelSelectorRoot open={open} onOpenChange={setOpen}>
|
||||
<ModelSelectorTrigger asChild>
|
||||
<ButtonWithTooltip
|
||||
tooltipContent={tooltipContent}
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
disabled={disabled}
|
||||
className="hover:bg-accent"
|
||||
>
|
||||
<Bot className="h-5 w-5 text-muted-foreground" />
|
||||
</ButtonWithTooltip>
|
||||
</ModelSelectorTrigger>
|
||||
<ModelSelectorContent title="Select Model">
|
||||
<ModelSelectorInput placeholder="Search models..." />
|
||||
<ModelSelectorList>
|
||||
<ModelSelectorEmpty>No models found.</ModelSelectorEmpty>
|
||||
|
||||
{/* Server Default Option */}
|
||||
<ModelSelectorGroup>
|
||||
<ModelSelectorItem
|
||||
value="__server_default__"
|
||||
onSelect={handleSelect}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-4 w-4",
|
||||
!selectedModelId
|
||||
? "opacity-100"
|
||||
: "opacity-0",
|
||||
)}
|
||||
/>
|
||||
<ModelSelectorName className="text-muted-foreground">
|
||||
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>
|
||||
</ModelSelectorList>
|
||||
</ModelSelectorContent>
|
||||
</ModelSelectorRoot>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client"
|
||||
|
||||
import { Moon, Sun } from "lucide-react"
|
||||
import { Moon, Settings2, Sun } from "lucide-react"
|
||||
import { useEffect, useState } from "react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
@@ -12,13 +12,6 @@ import {
|
||||
} from "@/components/ui/dialog"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select"
|
||||
import { Switch } from "@/components/ui/switch"
|
||||
import { useDictionary } from "@/hooks/use-dictionary"
|
||||
|
||||
@@ -26,6 +19,7 @@ interface SettingsDialogProps {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
onCloseProtectionChange?: (enabled: boolean) => void
|
||||
onOpenModelConfig: () => void
|
||||
drawioUi: "min" | "sketch"
|
||||
onToggleDrawioUi: () => void
|
||||
darkMode: boolean
|
||||
@@ -35,10 +29,6 @@ interface SettingsDialogProps {
|
||||
export const STORAGE_ACCESS_CODE_KEY = "next-ai-draw-io-access-code"
|
||||
export const STORAGE_CLOSE_PROTECTION_KEY = "next-ai-draw-io-close-protection"
|
||||
const STORAGE_ACCESS_CODE_REQUIRED_KEY = "next-ai-draw-io-access-code-required"
|
||||
export const STORAGE_AI_PROVIDER_KEY = "next-ai-draw-io-ai-provider"
|
||||
export const STORAGE_AI_BASE_URL_KEY = "next-ai-draw-io-ai-base-url"
|
||||
export const STORAGE_AI_API_KEY_KEY = "next-ai-draw-io-ai-api-key"
|
||||
export const STORAGE_AI_MODEL_KEY = "next-ai-draw-io-ai-model"
|
||||
|
||||
function getStoredAccessCodeRequired(): boolean | null {
|
||||
if (typeof window === "undefined") return null
|
||||
@@ -51,6 +41,7 @@ export function SettingsDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
onCloseProtectionChange,
|
||||
onOpenModelConfig,
|
||||
drawioUi,
|
||||
onToggleDrawioUi,
|
||||
darkMode,
|
||||
@@ -64,10 +55,6 @@ export function SettingsDialog({
|
||||
const [accessCodeRequired, setAccessCodeRequired] = useState(
|
||||
() => getStoredAccessCodeRequired() ?? false,
|
||||
)
|
||||
const [provider, setProvider] = useState("")
|
||||
const [baseUrl, setBaseUrl] = useState("")
|
||||
const [apiKey, setApiKey] = useState("")
|
||||
const [modelId, setModelId] = useState("")
|
||||
|
||||
useEffect(() => {
|
||||
// Only fetch if not cached in localStorage
|
||||
@@ -104,12 +91,6 @@ export function SettingsDialog({
|
||||
// Default to true if not set
|
||||
setCloseProtection(storedCloseProtection !== "false")
|
||||
|
||||
// Load AI provider settings
|
||||
setProvider(localStorage.getItem(STORAGE_AI_PROVIDER_KEY) || "")
|
||||
setBaseUrl(localStorage.getItem(STORAGE_AI_BASE_URL_KEY) || "")
|
||||
setApiKey(localStorage.getItem(STORAGE_AI_API_KEY_KEY) || "")
|
||||
setModelId(localStorage.getItem(STORAGE_AI_MODEL_KEY) || "")
|
||||
|
||||
setError("")
|
||||
}
|
||||
}, [open])
|
||||
@@ -197,188 +178,24 @@ export function SettingsDialog({
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="space-y-2">
|
||||
<Label>{dict.settings.aiProvider}</Label>
|
||||
<p className="text-[0.8rem] text-muted-foreground">
|
||||
{dict.settings.aiProviderDescription}
|
||||
</p>
|
||||
<div className="space-y-3 pt-2">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="ai-provider">
|
||||
{dict.settings.provider}
|
||||
</Label>
|
||||
<Select
|
||||
value={provider || "default"}
|
||||
onValueChange={(value) => {
|
||||
const actualValue =
|
||||
value === "default" ? "" : value
|
||||
setProvider(actualValue)
|
||||
localStorage.setItem(
|
||||
STORAGE_AI_PROVIDER_KEY,
|
||||
actualValue,
|
||||
)
|
||||
}}
|
||||
>
|
||||
<SelectTrigger id="ai-provider">
|
||||
<SelectValue
|
||||
placeholder={
|
||||
dict.settings.useServerDefault
|
||||
}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="default">
|
||||
{dict.settings.useServerDefault}
|
||||
</SelectItem>
|
||||
<SelectItem value="openai">
|
||||
{dict.providers.openai}
|
||||
</SelectItem>
|
||||
<SelectItem value="anthropic">
|
||||
{dict.providers.anthropic}
|
||||
</SelectItem>
|
||||
<SelectItem value="google">
|
||||
{dict.providers.google}
|
||||
</SelectItem>
|
||||
<SelectItem value="azure">
|
||||
{dict.providers.azure}
|
||||
</SelectItem>
|
||||
<SelectItem value="openrouter">
|
||||
{dict.providers.openrouter}
|
||||
</SelectItem>
|
||||
<SelectItem value="deepseek">
|
||||
{dict.providers.deepseek}
|
||||
</SelectItem>
|
||||
<SelectItem value="siliconflow">
|
||||
{dict.providers.siliconflow}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
{provider && provider !== "default" && (
|
||||
<>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="ai-model">
|
||||
{dict.settings.modelId}
|
||||
</Label>
|
||||
<Input
|
||||
id="ai-model"
|
||||
value={modelId}
|
||||
onChange={(e) => {
|
||||
setModelId(e.target.value)
|
||||
localStorage.setItem(
|
||||
STORAGE_AI_MODEL_KEY,
|
||||
e.target.value,
|
||||
)
|
||||
}}
|
||||
placeholder={
|
||||
provider === "openai"
|
||||
? "e.g., gpt-4o"
|
||||
: provider === "anthropic"
|
||||
? "e.g., claude-sonnet-4-5"
|
||||
: provider === "google"
|
||||
? "e.g., gemini-2.0-flash-exp"
|
||||
: provider ===
|
||||
"deepseek"
|
||||
? "e.g., deepseek-chat"
|
||||
: dict.settings
|
||||
.modelId
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="ai-api-key">
|
||||
{dict.settings.apiKey}
|
||||
</Label>
|
||||
<Input
|
||||
id="ai-api-key"
|
||||
type="password"
|
||||
value={apiKey}
|
||||
onChange={(e) => {
|
||||
setApiKey(e.target.value)
|
||||
localStorage.setItem(
|
||||
STORAGE_AI_API_KEY_KEY,
|
||||
e.target.value,
|
||||
)
|
||||
}}
|
||||
placeholder={
|
||||
dict.settings.apiKeyPlaceholder
|
||||
}
|
||||
autoComplete="off"
|
||||
/>
|
||||
<p className="text-[0.8rem] text-muted-foreground">
|
||||
{dict.settings.overrides}{" "}
|
||||
{provider === "openai"
|
||||
? "OPENAI_API_KEY"
|
||||
: provider === "anthropic"
|
||||
? "ANTHROPIC_API_KEY"
|
||||
: provider === "google"
|
||||
? "GOOGLE_GENERATIVE_AI_API_KEY"
|
||||
: provider === "azure"
|
||||
? "AZURE_API_KEY"
|
||||
: provider ===
|
||||
"openrouter"
|
||||
? "OPENROUTER_API_KEY"
|
||||
: provider ===
|
||||
"deepseek"
|
||||
? "DEEPSEEK_API_KEY"
|
||||
: provider ===
|
||||
"siliconflow"
|
||||
? "SILICONFLOW_API_KEY"
|
||||
: "server API key"}
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="ai-base-url">
|
||||
{dict.settings.baseUrl}
|
||||
</Label>
|
||||
<Input
|
||||
id="ai-base-url"
|
||||
value={baseUrl}
|
||||
onChange={(e) => {
|
||||
setBaseUrl(e.target.value)
|
||||
localStorage.setItem(
|
||||
STORAGE_AI_BASE_URL_KEY,
|
||||
e.target.value,
|
||||
)
|
||||
}}
|
||||
placeholder={
|
||||
provider === "anthropic"
|
||||
? "https://api.anthropic.com/v1"
|
||||
: provider === "siliconflow"
|
||||
? "https://api.siliconflow.com/v1"
|
||||
: dict.settings
|
||||
.customEndpoint
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-full"
|
||||
onClick={() => {
|
||||
localStorage.removeItem(
|
||||
STORAGE_AI_PROVIDER_KEY,
|
||||
)
|
||||
localStorage.removeItem(
|
||||
STORAGE_AI_BASE_URL_KEY,
|
||||
)
|
||||
localStorage.removeItem(
|
||||
STORAGE_AI_API_KEY_KEY,
|
||||
)
|
||||
localStorage.removeItem(
|
||||
STORAGE_AI_MODEL_KEY,
|
||||
)
|
||||
setProvider("")
|
||||
setBaseUrl("")
|
||||
setApiKey("")
|
||||
setModelId("")
|
||||
}}
|
||||
>
|
||||
{dict.settings.clearSettings}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<Label>{dict.settings.aiProvider}</Label>
|
||||
<p className="text-[0.8rem] text-muted-foreground">
|
||||
{dict.settings.aiProviderDescription}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
onOpenChange(false)
|
||||
onOpenModelConfig()
|
||||
}}
|
||||
>
|
||||
<Settings2 className="h-4 w-4 mr-2" />
|
||||
{dict.modelConfig?.configure || "Configure"}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
|
||||
191
components/ui/command.tsx
Normal file
191
components/ui/command.tsx
Normal file
@@ -0,0 +1,191 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Command as CommandPrimitive } from "cmdk"
|
||||
import { SearchIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog"
|
||||
|
||||
function Command({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive>) {
|
||||
return (
|
||||
<CommandPrimitive
|
||||
data-slot="command"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CommandDialog({
|
||||
title = "Command Palette",
|
||||
description = "Search for a command to run...",
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof Dialog> & {
|
||||
title?: string
|
||||
description?: string
|
||||
className?: string
|
||||
}) {
|
||||
return (
|
||||
<Dialog {...props}>
|
||||
<DialogHeader className="sr-only">
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
<DialogDescription>{description}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogContent className={cn("overflow-hidden p-0", className)}>
|
||||
<Command className="[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
||||
{children}
|
||||
</Command>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
function CommandInput({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Input>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="command-input-wrapper"
|
||||
className="flex h-9 items-center gap-2 border-b px-3"
|
||||
>
|
||||
<SearchIcon className="size-4 shrink-0 opacity-50" />
|
||||
<CommandPrimitive.Input
|
||||
data-slot="command-input"
|
||||
className={cn(
|
||||
"placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function CommandList({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.List>) {
|
||||
return (
|
||||
<CommandPrimitive.List
|
||||
data-slot="command-list"
|
||||
className={cn(
|
||||
"max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CommandEmpty({
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Empty>) {
|
||||
return (
|
||||
<CommandPrimitive.Empty
|
||||
data-slot="command-empty"
|
||||
className="py-6 text-center text-sm"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CommandGroup({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Group>) {
|
||||
return (
|
||||
<CommandPrimitive.Group
|
||||
data-slot="command-group"
|
||||
className={cn(
|
||||
"text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CommandSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Separator>) {
|
||||
return (
|
||||
<CommandPrimitive.Separator
|
||||
data-slot="command-separator"
|
||||
className={cn("bg-border -mx-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CommandItem({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Item>) {
|
||||
return (
|
||||
<CommandPrimitive.Item
|
||||
data-slot="command-item"
|
||||
className={cn(
|
||||
"data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
onMouseEnter={(e) => {
|
||||
// Ensure hover updates selection for visual feedback
|
||||
const item = e.currentTarget
|
||||
item.setAttribute("data-selected", "true")
|
||||
// Deselect siblings
|
||||
const siblings = item.parentElement?.querySelectorAll("[cmdk-item]")
|
||||
siblings?.forEach((sibling) => {
|
||||
if (sibling !== item) {
|
||||
sibling.setAttribute("data-selected", "false")
|
||||
}
|
||||
})
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CommandShortcut({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="command-shortcut"
|
||||
className={cn(
|
||||
"text-muted-foreground ml-auto text-xs tracking-widest",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Command,
|
||||
CommandDialog,
|
||||
CommandInput,
|
||||
CommandList,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandItem,
|
||||
CommandShortcut,
|
||||
CommandSeparator,
|
||||
}
|
||||
48
components/ui/popover.tsx
Normal file
48
components/ui/popover.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as PopoverPrimitive from "@radix-ui/react-popover"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Popover({
|
||||
...props
|
||||
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
|
||||
return <PopoverPrimitive.Root data-slot="popover" {...props} />
|
||||
}
|
||||
|
||||
function PopoverTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
|
||||
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
|
||||
}
|
||||
|
||||
function PopoverContent({
|
||||
className,
|
||||
align = "center",
|
||||
sideOffset = 4,
|
||||
...props
|
||||
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
|
||||
return (
|
||||
<PopoverPrimitive.Portal>
|
||||
<PopoverPrimitive.Content
|
||||
data-slot="popover-content"
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</PopoverPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
function PopoverAnchor({
|
||||
...props
|
||||
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
|
||||
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />
|
||||
}
|
||||
|
||||
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
|
||||
354
hooks/use-model-config.ts
Normal file
354
hooks/use-model-config.ts
Normal file
@@ -0,0 +1,354 @@
|
||||
"use client"
|
||||
|
||||
import { useCallback, useEffect, useState } from "react"
|
||||
import { STORAGE_KEYS } from "@/lib/storage"
|
||||
import {
|
||||
createEmptyConfig,
|
||||
createModelConfig,
|
||||
createProviderConfig,
|
||||
type FlattenedModel,
|
||||
findModelById,
|
||||
flattenModels,
|
||||
type ModelConfig,
|
||||
type MultiModelConfig,
|
||||
PROVIDER_INFO,
|
||||
type ProviderConfig,
|
||||
type ProviderName,
|
||||
} from "@/lib/types/model-config"
|
||||
|
||||
// Old storage keys for migration
|
||||
const OLD_KEYS = {
|
||||
aiProvider: "next-ai-draw-io-ai-provider",
|
||||
aiBaseUrl: "next-ai-draw-io-ai-base-url",
|
||||
aiApiKey: "next-ai-draw-io-ai-api-key",
|
||||
aiModel: "next-ai-draw-io-ai-model",
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate from old single-provider format to new multi-model format
|
||||
*/
|
||||
function migrateOldConfig(): MultiModelConfig | null {
|
||||
if (typeof window === "undefined") return null
|
||||
|
||||
const oldProvider = localStorage.getItem(OLD_KEYS.aiProvider)
|
||||
const oldApiKey = localStorage.getItem(OLD_KEYS.aiApiKey)
|
||||
const oldModel = localStorage.getItem(OLD_KEYS.aiModel)
|
||||
|
||||
// No old config to migrate
|
||||
if (!oldProvider || !oldApiKey || !oldModel) return null
|
||||
|
||||
const oldBaseUrl = localStorage.getItem(OLD_KEYS.aiBaseUrl)
|
||||
|
||||
// Create new config from old format
|
||||
const provider = createProviderConfig(oldProvider as ProviderName)
|
||||
provider.apiKey = oldApiKey
|
||||
if (oldBaseUrl) provider.baseUrl = oldBaseUrl
|
||||
|
||||
const model = createModelConfig(oldModel)
|
||||
provider.models.push(model)
|
||||
|
||||
const config: MultiModelConfig = {
|
||||
version: 1,
|
||||
providers: [provider],
|
||||
selectedModelId: model.id,
|
||||
}
|
||||
|
||||
// Clear old keys after migration
|
||||
localStorage.removeItem(OLD_KEYS.aiProvider)
|
||||
localStorage.removeItem(OLD_KEYS.aiBaseUrl)
|
||||
localStorage.removeItem(OLD_KEYS.aiApiKey)
|
||||
localStorage.removeItem(OLD_KEYS.aiModel)
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
/**
|
||||
* Load config from localStorage
|
||||
*/
|
||||
function loadConfig(): MultiModelConfig {
|
||||
if (typeof window === "undefined") return createEmptyConfig()
|
||||
|
||||
// First, check if new format exists
|
||||
const stored = localStorage.getItem(STORAGE_KEYS.modelConfigs)
|
||||
if (stored) {
|
||||
try {
|
||||
return JSON.parse(stored) as MultiModelConfig
|
||||
} catch {
|
||||
console.error("Failed to parse model config")
|
||||
}
|
||||
}
|
||||
|
||||
// Try migration from old format
|
||||
const migrated = migrateOldConfig()
|
||||
if (migrated) {
|
||||
// Save migrated config
|
||||
localStorage.setItem(
|
||||
STORAGE_KEYS.modelConfigs,
|
||||
JSON.stringify(migrated),
|
||||
)
|
||||
return migrated
|
||||
}
|
||||
|
||||
return createEmptyConfig()
|
||||
}
|
||||
|
||||
/**
|
||||
* Save config to localStorage
|
||||
*/
|
||||
function saveConfig(config: MultiModelConfig): void {
|
||||
if (typeof window === "undefined") return
|
||||
localStorage.setItem(STORAGE_KEYS.modelConfigs, JSON.stringify(config))
|
||||
}
|
||||
|
||||
export interface UseModelConfigReturn {
|
||||
// State
|
||||
config: MultiModelConfig
|
||||
isLoaded: boolean
|
||||
|
||||
// Getters
|
||||
models: FlattenedModel[]
|
||||
selectedModel: FlattenedModel | undefined
|
||||
selectedModelId: string | undefined
|
||||
|
||||
// Actions
|
||||
setSelectedModelId: (modelId: string | undefined) => void
|
||||
addProvider: (provider: ProviderName) => ProviderConfig
|
||||
updateProvider: (
|
||||
providerId: string,
|
||||
updates: Partial<ProviderConfig>,
|
||||
) => void
|
||||
deleteProvider: (providerId: string) => void
|
||||
addModel: (providerId: string, modelId: string) => ModelConfig
|
||||
updateModel: (
|
||||
providerId: string,
|
||||
modelConfigId: string,
|
||||
updates: Partial<ModelConfig>,
|
||||
) => void
|
||||
deleteModel: (providerId: string, modelConfigId: string) => void
|
||||
resetConfig: () => void
|
||||
}
|
||||
|
||||
export function useModelConfig(): UseModelConfigReturn {
|
||||
const [config, setConfig] = useState<MultiModelConfig>(createEmptyConfig)
|
||||
const [isLoaded, setIsLoaded] = useState(false)
|
||||
|
||||
// Load config on mount
|
||||
useEffect(() => {
|
||||
const loaded = loadConfig()
|
||||
setConfig(loaded)
|
||||
setIsLoaded(true)
|
||||
}, [])
|
||||
|
||||
// Save config whenever it changes (after initial load)
|
||||
useEffect(() => {
|
||||
if (isLoaded) {
|
||||
saveConfig(config)
|
||||
}
|
||||
}, [config, isLoaded])
|
||||
|
||||
// Derived state
|
||||
const models = flattenModels(config)
|
||||
const selectedModel = config.selectedModelId
|
||||
? findModelById(config, config.selectedModelId)
|
||||
: undefined
|
||||
|
||||
// Actions
|
||||
const setSelectedModelId = useCallback((modelId: string | undefined) => {
|
||||
setConfig((prev) => ({
|
||||
...prev,
|
||||
selectedModelId: modelId,
|
||||
}))
|
||||
}, [])
|
||||
|
||||
const addProvider = useCallback(
|
||||
(provider: ProviderName): ProviderConfig => {
|
||||
const newProvider = createProviderConfig(provider)
|
||||
setConfig((prev) => ({
|
||||
...prev,
|
||||
providers: [...prev.providers, newProvider],
|
||||
}))
|
||||
return newProvider
|
||||
},
|
||||
[],
|
||||
)
|
||||
|
||||
const updateProvider = useCallback(
|
||||
(providerId: string, updates: Partial<ProviderConfig>) => {
|
||||
setConfig((prev) => ({
|
||||
...prev,
|
||||
providers: prev.providers.map((p) =>
|
||||
p.id === providerId ? { ...p, ...updates } : p,
|
||||
),
|
||||
}))
|
||||
},
|
||||
[],
|
||||
)
|
||||
|
||||
const deleteProvider = useCallback((providerId: string) => {
|
||||
setConfig((prev) => {
|
||||
const provider = prev.providers.find((p) => p.id === providerId)
|
||||
const modelIds = provider?.models.map((m) => m.id) || []
|
||||
|
||||
// Clear selected model if it belongs to deleted provider
|
||||
const newSelectedId =
|
||||
prev.selectedModelId && modelIds.includes(prev.selectedModelId)
|
||||
? undefined
|
||||
: prev.selectedModelId
|
||||
|
||||
return {
|
||||
...prev,
|
||||
providers: prev.providers.filter((p) => p.id !== providerId),
|
||||
selectedModelId: newSelectedId,
|
||||
}
|
||||
})
|
||||
}, [])
|
||||
|
||||
const addModel = useCallback(
|
||||
(providerId: string, modelId: string): ModelConfig => {
|
||||
const newModel = createModelConfig(modelId)
|
||||
setConfig((prev) => ({
|
||||
...prev,
|
||||
providers: prev.providers.map((p) =>
|
||||
p.id === providerId
|
||||
? { ...p, models: [...p.models, newModel] }
|
||||
: p,
|
||||
),
|
||||
}))
|
||||
return newModel
|
||||
},
|
||||
[],
|
||||
)
|
||||
|
||||
const updateModel = useCallback(
|
||||
(
|
||||
providerId: string,
|
||||
modelConfigId: string,
|
||||
updates: Partial<ModelConfig>,
|
||||
) => {
|
||||
setConfig((prev) => ({
|
||||
...prev,
|
||||
providers: prev.providers.map((p) =>
|
||||
p.id === providerId
|
||||
? {
|
||||
...p,
|
||||
models: p.models.map((m) =>
|
||||
m.id === modelConfigId
|
||||
? { ...m, ...updates }
|
||||
: m,
|
||||
),
|
||||
}
|
||||
: p,
|
||||
),
|
||||
}))
|
||||
},
|
||||
[],
|
||||
)
|
||||
|
||||
const deleteModel = useCallback(
|
||||
(providerId: string, modelConfigId: string) => {
|
||||
setConfig((prev) => ({
|
||||
...prev,
|
||||
providers: prev.providers.map((p) =>
|
||||
p.id === providerId
|
||||
? {
|
||||
...p,
|
||||
models: p.models.filter(
|
||||
(m) => m.id !== modelConfigId,
|
||||
),
|
||||
}
|
||||
: p,
|
||||
),
|
||||
// Clear selected model if it was deleted
|
||||
selectedModelId:
|
||||
prev.selectedModelId === modelConfigId
|
||||
? undefined
|
||||
: prev.selectedModelId,
|
||||
}))
|
||||
},
|
||||
[],
|
||||
)
|
||||
|
||||
const resetConfig = useCallback(() => {
|
||||
setConfig(createEmptyConfig())
|
||||
}, [])
|
||||
|
||||
return {
|
||||
config,
|
||||
isLoaded,
|
||||
models,
|
||||
selectedModel,
|
||||
selectedModelId: config.selectedModelId,
|
||||
setSelectedModelId,
|
||||
addProvider,
|
||||
updateProvider,
|
||||
deleteProvider,
|
||||
addModel,
|
||||
updateModel,
|
||||
deleteModel,
|
||||
resetConfig,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the AI config for the currently selected model.
|
||||
* Returns format compatible with existing getAIConfig() usage.
|
||||
*/
|
||||
export function getSelectedAIConfig(): {
|
||||
accessCode: string
|
||||
aiProvider: string
|
||||
aiBaseUrl: string
|
||||
aiApiKey: string
|
||||
aiModel: string
|
||||
} {
|
||||
const empty = {
|
||||
accessCode: "",
|
||||
aiProvider: "",
|
||||
aiBaseUrl: "",
|
||||
aiApiKey: "",
|
||||
aiModel: "",
|
||||
}
|
||||
|
||||
if (typeof window === "undefined") return empty
|
||||
|
||||
// Get access code (separate from model config)
|
||||
const accessCode = localStorage.getItem(STORAGE_KEYS.accessCode) || ""
|
||||
|
||||
// Load multi-model config
|
||||
const stored = localStorage.getItem(STORAGE_KEYS.modelConfigs)
|
||||
if (!stored) {
|
||||
// Fallback to old format for backward compatibility
|
||||
return {
|
||||
accessCode,
|
||||
aiProvider: localStorage.getItem(OLD_KEYS.aiProvider) || "",
|
||||
aiBaseUrl: localStorage.getItem(OLD_KEYS.aiBaseUrl) || "",
|
||||
aiApiKey: localStorage.getItem(OLD_KEYS.aiApiKey) || "",
|
||||
aiModel: localStorage.getItem(OLD_KEYS.aiModel) || "",
|
||||
}
|
||||
}
|
||||
|
||||
let config: MultiModelConfig
|
||||
try {
|
||||
config = JSON.parse(stored)
|
||||
} catch {
|
||||
return { ...empty, accessCode }
|
||||
}
|
||||
|
||||
// No selected model = use server default
|
||||
if (!config.selectedModelId) {
|
||||
return { ...empty, accessCode }
|
||||
}
|
||||
|
||||
// Find selected model
|
||||
const model = findModelById(config, config.selectedModelId)
|
||||
if (!model) {
|
||||
return { ...empty, accessCode }
|
||||
}
|
||||
|
||||
return {
|
||||
accessCode,
|
||||
aiProvider: model.provider,
|
||||
aiBaseUrl: model.baseUrl || "",
|
||||
aiApiKey: model.apiKey,
|
||||
aiModel: model.modelId,
|
||||
}
|
||||
}
|
||||
@@ -180,5 +180,24 @@
|
||||
"seekingSponsorship": "Call for Sponsorship",
|
||||
"contactMe": "Contact Me",
|
||||
"usageNotice": "Due to high usage, I have changed the model from Claude to minimax-m2 and added some usage limits. See About page for details."
|
||||
},
|
||||
"modelConfig": {
|
||||
"title": "AI Model Configuration",
|
||||
"description": "Configure multiple AI providers and models",
|
||||
"configure": "Configure",
|
||||
"addProvider": "Add Provider",
|
||||
"addModel": "Add Model",
|
||||
"modelId": "Model ID",
|
||||
"modelLabel": "Display Label",
|
||||
"streaming": "Enable Streaming",
|
||||
"deleteProvider": "Delete Provider",
|
||||
"deleteModel": "Delete Model",
|
||||
"noModels": "No models configured. Add a model to get started.",
|
||||
"selectProvider": "Select a provider or add a new one",
|
||||
"configureMultiple": "Configure multiple AI providers and switch between them easily",
|
||||
"apiKeyStored": "API keys are stored locally in your browser",
|
||||
"test": "Test",
|
||||
"validationError": "Validation failed",
|
||||
"addModelFirst": "Add at least one model to validate"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,5 +180,24 @@
|
||||
"seekingSponsorship": "スポンサー募集",
|
||||
"contactMe": "お問い合わせ",
|
||||
"usageNotice": "利用量の増加に伴い、コスト削減のためモデルを Claude から minimax-m2 に変更し、いくつかの利用制限を設けました。詳細は概要ページをご覧ください。"
|
||||
},
|
||||
"modelConfig": {
|
||||
"title": "AIモデル設定",
|
||||
"description": "複数のAIプロバイダーとモデルを設定",
|
||||
"configure": "設定",
|
||||
"addProvider": "プロバイダーを追加",
|
||||
"addModel": "モデルを追加",
|
||||
"modelId": "モデルID",
|
||||
"modelLabel": "表示名",
|
||||
"streaming": "ストリーミングを有効",
|
||||
"deleteProvider": "プロバイダーを削除",
|
||||
"deleteModel": "モデルを削除",
|
||||
"noModels": "モデルが設定されていません。モデルを追加してください。",
|
||||
"selectProvider": "プロバイダーを選択または追加してください",
|
||||
"configureMultiple": "複数のAIプロバイダーを設定して簡単に切り替え",
|
||||
"apiKeyStored": "APIキーはブラウザにローカル保存されます",
|
||||
"test": "テスト",
|
||||
"validationError": "検証に失敗しました",
|
||||
"addModelFirst": "検証するには少なくとも1つのモデルを追加してください"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,5 +180,24 @@
|
||||
"seekingSponsorship": "寻求赞助(求大佬捞一把)",
|
||||
"contactMe": "联系我",
|
||||
"usageNotice": "由于使用量过高,我已将模型从 Claude 更换为 minimax-m2,并设置了一些用量限制。详情请查看关于页面。"
|
||||
},
|
||||
"modelConfig": {
|
||||
"title": "AI 模型配置",
|
||||
"description": "配置多个 AI 提供商和模型",
|
||||
"configure": "配置",
|
||||
"addProvider": "添加提供商",
|
||||
"addModel": "添加模型",
|
||||
"modelId": "模型 ID",
|
||||
"modelLabel": "显示名称",
|
||||
"streaming": "启用流式输出",
|
||||
"deleteProvider": "删除提供商",
|
||||
"deleteModel": "删除模型",
|
||||
"noModels": "尚未配置模型。添加模型以开始使用。",
|
||||
"selectProvider": "选择一个提供商或添加新的",
|
||||
"configureMultiple": "配置多个 AI 提供商并轻松切换",
|
||||
"apiKeyStored": "API 密钥存储在您的浏览器本地",
|
||||
"test": "测试",
|
||||
"validationError": "验证失败",
|
||||
"addModelFirst": "请先添加至少一个模型以进行验证"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,4 +24,8 @@ export const STORAGE_KEYS = {
|
||||
aiBaseUrl: "next-ai-draw-io-ai-base-url",
|
||||
aiApiKey: "next-ai-draw-io-ai-api-key",
|
||||
aiModel: "next-ai-draw-io-ai-model",
|
||||
|
||||
// Multi-model configuration
|
||||
modelConfigs: "next-ai-draw-io-model-configs",
|
||||
selectedModelId: "next-ai-draw-io-selected-model-id",
|
||||
} as const
|
||||
|
||||
290
lib/types/model-config.ts
Normal file
290
lib/types/model-config.ts
Normal file
@@ -0,0 +1,290 @@
|
||||
// Types for multi-provider model configuration
|
||||
|
||||
export type ProviderName =
|
||||
| "openai"
|
||||
| "anthropic"
|
||||
| "google"
|
||||
| "azure"
|
||||
| "bedrock"
|
||||
| "openrouter"
|
||||
| "deepseek"
|
||||
| "siliconflow"
|
||||
| "ollama"
|
||||
| "gateway"
|
||||
|
||||
// Individual model configuration
|
||||
export interface ModelConfig {
|
||||
id: string // UUID for this model
|
||||
modelId: string // e.g., "gpt-4o", "claude-sonnet-4-5"
|
||||
streaming?: boolean // Default true
|
||||
}
|
||||
|
||||
// Provider configuration
|
||||
export interface ProviderConfig {
|
||||
id: string // UUID for this provider config
|
||||
provider: ProviderName
|
||||
name?: string // Custom display name (e.g., "OpenAI Production")
|
||||
apiKey: string
|
||||
baseUrl?: string
|
||||
models: ModelConfig[]
|
||||
validated?: boolean // Has API key been validated
|
||||
}
|
||||
|
||||
// The complete multi-model configuration
|
||||
export interface MultiModelConfig {
|
||||
version: 1
|
||||
providers: ProviderConfig[]
|
||||
selectedModelId?: string // Currently selected model's UUID
|
||||
}
|
||||
|
||||
// Flattened model for dropdown display
|
||||
export interface FlattenedModel {
|
||||
id: string // Model config UUID
|
||||
modelId: string // Actual model ID
|
||||
provider: ProviderName
|
||||
providerLabel: string // Provider display name
|
||||
apiKey: string
|
||||
baseUrl?: string
|
||||
streaming?: boolean
|
||||
}
|
||||
|
||||
// Provider metadata
|
||||
export const PROVIDER_INFO: Record<
|
||||
ProviderName,
|
||||
{ label: string; defaultBaseUrl?: string }
|
||||
> = {
|
||||
openai: { label: "OpenAI" },
|
||||
anthropic: {
|
||||
label: "Anthropic",
|
||||
defaultBaseUrl: "https://api.anthropic.com/v1",
|
||||
},
|
||||
google: { label: "Google" },
|
||||
azure: { label: "Azure OpenAI" },
|
||||
bedrock: { label: "Amazon Bedrock" },
|
||||
openrouter: { label: "OpenRouter" },
|
||||
deepseek: { label: "DeepSeek" },
|
||||
siliconflow: {
|
||||
label: "SiliconFlow",
|
||||
defaultBaseUrl: "https://api.siliconflow.com/v1",
|
||||
},
|
||||
ollama: { label: "Ollama", defaultBaseUrl: "http://localhost:11434" },
|
||||
gateway: { label: "AI Gateway" },
|
||||
}
|
||||
|
||||
// Suggested models per provider for quick add
|
||||
export const SUGGESTED_MODELS: Record<ProviderName, string[]> = {
|
||||
openai: [
|
||||
// GPT-4o series
|
||||
"gpt-4o",
|
||||
"gpt-4o-mini",
|
||||
"gpt-4o-audio-preview",
|
||||
// GPT-4.1 series
|
||||
"gpt-4.1",
|
||||
"gpt-4.1-mini",
|
||||
"gpt-4.1-nano",
|
||||
// GPT-4 Turbo
|
||||
"gpt-4-turbo",
|
||||
"gpt-4-turbo-preview",
|
||||
// Reasoning models
|
||||
"o1",
|
||||
"o1-mini",
|
||||
"o1-preview",
|
||||
"o3",
|
||||
"o3-mini",
|
||||
"o4-mini",
|
||||
// Legacy
|
||||
"gpt-4",
|
||||
"gpt-3.5-turbo",
|
||||
],
|
||||
anthropic: [
|
||||
// Claude 4.5 series (latest)
|
||||
"claude-opus-4-5-20250514",
|
||||
"claude-sonnet-4-5-20250514",
|
||||
// Claude 4 series
|
||||
"claude-opus-4-20250514",
|
||||
"claude-sonnet-4-20250514",
|
||||
// Claude 3.7 series
|
||||
"claude-3-7-sonnet-20250219",
|
||||
// Claude 3.5 series
|
||||
"claude-3-5-sonnet-20241022",
|
||||
"claude-3-5-haiku-20241022",
|
||||
// Claude 3 series
|
||||
"claude-3-opus-20240229",
|
||||
"claude-3-sonnet-20240229",
|
||||
"claude-3-haiku-20240307",
|
||||
],
|
||||
google: [
|
||||
// Gemini 2.5 series
|
||||
"gemini-2.5-pro",
|
||||
"gemini-2.5-flash",
|
||||
"gemini-2.5-flash-preview-05-20",
|
||||
// Gemini 2.0 series
|
||||
"gemini-2.0-flash",
|
||||
"gemini-2.0-flash-exp",
|
||||
"gemini-2.0-flash-lite",
|
||||
// Gemini 1.5 series
|
||||
"gemini-1.5-pro",
|
||||
"gemini-1.5-flash",
|
||||
// Legacy
|
||||
"gemini-pro",
|
||||
],
|
||||
azure: ["gpt-4o", "gpt-4o-mini", "gpt-4-turbo", "gpt-4", "gpt-35-turbo"],
|
||||
bedrock: [
|
||||
// Anthropic Claude
|
||||
"anthropic.claude-opus-4-5-20250514-v1:0",
|
||||
"anthropic.claude-sonnet-4-5-20250514-v1:0",
|
||||
"anthropic.claude-opus-4-20250514-v1:0",
|
||||
"anthropic.claude-sonnet-4-20250514-v1:0",
|
||||
"anthropic.claude-3-7-sonnet-20250219-v1:0",
|
||||
"anthropic.claude-3-5-sonnet-20241022-v2:0",
|
||||
"anthropic.claude-3-5-haiku-20241022-v1:0",
|
||||
"anthropic.claude-3-opus-20240229-v1:0",
|
||||
"anthropic.claude-3-sonnet-20240229-v1:0",
|
||||
"anthropic.claude-3-haiku-20240307-v1:0",
|
||||
// Amazon Nova
|
||||
"amazon.nova-pro-v1:0",
|
||||
"amazon.nova-lite-v1:0",
|
||||
"amazon.nova-micro-v1:0",
|
||||
// Meta Llama
|
||||
"meta.llama3-3-70b-instruct-v1:0",
|
||||
"meta.llama3-1-405b-instruct-v1:0",
|
||||
"meta.llama3-1-70b-instruct-v1:0",
|
||||
// Mistral
|
||||
"mistral.mistral-large-2411-v1:0",
|
||||
"mistral.mistral-small-2503-v1:0",
|
||||
],
|
||||
openrouter: [
|
||||
// Anthropic
|
||||
"anthropic/claude-sonnet-4",
|
||||
"anthropic/claude-opus-4",
|
||||
"anthropic/claude-3.5-sonnet",
|
||||
"anthropic/claude-3.5-haiku",
|
||||
// OpenAI
|
||||
"openai/gpt-4o",
|
||||
"openai/gpt-4o-mini",
|
||||
"openai/o1",
|
||||
"openai/o3-mini",
|
||||
// Google
|
||||
"google/gemini-2.5-pro",
|
||||
"google/gemini-2.5-flash",
|
||||
"google/gemini-2.0-flash-exp:free",
|
||||
// Meta Llama
|
||||
"meta-llama/llama-3.3-70b-instruct",
|
||||
"meta-llama/llama-3.1-405b-instruct",
|
||||
"meta-llama/llama-3.1-70b-instruct",
|
||||
// DeepSeek
|
||||
"deepseek/deepseek-chat",
|
||||
"deepseek/deepseek-r1",
|
||||
// Qwen
|
||||
"qwen/qwen-2.5-72b-instruct",
|
||||
],
|
||||
deepseek: ["deepseek-chat", "deepseek-reasoner", "deepseek-coder"],
|
||||
siliconflow: [
|
||||
// DeepSeek
|
||||
"deepseek-ai/DeepSeek-V3",
|
||||
"deepseek-ai/DeepSeek-R1",
|
||||
"deepseek-ai/DeepSeek-V2.5",
|
||||
// Qwen
|
||||
"Qwen/Qwen2.5-72B-Instruct",
|
||||
"Qwen/Qwen2.5-32B-Instruct",
|
||||
"Qwen/Qwen2.5-Coder-32B-Instruct",
|
||||
"Qwen/Qwen2.5-7B-Instruct",
|
||||
"Qwen/Qwen2-VL-72B-Instruct",
|
||||
],
|
||||
ollama: [
|
||||
"llama3.3",
|
||||
"llama3.2",
|
||||
"llama3.1",
|
||||
"qwen2.5",
|
||||
"qwen2.5-coder",
|
||||
"deepseek-r1",
|
||||
"deepseek-coder-v2",
|
||||
"gemma2",
|
||||
"phi4",
|
||||
"mistral",
|
||||
"codellama",
|
||||
],
|
||||
gateway: [
|
||||
"openai/gpt-4o",
|
||||
"openai/gpt-4o-mini",
|
||||
"anthropic/claude-sonnet-4-5",
|
||||
"anthropic/claude-3-5-sonnet",
|
||||
"google/gemini-2.0-flash",
|
||||
],
|
||||
}
|
||||
|
||||
// Helper to generate UUID
|
||||
export function generateId(): string {
|
||||
return `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`
|
||||
}
|
||||
|
||||
// Create empty config
|
||||
export function createEmptyConfig(): MultiModelConfig {
|
||||
return {
|
||||
version: 1,
|
||||
providers: [],
|
||||
selectedModelId: undefined,
|
||||
}
|
||||
}
|
||||
|
||||
// Create new provider config
|
||||
export function createProviderConfig(provider: ProviderName): ProviderConfig {
|
||||
return {
|
||||
id: generateId(),
|
||||
provider,
|
||||
apiKey: "",
|
||||
baseUrl: PROVIDER_INFO[provider].defaultBaseUrl,
|
||||
models: [],
|
||||
validated: false,
|
||||
}
|
||||
}
|
||||
|
||||
// Create new model config
|
||||
export function createModelConfig(modelId: string): ModelConfig {
|
||||
return {
|
||||
id: generateId(),
|
||||
modelId,
|
||||
streaming: true,
|
||||
}
|
||||
}
|
||||
|
||||
// Get all models as flattened list for dropdown
|
||||
export function flattenModels(config: MultiModelConfig): FlattenedModel[] {
|
||||
const models: FlattenedModel[] = []
|
||||
|
||||
for (const provider of config.providers) {
|
||||
// Use custom name if provided, otherwise use default provider label
|
||||
const providerLabel =
|
||||
provider.name || PROVIDER_INFO[provider.provider].label
|
||||
|
||||
for (const model of provider.models) {
|
||||
models.push({
|
||||
id: model.id,
|
||||
modelId: model.modelId,
|
||||
provider: provider.provider,
|
||||
providerLabel,
|
||||
apiKey: provider.apiKey,
|
||||
baseUrl: provider.baseUrl,
|
||||
streaming: model.streaming,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return models
|
||||
}
|
||||
|
||||
// Find model by ID
|
||||
export function findModelById(
|
||||
config: MultiModelConfig,
|
||||
modelId: string,
|
||||
): FlattenedModel | undefined {
|
||||
return flattenModels(config).find((m) => m.id === modelId)
|
||||
}
|
||||
|
||||
// Get selected model
|
||||
export function getSelectedModel(
|
||||
config: MultiModelConfig,
|
||||
): FlattenedModel | undefined {
|
||||
if (!config.selectedModelId) return undefined
|
||||
return findModelById(config, config.selectedModelId)
|
||||
}
|
||||
741
package-lock.json
generated
741
package-lock.json
generated
@@ -27,8 +27,9 @@
|
||||
"@opentelemetry/exporter-trace-otlp-http": "^0.208.0",
|
||||
"@opentelemetry/sdk-trace-node": "^2.2.0",
|
||||
"@radix-ui/react-collapsible": "^1.1.12",
|
||||
"@radix-ui/react-dialog": "^1.1.6",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-label": "^2.1.8",
|
||||
"@radix-ui/react-popover": "^1.1.15",
|
||||
"@radix-ui/react-scroll-area": "^1.2.3",
|
||||
"@radix-ui/react-select": "^2.2.6",
|
||||
"@radix-ui/react-slot": "^1.1.2",
|
||||
@@ -40,6 +41,7 @@
|
||||
"base-64": "^1.0.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"js-tiktoken": "^1.0.21",
|
||||
"jsdom": "^26.0.0",
|
||||
"jsonrepair": "^3.13.1",
|
||||
@@ -4655,23 +4657,23 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dialog": {
|
||||
"version": "1.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.6.tgz",
|
||||
"integrity": "sha512-/IVhJV5AceX620DUJ4uYVMymzsipdKBzo3edo+omeskCKGm9FRHM0ebIdbPnlQVJqyuHbuBltQUOG2mOTq2IYw==",
|
||||
"version": "1.1.15",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz",
|
||||
"integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.1",
|
||||
"@radix-ui/react-compose-refs": "1.1.1",
|
||||
"@radix-ui/react-context": "1.1.1",
|
||||
"@radix-ui/react-dismissable-layer": "1.1.5",
|
||||
"@radix-ui/react-focus-guards": "1.1.1",
|
||||
"@radix-ui/react-focus-scope": "1.1.2",
|
||||
"@radix-ui/react-id": "1.1.0",
|
||||
"@radix-ui/react-portal": "1.1.4",
|
||||
"@radix-ui/react-presence": "1.1.2",
|
||||
"@radix-ui/react-primitive": "2.0.2",
|
||||
"@radix-ui/react-slot": "1.1.2",
|
||||
"@radix-ui/react-use-controllable-state": "1.1.0",
|
||||
"@radix-ui/primitive": "1.1.3",
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-context": "1.1.2",
|
||||
"@radix-ui/react-dismissable-layer": "1.1.11",
|
||||
"@radix-ui/react-focus-guards": "1.1.3",
|
||||
"@radix-ui/react-focus-scope": "1.1.7",
|
||||
"@radix-ui/react-id": "1.1.1",
|
||||
"@radix-ui/react-portal": "1.1.9",
|
||||
"@radix-ui/react-presence": "1.1.5",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-slot": "1.2.3",
|
||||
"@radix-ui/react-use-controllable-state": "1.2.2",
|
||||
"aria-hidden": "^1.2.4",
|
||||
"react-remove-scroll": "^2.6.3"
|
||||
},
|
||||
@@ -4690,14 +4692,214 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-use-controllable-state": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz",
|
||||
"integrity": "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==",
|
||||
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/primitive": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
|
||||
"integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-compose-refs": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
|
||||
"integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-context": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
|
||||
"integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-dismissable-layer": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz",
|
||||
"integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-use-callback-ref": "1.1.0"
|
||||
"@radix-ui/primitive": "1.1.3",
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-use-callback-ref": "1.1.1",
|
||||
"@radix-ui/react-use-escape-keydown": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-id": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz",
|
||||
"integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-portal": {
|
||||
"version": "1.1.9",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz",
|
||||
"integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-presence": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz",
|
||||
"integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-primitive": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
|
||||
"integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-slot": "1.2.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-use-callback-ref": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
|
||||
"integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-use-escape-keydown": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz",
|
||||
"integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-use-callback-ref": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-use-layout-effect": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
|
||||
"integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
@@ -4751,9 +4953,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-focus-guards": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.1.tgz",
|
||||
"integrity": "sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==",
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz",
|
||||
"integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
@@ -4766,14 +4968,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-focus-scope": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.2.tgz",
|
||||
"integrity": "sha512-zxwE80FCU7lcXUGWkdt6XpTTCKPitG1XKOwViTxHVKIJhZl9MvIl2dVHeZENCWD9+EdWv05wlaEkRXUykU27RA==",
|
||||
"version": "1.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz",
|
||||
"integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.1",
|
||||
"@radix-ui/react-primitive": "2.0.2",
|
||||
"@radix-ui/react-use-callback-ref": "1.1.0"
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-use-callback-ref": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
@@ -4790,6 +4992,77 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-focus-scope/node_modules/@radix-ui/react-compose-refs": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
|
||||
"integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-focus-scope/node_modules/@radix-ui/react-primitive": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
|
||||
"integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-slot": "1.2.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-focus-scope/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-focus-scope/node_modules/@radix-ui/react-use-callback-ref": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
|
||||
"integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-id": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.0.tgz",
|
||||
@@ -4887,6 +5160,358 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-popover": {
|
||||
"version": "1.1.15",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz",
|
||||
"integrity": "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.3",
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-context": "1.1.2",
|
||||
"@radix-ui/react-dismissable-layer": "1.1.11",
|
||||
"@radix-ui/react-focus-guards": "1.1.3",
|
||||
"@radix-ui/react-focus-scope": "1.1.7",
|
||||
"@radix-ui/react-id": "1.1.1",
|
||||
"@radix-ui/react-popper": "1.2.8",
|
||||
"@radix-ui/react-portal": "1.1.9",
|
||||
"@radix-ui/react-presence": "1.1.5",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-slot": "1.2.3",
|
||||
"@radix-ui/react-use-controllable-state": "1.2.2",
|
||||
"aria-hidden": "^1.2.4",
|
||||
"react-remove-scroll": "^2.6.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-popover/node_modules/@radix-ui/primitive": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
|
||||
"integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-arrow": {
|
||||
"version": "1.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz",
|
||||
"integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-primitive": "2.1.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-compose-refs": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
|
||||
"integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-context": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
|
||||
"integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-dismissable-layer": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz",
|
||||
"integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.3",
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-use-callback-ref": "1.1.1",
|
||||
"@radix-ui/react-use-escape-keydown": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-id": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz",
|
||||
"integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-popper": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz",
|
||||
"integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/react-dom": "^2.0.0",
|
||||
"@radix-ui/react-arrow": "1.1.7",
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-context": "1.1.2",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-use-callback-ref": "1.1.1",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.1",
|
||||
"@radix-ui/react-use-rect": "1.1.1",
|
||||
"@radix-ui/react-use-size": "1.1.1",
|
||||
"@radix-ui/rect": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-portal": {
|
||||
"version": "1.1.9",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz",
|
||||
"integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-presence": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz",
|
||||
"integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-primitive": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
|
||||
"integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-slot": "1.2.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-use-callback-ref": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
|
||||
"integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-use-escape-keydown": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz",
|
||||
"integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-use-callback-ref": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-use-layout-effect": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
|
||||
"integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-use-rect": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz",
|
||||
"integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/rect": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-use-size": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz",
|
||||
"integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-popover/node_modules/@radix-ui/rect": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz",
|
||||
"integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@radix-ui/react-popper": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.2.tgz",
|
||||
@@ -5171,46 +5796,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-focus-guards": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz",
|
||||
"integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-focus-scope": {
|
||||
"version": "1.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz",
|
||||
"integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-use-callback-ref": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-id": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz",
|
||||
@@ -8853,6 +9438,22 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/cmdk": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.1.1.tgz",
|
||||
"integrity": "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "^1.1.1",
|
||||
"@radix-ui/react-dialog": "^1.1.6",
|
||||
"@radix-ui/react-id": "^1.1.0",
|
||||
"@radix-ui/react-primitive": "^2.0.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18 || ^19 || ^19.0.0-rc",
|
||||
"react-dom": "^18 || ^19 || ^19.0.0-rc"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
|
||||
@@ -42,8 +42,9 @@
|
||||
"@opentelemetry/exporter-trace-otlp-http": "^0.208.0",
|
||||
"@opentelemetry/sdk-trace-node": "^2.2.0",
|
||||
"@radix-ui/react-collapsible": "^1.1.12",
|
||||
"@radix-ui/react-dialog": "^1.1.6",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-label": "^2.1.8",
|
||||
"@radix-ui/react-popover": "^1.1.15",
|
||||
"@radix-ui/react-scroll-area": "^1.2.3",
|
||||
"@radix-ui/react-select": "^2.2.6",
|
||||
"@radix-ui/react-slot": "^1.1.2",
|
||||
@@ -55,6 +56,7 @@
|
||||
"base-64": "^1.0.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"js-tiktoken": "^1.0.21",
|
||||
"jsdom": "^26.0.0",
|
||||
"jsonrepair": "^3.13.1",
|
||||
|
||||
Reference in New Issue
Block a user