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 { Toaster, toast } from "sonner"
|
||||||
import { ButtonWithTooltip } from "@/components/button-with-tooltip"
|
import { ButtonWithTooltip } from "@/components/button-with-tooltip"
|
||||||
import { ChatInput } from "@/components/chat-input"
|
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 { ResetWarningModal } from "@/components/reset-warning-modal"
|
||||||
import { SettingsDialog } from "@/components/settings-dialog"
|
import { SettingsDialog } from "@/components/settings-dialog"
|
||||||
import { useDiagram } from "@/contexts/diagram-context"
|
import { useDiagram } from "@/contexts/diagram-context"
|
||||||
import { useDictionary } from "@/hooks/use-dictionary"
|
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 { findCachedResponse } from "@/lib/cached-responses"
|
||||||
import { isPdfFile, isTextFile } from "@/lib/pdf-utils"
|
import { isPdfFile, isTextFile } from "@/lib/pdf-utils"
|
||||||
import { type FileData, useFileProcessor } from "@/lib/use-file-processor"
|
import { type FileData, useFileProcessor } from "@/lib/use-file-processor"
|
||||||
@@ -146,7 +148,11 @@ export default function ChatPanel({
|
|||||||
|
|
||||||
const [showHistory, setShowHistory] = useState(false)
|
const [showHistory, setShowHistory] = useState(false)
|
||||||
const [showSettingsDialog, setShowSettingsDialog] = useState(false)
|
const [showSettingsDialog, setShowSettingsDialog] = useState(false)
|
||||||
|
const [showModelConfigDialog, setShowModelConfigDialog] = useState(false)
|
||||||
const [, setAccessCodeRequired] = useState(false)
|
const [, setAccessCodeRequired] = useState(false)
|
||||||
|
|
||||||
|
// Model configuration hook
|
||||||
|
const modelConfig = useModelConfig()
|
||||||
const [input, setInput] = useState("")
|
const [input, setInput] = useState("")
|
||||||
const [dailyRequestLimit, setDailyRequestLimit] = useState(0)
|
const [dailyRequestLimit, setDailyRequestLimit] = useState(0)
|
||||||
const [dailyTokenLimit, setDailyTokenLimit] = useState(0)
|
const [dailyTokenLimit, setDailyTokenLimit] = useState(0)
|
||||||
@@ -1019,7 +1025,7 @@ Continue from EXACTLY where you stopped.`,
|
|||||||
autoRetryCountRef.current = 0
|
autoRetryCountRef.current = 0
|
||||||
partialXmlRef.current = ""
|
partialXmlRef.current = ""
|
||||||
|
|
||||||
const config = getAIConfig()
|
const config = getSelectedAIConfig()
|
||||||
|
|
||||||
sendMessage(
|
sendMessage(
|
||||||
{ parts },
|
{ parts },
|
||||||
@@ -1298,6 +1304,16 @@ Continue from EXACTLY where you stopped.`,
|
|||||||
className={`${isMobile ? "w-4 h-4" : "w-5 h-5"}`}
|
className={`${isMobile ? "w-4 h-4" : "w-5 h-5"}`}
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
|
{!isMobile && modelConfig.isLoaded && (
|
||||||
|
<ModelSelector
|
||||||
|
models={modelConfig.models}
|
||||||
|
selectedModelId={modelConfig.selectedModelId}
|
||||||
|
onSelect={modelConfig.setSelectedModelId}
|
||||||
|
onConfigure={() =>
|
||||||
|
setShowModelConfigDialog(true)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<ButtonWithTooltip
|
<ButtonWithTooltip
|
||||||
tooltipContent={dict.nav.settings}
|
tooltipContent={dict.nav.settings}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@@ -1368,12 +1384,19 @@ Continue from EXACTLY where you stopped.`,
|
|||||||
open={showSettingsDialog}
|
open={showSettingsDialog}
|
||||||
onOpenChange={setShowSettingsDialog}
|
onOpenChange={setShowSettingsDialog}
|
||||||
onCloseProtectionChange={onCloseProtectionChange}
|
onCloseProtectionChange={onCloseProtectionChange}
|
||||||
|
onOpenModelConfig={() => setShowModelConfigDialog(true)}
|
||||||
drawioUi={drawioUi}
|
drawioUi={drawioUi}
|
||||||
onToggleDrawioUi={onToggleDrawioUi}
|
onToggleDrawioUi={onToggleDrawioUi}
|
||||||
darkMode={darkMode}
|
darkMode={darkMode}
|
||||||
onToggleDarkMode={onToggleDarkMode}
|
onToggleDarkMode={onToggleDarkMode}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<ModelConfigDialog
|
||||||
|
open={showModelConfigDialog}
|
||||||
|
onOpenChange={setShowModelConfigDialog}
|
||||||
|
modelConfig={modelConfig}
|
||||||
|
/>
|
||||||
|
|
||||||
<ResetWarningModal
|
<ResetWarningModal
|
||||||
open={showNewChatDialog}
|
open={showNewChatDialog}
|
||||||
onOpenChange={setShowNewChatDialog}
|
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"
|
"use client"
|
||||||
|
|
||||||
import { Moon, Sun } from "lucide-react"
|
import { Moon, Settings2, Sun } from "lucide-react"
|
||||||
import { useEffect, useState } from "react"
|
import { useEffect, useState } from "react"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import {
|
import {
|
||||||
@@ -12,13 +12,6 @@ import {
|
|||||||
} from "@/components/ui/dialog"
|
} from "@/components/ui/dialog"
|
||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
import { Label } from "@/components/ui/label"
|
import { Label } from "@/components/ui/label"
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "@/components/ui/select"
|
|
||||||
import { Switch } from "@/components/ui/switch"
|
import { Switch } from "@/components/ui/switch"
|
||||||
import { useDictionary } from "@/hooks/use-dictionary"
|
import { useDictionary } from "@/hooks/use-dictionary"
|
||||||
|
|
||||||
@@ -26,6 +19,7 @@ interface SettingsDialogProps {
|
|||||||
open: boolean
|
open: boolean
|
||||||
onOpenChange: (open: boolean) => void
|
onOpenChange: (open: boolean) => void
|
||||||
onCloseProtectionChange?: (enabled: boolean) => void
|
onCloseProtectionChange?: (enabled: boolean) => void
|
||||||
|
onOpenModelConfig: () => void
|
||||||
drawioUi: "min" | "sketch"
|
drawioUi: "min" | "sketch"
|
||||||
onToggleDrawioUi: () => void
|
onToggleDrawioUi: () => void
|
||||||
darkMode: boolean
|
darkMode: boolean
|
||||||
@@ -35,10 +29,6 @@ interface SettingsDialogProps {
|
|||||||
export const STORAGE_ACCESS_CODE_KEY = "next-ai-draw-io-access-code"
|
export const STORAGE_ACCESS_CODE_KEY = "next-ai-draw-io-access-code"
|
||||||
export const STORAGE_CLOSE_PROTECTION_KEY = "next-ai-draw-io-close-protection"
|
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"
|
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 {
|
function getStoredAccessCodeRequired(): boolean | null {
|
||||||
if (typeof window === "undefined") return null
|
if (typeof window === "undefined") return null
|
||||||
@@ -51,6 +41,7 @@ export function SettingsDialog({
|
|||||||
open,
|
open,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
onCloseProtectionChange,
|
onCloseProtectionChange,
|
||||||
|
onOpenModelConfig,
|
||||||
drawioUi,
|
drawioUi,
|
||||||
onToggleDrawioUi,
|
onToggleDrawioUi,
|
||||||
darkMode,
|
darkMode,
|
||||||
@@ -64,10 +55,6 @@ export function SettingsDialog({
|
|||||||
const [accessCodeRequired, setAccessCodeRequired] = useState(
|
const [accessCodeRequired, setAccessCodeRequired] = useState(
|
||||||
() => getStoredAccessCodeRequired() ?? false,
|
() => getStoredAccessCodeRequired() ?? false,
|
||||||
)
|
)
|
||||||
const [provider, setProvider] = useState("")
|
|
||||||
const [baseUrl, setBaseUrl] = useState("")
|
|
||||||
const [apiKey, setApiKey] = useState("")
|
|
||||||
const [modelId, setModelId] = useState("")
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Only fetch if not cached in localStorage
|
// Only fetch if not cached in localStorage
|
||||||
@@ -104,12 +91,6 @@ export function SettingsDialog({
|
|||||||
// Default to true if not set
|
// Default to true if not set
|
||||||
setCloseProtection(storedCloseProtection !== "false")
|
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("")
|
setError("")
|
||||||
}
|
}
|
||||||
}, [open])
|
}, [open])
|
||||||
@@ -197,188 +178,24 @@ export function SettingsDialog({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="space-y-2">
|
<div className="flex items-center justify-between">
|
||||||
<Label>{dict.settings.aiProvider}</Label>
|
<div className="space-y-0.5">
|
||||||
<p className="text-[0.8rem] text-muted-foreground">
|
<Label>{dict.settings.aiProvider}</Label>
|
||||||
{dict.settings.aiProviderDescription}
|
<p className="text-[0.8rem] text-muted-foreground">
|
||||||
</p>
|
{dict.settings.aiProviderDescription}
|
||||||
<div className="space-y-3 pt-2">
|
</p>
|
||||||
<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>
|
</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>
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
<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",
|
"seekingSponsorship": "Call for Sponsorship",
|
||||||
"contactMe": "Contact Me",
|
"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."
|
"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": "スポンサー募集",
|
"seekingSponsorship": "スポンサー募集",
|
||||||
"contactMe": "お問い合わせ",
|
"contactMe": "お問い合わせ",
|
||||||
"usageNotice": "利用量の増加に伴い、コスト削減のためモデルを Claude から minimax-m2 に変更し、いくつかの利用制限を設けました。詳細は概要ページをご覧ください。"
|
"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": "寻求赞助(求大佬捞一把)",
|
"seekingSponsorship": "寻求赞助(求大佬捞一把)",
|
||||||
"contactMe": "联系我",
|
"contactMe": "联系我",
|
||||||
"usageNotice": "由于使用量过高,我已将模型从 Claude 更换为 minimax-m2,并设置了一些用量限制。详情请查看关于页面。"
|
"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",
|
aiBaseUrl: "next-ai-draw-io-ai-base-url",
|
||||||
aiApiKey: "next-ai-draw-io-ai-api-key",
|
aiApiKey: "next-ai-draw-io-ai-api-key",
|
||||||
aiModel: "next-ai-draw-io-ai-model",
|
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
|
} 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/exporter-trace-otlp-http": "^0.208.0",
|
||||||
"@opentelemetry/sdk-trace-node": "^2.2.0",
|
"@opentelemetry/sdk-trace-node": "^2.2.0",
|
||||||
"@radix-ui/react-collapsible": "^1.1.12",
|
"@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-label": "^2.1.8",
|
||||||
|
"@radix-ui/react-popover": "^1.1.15",
|
||||||
"@radix-ui/react-scroll-area": "^1.2.3",
|
"@radix-ui/react-scroll-area": "^1.2.3",
|
||||||
"@radix-ui/react-select": "^2.2.6",
|
"@radix-ui/react-select": "^2.2.6",
|
||||||
"@radix-ui/react-slot": "^1.1.2",
|
"@radix-ui/react-slot": "^1.1.2",
|
||||||
@@ -40,6 +41,7 @@
|
|||||||
"base-64": "^1.0.0",
|
"base-64": "^1.0.0",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"cmdk": "^1.1.1",
|
||||||
"js-tiktoken": "^1.0.21",
|
"js-tiktoken": "^1.0.21",
|
||||||
"jsdom": "^26.0.0",
|
"jsdom": "^26.0.0",
|
||||||
"jsonrepair": "^3.13.1",
|
"jsonrepair": "^3.13.1",
|
||||||
@@ -4655,23 +4657,23 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@radix-ui/react-dialog": {
|
"node_modules/@radix-ui/react-dialog": {
|
||||||
"version": "1.1.6",
|
"version": "1.1.15",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.6.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz",
|
||||||
"integrity": "sha512-/IVhJV5AceX620DUJ4uYVMymzsipdKBzo3edo+omeskCKGm9FRHM0ebIdbPnlQVJqyuHbuBltQUOG2mOTq2IYw==",
|
"integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/primitive": "1.1.1",
|
"@radix-ui/primitive": "1.1.3",
|
||||||
"@radix-ui/react-compose-refs": "1.1.1",
|
"@radix-ui/react-compose-refs": "1.1.2",
|
||||||
"@radix-ui/react-context": "1.1.1",
|
"@radix-ui/react-context": "1.1.2",
|
||||||
"@radix-ui/react-dismissable-layer": "1.1.5",
|
"@radix-ui/react-dismissable-layer": "1.1.11",
|
||||||
"@radix-ui/react-focus-guards": "1.1.1",
|
"@radix-ui/react-focus-guards": "1.1.3",
|
||||||
"@radix-ui/react-focus-scope": "1.1.2",
|
"@radix-ui/react-focus-scope": "1.1.7",
|
||||||
"@radix-ui/react-id": "1.1.0",
|
"@radix-ui/react-id": "1.1.1",
|
||||||
"@radix-ui/react-portal": "1.1.4",
|
"@radix-ui/react-portal": "1.1.9",
|
||||||
"@radix-ui/react-presence": "1.1.2",
|
"@radix-ui/react-presence": "1.1.5",
|
||||||
"@radix-ui/react-primitive": "2.0.2",
|
"@radix-ui/react-primitive": "2.1.3",
|
||||||
"@radix-ui/react-slot": "1.1.2",
|
"@radix-ui/react-slot": "1.2.3",
|
||||||
"@radix-ui/react-use-controllable-state": "1.1.0",
|
"@radix-ui/react-use-controllable-state": "1.2.2",
|
||||||
"aria-hidden": "^1.2.4",
|
"aria-hidden": "^1.2.4",
|
||||||
"react-remove-scroll": "^2.6.3"
|
"react-remove-scroll": "^2.6.3"
|
||||||
},
|
},
|
||||||
@@ -4690,14 +4692,214 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-use-controllable-state": {
|
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/primitive": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
|
||||||
"integrity": "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==",
|
"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",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"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": {
|
"peerDependencies": {
|
||||||
"@types/react": "*",
|
"@types/react": "*",
|
||||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
@@ -4751,9 +4953,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@radix-ui/react-focus-guards": {
|
"node_modules/@radix-ui/react-focus-guards": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz",
|
||||||
"integrity": "sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==",
|
"integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "*",
|
"@types/react": "*",
|
||||||
@@ -4766,14 +4968,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@radix-ui/react-focus-scope": {
|
"node_modules/@radix-ui/react-focus-scope": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.7",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz",
|
||||||
"integrity": "sha512-zxwE80FCU7lcXUGWkdt6XpTTCKPitG1XKOwViTxHVKIJhZl9MvIl2dVHeZENCWD9+EdWv05wlaEkRXUykU27RA==",
|
"integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-compose-refs": "1.1.1",
|
"@radix-ui/react-compose-refs": "1.1.2",
|
||||||
"@radix-ui/react-primitive": "2.0.2",
|
"@radix-ui/react-primitive": "2.1.3",
|
||||||
"@radix-ui/react-use-callback-ref": "1.1.0"
|
"@radix-ui/react-use-callback-ref": "1.1.1"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "*",
|
"@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": {
|
"node_modules/@radix-ui/react-id": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.0.tgz",
|
"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": {
|
"node_modules/@radix-ui/react-popper": {
|
||||||
"version": "1.2.2",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.2.tgz",
|
"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": {
|
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-id": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz",
|
||||||
@@ -8853,6 +9438,22 @@
|
|||||||
"node": ">=6"
|
"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": {
|
"node_modules/color-convert": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
"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/exporter-trace-otlp-http": "^0.208.0",
|
||||||
"@opentelemetry/sdk-trace-node": "^2.2.0",
|
"@opentelemetry/sdk-trace-node": "^2.2.0",
|
||||||
"@radix-ui/react-collapsible": "^1.1.12",
|
"@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-label": "^2.1.8",
|
||||||
|
"@radix-ui/react-popover": "^1.1.15",
|
||||||
"@radix-ui/react-scroll-area": "^1.2.3",
|
"@radix-ui/react-scroll-area": "^1.2.3",
|
||||||
"@radix-ui/react-select": "^2.2.6",
|
"@radix-ui/react-select": "^2.2.6",
|
||||||
"@radix-ui/react-slot": "^1.1.2",
|
"@radix-ui/react-slot": "^1.1.2",
|
||||||
@@ -55,6 +56,7 @@
|
|||||||
"base-64": "^1.0.0",
|
"base-64": "^1.0.0",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"cmdk": "^1.1.1",
|
||||||
"js-tiktoken": "^1.0.21",
|
"js-tiktoken": "^1.0.21",
|
||||||
"jsdom": "^26.0.0",
|
"jsdom": "^26.0.0",
|
||||||
"jsonrepair": "^3.13.1",
|
"jsonrepair": "^3.13.1",
|
||||||
|
|||||||
Reference in New Issue
Block a user