mirror of
https://github.com/browseros-ai/BrowserOS.git
synced 2026-05-18 02:57:47 +00:00
Compare commits
1 Commits
fix/releas
...
codex/0318
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
727136f92f |
@@ -31,6 +31,7 @@ import type { LlmProviderConfig } from '@/lib/llm-providers/types'
|
||||
import { useLlmProviders } from '@/lib/llm-providers/useLlmProviders'
|
||||
import { useOAuthStatus } from '@/lib/llm-providers/useOAuthStatus'
|
||||
import { track } from '@/lib/metrics/track'
|
||||
import { ChatGPTProFeatureCard } from './ChatGPTProFeatureCard'
|
||||
import { ConfiguredProvidersList } from './ConfiguredProvidersList'
|
||||
import {
|
||||
DeleteRemoteLlmProviderDocument,
|
||||
@@ -114,9 +115,12 @@ export const AISettingsPage: FC = () => {
|
||||
// OAuth status for ChatGPT Plus/Pro
|
||||
const {
|
||||
status: chatgptProStatus,
|
||||
isPolling: isChatGPTProPolling,
|
||||
startPolling: startChatGPTProPolling,
|
||||
disconnect: disconnectChatGPTPro,
|
||||
} = useOAuthStatus('chatgpt-pro')
|
||||
const chatgptProProvider = providers.find((p) => p.type === 'chatgpt-pro')
|
||||
const isChatGPTProDefault = defaultProviderId === chatgptProProvider?.id
|
||||
|
||||
// Track whether user explicitly started an OAuth flow this session
|
||||
const oauthFlowStartedRef = useRef(false)
|
||||
@@ -329,6 +333,25 @@ export const AISettingsPage: FC = () => {
|
||||
onAddProvider={handleAddProvider}
|
||||
/>
|
||||
|
||||
<ChatGPTProFeatureCard
|
||||
provider={chatgptProProvider}
|
||||
email={chatgptProStatus?.email}
|
||||
isAuthenticated={chatgptProStatus?.authenticated === true}
|
||||
isPolling={isChatGPTProPolling}
|
||||
isDefault={isChatGPTProDefault}
|
||||
onConnect={handleStartChatGPTProOAuth}
|
||||
onDisconnect={() => {
|
||||
if (chatgptProProvider) {
|
||||
handleDeleteProvider(chatgptProProvider)
|
||||
}
|
||||
}}
|
||||
onMakeDefault={() => {
|
||||
if (chatgptProProvider) {
|
||||
handleSelectProvider(chatgptProProvider.id)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<ProviderTemplatesSection onUseTemplate={handleUseTemplate} />
|
||||
|
||||
<ConfiguredProvidersList
|
||||
|
||||
@@ -0,0 +1,298 @@
|
||||
import {
|
||||
ArrowRight,
|
||||
CheckCircle2,
|
||||
ExternalLink,
|
||||
Loader2,
|
||||
ShieldCheck,
|
||||
Sparkles,
|
||||
Unplug,
|
||||
} from 'lucide-react'
|
||||
import type { FC } from 'react'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { ProviderIcon } from '@/lib/llm-providers/providerIcons'
|
||||
import { getProviderTemplate } from '@/lib/llm-providers/providerTemplates'
|
||||
import type { LlmProviderConfig } from '@/lib/llm-providers/types'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface ChatGPTProFeatureCardProps {
|
||||
provider?: LlmProviderConfig
|
||||
email?: string
|
||||
isAuthenticated: boolean
|
||||
isPolling: boolean
|
||||
isDefault: boolean
|
||||
onConnect: () => void
|
||||
onDisconnect: () => void
|
||||
onMakeDefault: () => void
|
||||
}
|
||||
|
||||
type ChatGPTProState =
|
||||
| 'disconnected'
|
||||
| 'connecting'
|
||||
| 'provisioning'
|
||||
| 'connected'
|
||||
|
||||
function getChatGPTProState(
|
||||
isAuthenticated: boolean,
|
||||
isPolling: boolean,
|
||||
provider?: LlmProviderConfig,
|
||||
): ChatGPTProState {
|
||||
if (isPolling) return 'connecting'
|
||||
if (isAuthenticated && provider) return 'connected'
|
||||
if (isAuthenticated) return 'provisioning'
|
||||
return 'disconnected'
|
||||
}
|
||||
|
||||
function getStateCopy(state: ChatGPTProState, isDefault: boolean) {
|
||||
switch (state) {
|
||||
case 'connecting':
|
||||
return {
|
||||
badge: 'Waiting for sign-in',
|
||||
title: 'Finish the login in the opened ChatGPT tab',
|
||||
description:
|
||||
'BrowserOS is polling for completion and will finish setup automatically once your ChatGPT account is authenticated.',
|
||||
}
|
||||
case 'provisioning':
|
||||
return {
|
||||
badge: 'Finalizing setup',
|
||||
title: 'Authentication succeeded. Creating your provider now.',
|
||||
description:
|
||||
'The OAuth handshake is done. BrowserOS is applying the local provider configuration so the account can be used inside the extension.',
|
||||
}
|
||||
case 'connected':
|
||||
return {
|
||||
badge: isDefault ? 'Connected and default' : 'Connected',
|
||||
title: isDefault
|
||||
? 'Your ChatGPT Plus/Pro account is ready to use'
|
||||
: 'Your ChatGPT Plus/Pro account is connected',
|
||||
description: isDefault
|
||||
? 'This provider is already the default model route for BrowserOS chats.'
|
||||
: 'You can make it the default provider or keep it available alongside your other models.',
|
||||
}
|
||||
default:
|
||||
return {
|
||||
badge: 'Not connected',
|
||||
title: 'Connect ChatGPT Plus/Pro without managing API keys',
|
||||
description:
|
||||
'Use your ChatGPT subscription directly inside BrowserOS with managed OAuth, GPT-5/Codex-ready models, and the same provider list as the rest of your setup.',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const ChatGPTProFeatureCard: FC<ChatGPTProFeatureCardProps> = ({
|
||||
provider,
|
||||
email,
|
||||
isAuthenticated,
|
||||
isPolling,
|
||||
isDefault,
|
||||
onConnect,
|
||||
onDisconnect,
|
||||
onMakeDefault,
|
||||
}) => {
|
||||
const state = getChatGPTProState(isAuthenticated, isPolling, provider)
|
||||
const copy = getStateCopy(state, isDefault)
|
||||
const setupGuideUrl = getProviderTemplate('chatgpt-pro')?.setupGuideUrl
|
||||
const detailChips = [
|
||||
provider?.modelId ?? 'GPT-5 / Codex-ready',
|
||||
email ?? 'Managed OAuth',
|
||||
'No local API key',
|
||||
]
|
||||
|
||||
return (
|
||||
<section className="relative overflow-hidden rounded-[28px] border border-orange-300/70 bg-[linear-gradient(135deg,rgba(255,247,239,0.98),rgba(240,251,247,0.96))] p-6 shadow-[0_20px_50px_-34px_rgba(191,98,22,0.45)] dark:border-orange-400/20 dark:bg-[linear-gradient(135deg,rgba(63,38,23,0.82),rgba(18,43,38,0.88))]">
|
||||
<div className="pointer-events-none absolute inset-y-0 right-0 w-1/2 bg-[radial-gradient(circle_at_top_right,rgba(16,163,127,0.18),transparent_60%)] dark:bg-[radial-gradient(circle_at_top_right,rgba(16,163,127,0.22),transparent_62%)]" />
|
||||
<div className="relative flex flex-col gap-6 xl:flex-row xl:items-end xl:justify-between">
|
||||
<div className="max-w-3xl space-y-5">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Badge className="border-transparent bg-foreground text-background">
|
||||
Featured integration
|
||||
</Badge>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="border-orange-400/40 bg-white/[0.65] text-foreground dark:bg-white/10"
|
||||
>
|
||||
{copy.badge}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="flex h-14 w-14 shrink-0 items-center justify-center rounded-2xl border border-white/60 bg-white/80 shadow-sm dark:border-white/10 dark:bg-white/10">
|
||||
<ProviderIcon
|
||||
type="chatgpt-pro"
|
||||
size={30}
|
||||
className="text-[#10a37f]"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<p className="font-semibold text-lg tracking-tight">
|
||||
ChatGPT Plus/Pro
|
||||
</p>
|
||||
<h3 className="font-semibold text-3xl leading-tight tracking-tight">
|
||||
{copy.title}
|
||||
</h3>
|
||||
</div>
|
||||
<p className="max-w-2xl text-[15px] text-foreground/75 leading-6 dark:text-foreground/80">
|
||||
{copy.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3 md:grid-cols-3">
|
||||
<div className="rounded-2xl border border-white/60 bg-white/70 p-4 shadow-sm dark:border-white/10 dark:bg-white/[0.08]">
|
||||
<div className="mb-2 flex items-center gap-2 font-medium text-sm">
|
||||
<ShieldCheck className="h-4 w-4 text-[#10a37f]" />
|
||||
Managed access
|
||||
</div>
|
||||
<p className="text-muted-foreground text-sm leading-5">
|
||||
Start with an OAuth login instead of copying keys or endpoint
|
||||
values into the extension.
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-2xl border border-white/60 bg-white/70 p-4 shadow-sm dark:border-white/10 dark:bg-white/[0.08]">
|
||||
<div className="mb-2 flex items-center gap-2 font-medium text-sm">
|
||||
<Sparkles className="h-4 w-4 text-[var(--accent-orange)]" />
|
||||
Premium models
|
||||
</div>
|
||||
<p className="text-muted-foreground text-sm leading-5">
|
||||
Keep ChatGPT Plus/Pro available for GPT-5 and Codex-style work
|
||||
inside BrowserOS.
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-2xl border border-white/60 bg-white/70 p-4 shadow-sm dark:border-white/10 dark:bg-white/[0.08]">
|
||||
<div className="mb-2 flex items-center gap-2 font-medium text-sm">
|
||||
<CheckCircle2 className="h-4 w-4 text-[var(--accent-orange)]" />
|
||||
Ready in settings
|
||||
</div>
|
||||
<p className="text-muted-foreground text-sm leading-5">
|
||||
Connect, confirm the account, and keep it alongside your other
|
||||
configured providers.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{detailChips.map((chip) => (
|
||||
<Badge
|
||||
key={chip}
|
||||
variant="outline"
|
||||
className="rounded-full border-white/70 bg-white/[0.65] px-3 py-1 text-foreground/80 dark:border-white/[0.12] dark:bg-white/[0.08] dark:text-foreground/85"
|
||||
>
|
||||
{chip}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full max-w-sm rounded-3xl border border-white/60 bg-white/[0.78] p-5 shadow-sm backdrop-blur dark:border-white/10 dark:bg-white/[0.08]">
|
||||
<p className="font-medium text-foreground/70 text-sm uppercase tracking-[0.18em]">
|
||||
Current status
|
||||
</p>
|
||||
<div className="mt-3 space-y-3">
|
||||
<div className="rounded-2xl border border-border/70 bg-background/80 p-4 dark:bg-background/30">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<p className="font-medium text-sm">Account</p>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{email ?? 'Not signed in'}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
'rounded-full px-3 py-1 font-medium text-xs',
|
||||
state === 'connected'
|
||||
? 'bg-emerald-500/12 text-emerald-700 dark:text-emerald-300'
|
||||
: state === 'connecting' || state === 'provisioning'
|
||||
? 'bg-orange-500/12 text-orange-700 dark:text-orange-300'
|
||||
: 'bg-muted text-muted-foreground',
|
||||
)}
|
||||
>
|
||||
{copy.badge}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-2xl border border-border/70 bg-background/80 p-4 dark:bg-background/30">
|
||||
<p className="font-medium text-sm">Provider</p>
|
||||
<p className="mt-1 text-muted-foreground text-sm">
|
||||
{provider
|
||||
? `${provider.name} with ${provider.modelId}`
|
||||
: 'A local provider entry will be created automatically after authentication.'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-5 flex flex-col gap-3">
|
||||
{(state === 'disconnected' || state === 'connecting') && (
|
||||
<Button
|
||||
size="lg"
|
||||
onClick={onConnect}
|
||||
className="w-full bg-[var(--accent-orange)] text-white hover:bg-[var(--accent-orange)]/90"
|
||||
>
|
||||
{state === 'connecting' ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
Reopen login tab
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Connect ChatGPT Plus/Pro
|
||||
<ArrowRight className="h-4 w-4" />
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{state === 'provisioning' && (
|
||||
<Button size="lg" disabled className="w-full">
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
Finishing setup
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{state === 'connected' && !isDefault && (
|
||||
<Button
|
||||
size="lg"
|
||||
onClick={onMakeDefault}
|
||||
className="w-full bg-[var(--accent-orange)] text-white hover:bg-[var(--accent-orange)]/90"
|
||||
>
|
||||
Make default provider
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{state === 'connected' && isDefault && (
|
||||
<Button size="lg" variant="secondary" disabled className="w-full">
|
||||
Default provider selected
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col gap-2 sm:flex-row">
|
||||
{setupGuideUrl && (
|
||||
<Button variant="outline" className="flex-1" asChild>
|
||||
<a
|
||||
href={setupGuideUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Setup guide
|
||||
<ExternalLink className="h-4 w-4" />
|
||||
</a>
|
||||
</Button>
|
||||
)}
|
||||
{state === 'connected' && (
|
||||
<Button
|
||||
variant="outline"
|
||||
className="flex-1"
|
||||
onClick={onDisconnect}
|
||||
>
|
||||
<Unplug className="h-4 w-4" />
|
||||
Disconnect
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Check, Loader2, Trash2 } from 'lucide-react'
|
||||
import { Check, Loader2, PencilLine, Trash2 } from 'lucide-react'
|
||||
import type { FC } from 'react'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { useKimiLaunch } from '@/lib/feature-flags/useKimiLaunch'
|
||||
import { BrowserOSIcon, ProviderIcon } from '@/lib/llm-providers/providerIcons'
|
||||
import { getProviderTemplate } from '@/lib/llm-providers/providerTemplates'
|
||||
import type { LlmProviderConfig } from '@/lib/llm-providers/types'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
@@ -31,15 +32,26 @@ export const ProviderCard: FC<ProviderCardProps> = ({
|
||||
}) => {
|
||||
const inputId = `provider-${provider.id}`
|
||||
const kimiLaunch = useKimiLaunch()
|
||||
const providerLabel = isBuiltIn
|
||||
? 'BrowserOS hosted'
|
||||
: (getProviderTemplate(provider.type)?.name ?? provider.type)
|
||||
const providerHost = getProviderHost(provider.baseUrl)
|
||||
const detailBadges = [providerLabel, provider.modelId, providerHost].filter(
|
||||
Boolean,
|
||||
)
|
||||
const description = getProviderDescription({
|
||||
provider,
|
||||
isBuiltIn,
|
||||
kimiLaunch,
|
||||
})
|
||||
|
||||
return (
|
||||
<label
|
||||
htmlFor={inputId}
|
||||
<div
|
||||
className={cn(
|
||||
'group flex w-full cursor-pointer items-center gap-4 rounded-xl border p-4 text-left transition-all',
|
||||
'group rounded-2xl border bg-card p-4 transition-all',
|
||||
isSelected
|
||||
? 'border-[var(--accent-orange)] bg-[var(--accent-orange)]/5 shadow-md'
|
||||
: 'border-border bg-card hover:border-[var(--accent-orange)]/50 hover:shadow-sm',
|
||||
: 'border-border hover:border-[var(--accent-orange)]/50 hover:shadow-sm',
|
||||
)}
|
||||
>
|
||||
<input
|
||||
@@ -50,90 +62,127 @@ export const ProviderCard: FC<ProviderCardProps> = ({
|
||||
checked={isSelected}
|
||||
onChange={() => onSelect()}
|
||||
/>
|
||||
<div
|
||||
className={cn(
|
||||
'flex h-5 w-5 shrink-0 items-center justify-center rounded-full border-2 transition-all',
|
||||
isSelected
|
||||
? 'border-[var(--accent-orange)] bg-[var(--accent-orange)]'
|
||||
: 'border-border',
|
||||
)}
|
||||
>
|
||||
{isSelected && <Check className="h-3 w-3 text-white" />}
|
||||
</div>
|
||||
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-[var(--accent-orange)]/10 text-[var(--accent-orange)]">
|
||||
{isBuiltIn ? (
|
||||
<BrowserOSIcon size={24} />
|
||||
) : (
|
||||
<ProviderIcon type={provider.type} size={24} />
|
||||
)}
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="mb-1 flex items-center gap-2">
|
||||
<span className="font-semibold">{provider.name}</span>
|
||||
{isSelected && (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="rounded bg-[var(--accent-orange)]/10 text-[var(--accent-orange)]"
|
||||
>
|
||||
DEFAULT
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
{isBuiltIn && provider.type === 'browseros' && kimiLaunch && (
|
||||
<span className="mb-1 inline-block rounded-full border border-orange-300/60 bg-orange-100/70 px-3 py-0.5 font-semibold text-orange-700 text-xs dark:border-orange-400/40 dark:bg-orange-500/15 dark:text-orange-300">
|
||||
In partnership with Moonshot AI
|
||||
</span>
|
||||
)}
|
||||
<p className="truncate text-muted-foreground text-sm">
|
||||
{isBuiltIn ? (
|
||||
kimiLaunch ? (
|
||||
'Extended usage limits for the next 2 weeks!'
|
||||
<div className="flex flex-col gap-4 lg:flex-row lg:items-center">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onSelect}
|
||||
className="flex min-w-0 flex-1 items-start gap-4 text-left"
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'mt-1 flex h-5 w-5 shrink-0 items-center justify-center rounded-full border-2 transition-all',
|
||||
isSelected
|
||||
? 'border-[var(--accent-orange)] bg-[var(--accent-orange)]'
|
||||
: 'border-border bg-background',
|
||||
)}
|
||||
>
|
||||
{isSelected && <Check className="h-3 w-3 text-white" />}
|
||||
</div>
|
||||
<div className="flex h-11 w-11 shrink-0 items-center justify-center rounded-xl bg-[var(--accent-orange)]/10 text-[var(--accent-orange)]">
|
||||
{isBuiltIn ? (
|
||||
<BrowserOSIcon size={24} />
|
||||
) : (
|
||||
<>
|
||||
BrowserOS-hosted model with strict rate limits.{' '}
|
||||
<a
|
||||
href="https://docs.browseros.com/features/bring-your-own-llm"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline hover:text-foreground"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
<ProviderIcon type={provider.type} size={24} />
|
||||
)}
|
||||
</div>
|
||||
<div className="min-w-0 flex-1 space-y-3">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="font-semibold text-[15px]">{provider.name}</span>
|
||||
{isSelected && (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="rounded-full bg-[var(--accent-orange)]/10 px-3 py-1 text-[var(--accent-orange)]"
|
||||
>
|
||||
Bring your own key
|
||||
</a>{' '}
|
||||
for better performance.
|
||||
</>
|
||||
)
|
||||
) : (
|
||||
provider.baseUrl
|
||||
? `${provider.modelId} • ${provider.baseUrl}`
|
||||
: provider.modelId
|
||||
)}
|
||||
</p>
|
||||
Default
|
||||
</Badge>
|
||||
)}
|
||||
{provider.type === 'chatgpt-pro' && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="rounded-full border-emerald-500/30 bg-emerald-500/[0.08] px-3 py-1 text-emerald-700 dark:text-emerald-300"
|
||||
>
|
||||
Managed OAuth
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-muted-foreground text-sm leading-5">
|
||||
{description}
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{detailBadges.map((item) => (
|
||||
<Badge
|
||||
key={item}
|
||||
variant="outline"
|
||||
className="rounded-full px-3 py-1 text-muted-foreground"
|
||||
>
|
||||
{item}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
{!isBuiltIn && (
|
||||
<div className="flex shrink-0 flex-wrap gap-2 lg:justify-end">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={isTesting}
|
||||
onClick={() => onTest?.()}
|
||||
>
|
||||
{isTesting && <Loader2 className="h-4 w-4 animate-spin" />}
|
||||
{isTesting ? 'Testing...' : 'Test'}
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={() => onEdit?.()}>
|
||||
<PencilLine className="h-4 w-4" />
|
||||
Edit
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
className="text-muted-foreground hover:bg-destructive/10 hover:text-destructive"
|
||||
onClick={() => onDelete?.()}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{!isBuiltIn && (
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={isTesting}
|
||||
onClick={() => onTest?.()}
|
||||
>
|
||||
{isTesting && <Loader2 className="mr-1.5 h-4 w-4 animate-spin" />}
|
||||
{isTesting ? 'Testing...' : 'Test'}
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={() => onEdit?.()}>
|
||||
Edit
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
className="text-muted-foreground hover:bg-destructive/10 hover:text-destructive"
|
||||
onClick={() => onDelete?.()}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</label>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function getProviderHost(baseUrl?: string): string | null {
|
||||
if (!baseUrl) return null
|
||||
try {
|
||||
return new URL(baseUrl).host
|
||||
} catch {
|
||||
return baseUrl.replace(/^https?:\/\//, '')
|
||||
}
|
||||
}
|
||||
|
||||
function getProviderDescription({
|
||||
provider,
|
||||
isBuiltIn,
|
||||
kimiLaunch,
|
||||
}: {
|
||||
provider: LlmProviderConfig
|
||||
isBuiltIn: boolean
|
||||
kimiLaunch: boolean
|
||||
}) {
|
||||
if (isBuiltIn) {
|
||||
if (kimiLaunch) {
|
||||
return 'Extended usage limits are enabled through the Moonshot AI partnership.'
|
||||
}
|
||||
return 'BrowserOS-hosted model with stricter shared limits than bring-your-own-provider setups.'
|
||||
}
|
||||
|
||||
if (provider.type === 'chatgpt-pro') {
|
||||
return 'Connected through your ChatGPT account so you can use the managed BrowserOS flow without local API keys.'
|
||||
}
|
||||
|
||||
if (provider.baseUrl) {
|
||||
return `Configured against ${provider.baseUrl}.`
|
||||
}
|
||||
|
||||
return 'Custom provider configuration.'
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ export const ProviderTemplatesSection: FC<ProviderTemplatesSectionProps> = ({
|
||||
const kimiLaunch = useKimiLaunch()
|
||||
|
||||
const filteredTemplates = providerTemplates.filter((template) => {
|
||||
if (template.id === 'chatgpt-pro') return false
|
||||
if (template.id === 'moonshot') return kimiLaunch
|
||||
if (template.id === 'openai-compatible') {
|
||||
return supports(Feature.OPENAI_COMPATIBLE_SUPPORT)
|
||||
@@ -38,9 +39,9 @@ export const ProviderTemplatesSection: FC<ProviderTemplatesSectionProps> = ({
|
||||
<div className="rounded-xl border border-border bg-card p-6 shadow-sm transition-all hover:shadow-md">
|
||||
<CollapsibleTrigger className="mb-4 flex w-full items-center justify-between text-left">
|
||||
<div>
|
||||
<h3 className="font-semibold text-lg">Quick provider templates</h3>
|
||||
<h3 className="font-semibold text-lg">More provider templates</h3>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{filteredTemplates.length} templates available
|
||||
Manual and API-key based providers
|
||||
</p>
|
||||
</div>
|
||||
<ChevronDown
|
||||
|
||||
Reference in New Issue
Block a user