Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions app/api/user-key-status/route.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { PROVIDERS } from "@/lib/providers"
import { createClient } from "@/lib/supabase/server"
import { NextResponse } from "next/server"

// Supported providers
const SUPPORTED_PROVIDERS = ["openrouter", "openai"]
const SUPPORTED_PROVIDERS = PROVIDERS.map((p) => p.id)

export async function GET() {
try {
Expand Down
52 changes: 48 additions & 4 deletions app/components/layout/settings/apikeys/byok-section.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
"use client"

import ClaudeIcon from "@/components/icons/claude"
import GoogleIcon from "@/components/icons/google"
import MistralIcon from "@/components/icons/mistral"
import OpenAIIcon from "@/components/icons/openai"
import OpenRouterIcon from "@/components/icons/openrouter"
import PerplexityIcon from "@/components/icons/perplexity"
import XaiIcon from "@/components/icons/xai"
import {
AlertDialog,
AlertDialogAction,
Expand All @@ -11,7 +16,6 @@ import {
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
Expand Down Expand Up @@ -51,6 +55,46 @@ const PROVIDERS: Provider[] = [
getKeyUrl: "https://platform.openai.com/api-keys",
defaultKey: "sk-............",
},
{
id: "mistral",
name: "Mistral",
icon: MistralIcon,
placeholder: "...",
getKeyUrl: "https://console.mistral.ai/api-keys/",
defaultKey: "............",
},
{
id: "google",
name: "Google",
icon: GoogleIcon,
placeholder: "AIza...",
getKeyUrl: "https://ai.google.dev/gemini-api/docs/api-key",
defaultKey: "AIza............",
},
{
id: "perplexity",
name: "Perplexity",
icon: PerplexityIcon,
placeholder: "pplx-...",
getKeyUrl: "https://docs.perplexity.ai/guides/getting-started",
defaultKey: "pplx-............",
},
{
id: "xai",
name: "XAI",
icon: XaiIcon,
placeholder: "xai-...",
getKeyUrl: "https://console.x.ai/",
defaultKey: "xai-............",
},
{
id: "anthropic",
name: "Claude",
icon: ClaudeIcon,
placeholder: "sk-ant-...",
getKeyUrl: "https://console.anthropic.com/settings/keys",
defaultKey: "sk-ant-............",
},
]

export function ByokSection() {
Expand Down Expand Up @@ -176,14 +220,14 @@ export function ByokSection() {
Your keys are stored securely with end-to-end encryption.
</p>

<div className="mt-4 flex flex-row items-start justify-start gap-3">
<div className="mt-4 -ml-2 flex flex-row items-start justify-start gap-3 overflow-x-auto mask-x-from-98% mask-x-to-100% px-2 py-2">
{PROVIDERS.map((provider) => (
<button
key={provider.id}
type="button"
onClick={() => setSelectedProvider(provider.id)}
className={cn(
"flex aspect-square w-28 flex-col items-center justify-center gap-2 rounded-lg border p-4",
"flex aspect-square min-w-28 flex-col items-center justify-center gap-2 rounded-lg border p-4",
selectedProvider === provider.id
? "border-primary ring-primary/30 ring-2"
: "border-border"
Expand All @@ -198,7 +242,7 @@ export function ByokSection() {
type="button"
disabled
className={cn(
"flex aspect-square w-28 flex-col items-center justify-center gap-2 rounded-lg border p-4 opacity-20",
"flex aspect-square min-w-28 flex-col items-center justify-center gap-2 rounded-lg border p-4 opacity-20",
"border-primary border-dashed"
)}
>
Expand Down
137 changes: 137 additions & 0 deletions app/components/layout/settings/models/model-visibility-settings.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
"use client"

import { Switch } from "@/components/ui/switch"
import { useModel } from "@/lib/model-store/provider"
import { PROVIDERS } from "@/lib/providers"
import { useUserPreferences } from "@/lib/user-preference-store/provider"
import { useState } from "react"

export function ModelVisibilitySettings() {
const { models } = useModel()
const { toggleModelVisibility, isModelHidden } = useUserPreferences()
const [searchQuery, setSearchQuery] = useState("")
const [optimisticStates, setOptimisticStates] = useState<
Record<string, boolean>
>({})

const filteredModels = models.filter((model) =>
model.name.toLowerCase().includes(searchQuery.toLowerCase())
)

// Group models by icon (real provider type) and keep all models including duplicates from different providers
const modelsByProvider = filteredModels.reduce(
(acc, model) => {
const iconKey = model.icon || "unknown"

if (!acc[iconKey]) {
acc[iconKey] = []
}

acc[iconKey].push(model)

return acc
},
{} as Record<string, typeof models>
)

const handleToggle = (modelId: string) => {
// Optimistic update
const currentState =
optimisticStates[modelId] !== undefined
? optimisticStates[modelId]
: !isModelHidden(modelId)

setOptimisticStates((prev) => ({
...prev,
[modelId]: !currentState,
}))

// Actual update
toggleModelVisibility(modelId)
}

const getModelVisibility = (modelId: string) => {
return optimisticStates[modelId] !== undefined
? optimisticStates[modelId]
: !isModelHidden(modelId)
}

return (
<div>
<h3 className="mb-2 text-lg font-medium">Model</h3>
<p className="text-muted-foreground mb-4 text-sm">
Choose which models appear in your model selector.
</p>

{/* Search input */}
<div className="mb-4">
<input
type="text"
placeholder="Search models..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex h-9 w-full rounded-md border px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:ring-1 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
/>
</div>

{/* Models grouped by icon/type */}
<div className="space-y-6 pb-6">
{Object.entries(modelsByProvider).map(([iconKey, modelsGroup]) => {
const firstModel = modelsGroup[0]
const provider = PROVIDERS.find((p) => p.id === firstModel.icon)

return (
<div key={iconKey} className="space-y-3">
<div className="flex items-center gap-2">
{provider?.icon && <provider.icon className="size-5" />}
<h4 className="font-medium">{provider?.name || iconKey}</h4>
<span className="text-muted-foreground text-sm">
({modelsGroup.length} models)
</span>
</div>

<div className="space-y-2 pl-7">
{modelsGroup.map((model) => {
const modelProvider = PROVIDERS.find(
(p) => p.id === model.provider
)

return (
<div
key={model.id}
className="flex items-center justify-between py-1"
>
<div className="flex flex-col">
<div className="flex items-center gap-2">
<span className="text-sm">{model.name}</span>
<span className="text-muted-foreground bg-muted rounded px-1.5 py-0.5 text-xs">
via {modelProvider?.name || model.provider}
</span>
</div>
{model.description && (
<span className="text-muted-foreground text-xs">
{model.description}
</span>
)}
</div>
<Switch
checked={getModelVisibility(model.id)}
onCheckedChange={() => handleToggle(model.id)}
/>
</div>
)
})}
</div>
</div>
)
})}
</div>

{filteredModels.length === 0 && (
<div className="text-muted-foreground py-8 text-center text-sm">
No models found matching your search.
</div>
)}
</div>
)
}
28 changes: 27 additions & 1 deletion app/components/layout/settings/settings-content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { isSupabaseEnabled } from "@/lib/supabase/config"
import { cn, isDev } from "@/lib/utils"
import {
CubeIcon,
GearSixIcon,
KeyIcon,
PaintBrushIcon,
Expand All @@ -22,13 +23,14 @@ import { OllamaSection } from "./connections/ollama-section"
import { AccountManagement } from "./general/account-management"
import { ModelPreferences } from "./general/model-preferences"
import { UserProfile } from "./general/user-profile"
import { ModelVisibilitySettings } from "./models/model-visibility-settings"

type SettingsContentProps = {
onClose: () => void
isDrawer?: boolean
}

type TabType = "general" | "appearance" | "connections"
type TabType = "general" | "appearance" | "models" | "connections"

export function SettingsContent({
onClose,
Expand Down Expand Up @@ -86,6 +88,13 @@ export function SettingsContent({
<KeyIcon className="size-4" />
<span>API Keys</span>
</TabsTrigger>
<TabsTrigger
value="models"
className="flex shrink-0 items-center gap-2"
>
<CubeIcon className="size-4" />
<span>Models</span>
</TabsTrigger>
<TabsTrigger
value="connections"
className="flex shrink-0 items-center gap-2"
Expand Down Expand Up @@ -117,6 +126,10 @@ export function SettingsContent({
<ByokSection />
</TabsContent>

<TabsContent value="models" className="px-6">
<ModelVisibilitySettings />
</TabsContent>

<TabsContent value="connections" className="space-y-6 px-6">
{!isDev && <ConnectionsPlaceholder />}
{isDev && <OllamaSection />}
Expand Down Expand Up @@ -157,6 +170,15 @@ export function SettingsContent({
<span>API Keys</span>
</div>
</TabsTrigger>
<TabsTrigger
value="models"
className="w-full justify-start rounded-md px-3 py-2 text-left"
>
<div className="flex items-center gap-2">
<CubeIcon className="size-4" />
<span>Models</span>
</div>
</TabsTrigger>
<TabsTrigger
value="connections"
className="w-full justify-start rounded-md px-3 py-2 text-left"
Expand Down Expand Up @@ -191,6 +213,10 @@ export function SettingsContent({
<ByokSection />
</TabsContent>

<TabsContent value="models" className="mt-0 space-y-6">
<ModelVisibilitySettings />
</TabsContent>

<TabsContent value="connections" className="mt-0 space-y-6">
{!isDev && <ConnectionsPlaceholder />}
{isDev && <OllamaSection />}
Expand Down
3 changes: 3 additions & 0 deletions components/common/model-selector/base.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { FREE_MODELS_IDS } from "@/lib/config"
import { useModel } from "@/lib/model-store/provider"
import { ModelConfig } from "@/lib/models/types"
import { PROVIDERS } from "@/lib/providers"
import { useUserPreferences } from "@/lib/user-preference-store/provider"
import { cn } from "@/lib/utils"
import {
CaretDownIcon,
Expand All @@ -52,6 +53,7 @@ export function ModelSelector({
isUserAuthenticated = true,
}: ModelSelectorProps) {
const { models, isLoading: isLoadingModels } = useModel()
const { isModelHidden } = useUserPreferences()

const currentModel = models.find((model) => model.id === selectedModelId)
const currentProvider = PROVIDERS.find(
Expand Down Expand Up @@ -124,6 +126,7 @@ export function ModelSelector({
const hoveredModelData = models.find((model) => model.id === hoveredModel)

const filteredModels = models
.filter((model) => !isModelHidden(model.id))
.filter((model) =>
model.name.toLowerCase().includes(searchQuery.toLowerCase())
)
Expand Down
20 changes: 19 additions & 1 deletion lib/model-store/provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ import {
type UserKeyStatus = {
openrouter: boolean
openai: boolean
mistral: boolean
google: boolean
perplexity: boolean
xai: boolean
anthropic: boolean
[key: string]: boolean // Allow for additional providers
}

Expand All @@ -32,6 +37,11 @@ export function ModelProvider({ children }: { children: React.ReactNode }) {
const [userKeyStatus, setUserKeyStatus] = useState<UserKeyStatus>({
openrouter: false,
openai: false,
mistral: false,
google: false,
perplexity: false,
xai: false,
anthropic: false,
})
const [isLoading, setIsLoading] = useState(false)

Expand All @@ -57,7 +67,15 @@ export function ModelProvider({ children }: { children: React.ReactNode }) {
} catch (error) {
console.error("Failed to fetch user key status:", error)
// Set default values on error
setUserKeyStatus({ openrouter: false, openai: false })
setUserKeyStatus({
openrouter: false,
openai: false,
mistral: false,
google: false,
perplexity: false,
xai: false,
anthropic: false,
})
}
}, [])

Expand Down
2 changes: 1 addition & 1 deletion lib/providers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import Mistral from "@/components/icons/mistral"
import Ollama from "@/components/icons/ollama"
import OpenAI from "@/components/icons/openai"
import OpenRouter from "@/components/icons/openrouter"
import Xai from "@/components/icons/xai"
import Preplexity from "@/components/icons/perplexity"
import Xai from "@/components/icons/xai"

export type Provider = {
id: string
Expand Down
Loading