feat(ui): reposition providers around quality and local lanes

This commit is contained in:
Tam Nhu Tran
2026-04-10 05:50:26 -04:00
parent 63b67f1c1c
commit ad70f93b1b
6 changed files with 352 additions and 121 deletions
@@ -3,12 +3,16 @@ import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { useOpenRouterReady } from '@/hooks/use-openrouter-models';
import { useLocalRuntimeReadiness } from '@/hooks/use-local-runtime-readiness';
import { cn } from '@/lib/utils';
import {
ArrowRight,
BrainCircuit,
CloudCog,
ExternalLink,
HardDriveDownload,
KeyRound,
Laptop,
SlidersHorizontal,
Sparkles,
Zap,
@@ -22,6 +26,8 @@ interface OpenRouterQuickStartProps {
onAlibabaCodingPlanClick: () => void;
onCliproxyClick: () => void;
onCustomClick: () => void;
onOllamaClick: () => void;
onLlamacppClick: () => void;
}
interface QuickStartCardProps {
@@ -38,6 +44,24 @@ interface QuickStartCardProps {
footer?: ReactNode;
}
type LocalRuntimeStatus = 'ready' | 'missing-model' | 'offline';
type LocalRuntimeView = {
id: 'ollama' | 'llamacpp';
endpoint: string;
status: LocalRuntimeStatus;
commandHint: string;
detectedModelCount: number;
};
type LocalRuntimeCardState = {
badge: string;
badgeClassName: string;
actionLabel: string;
description: string;
footer: string;
};
function QuickStartCard({
badge,
badgeClassName,
@@ -84,6 +108,52 @@ function QuickStartCard({
);
}
function getLocalRuntimeCardState(
runtime: LocalRuntimeView | undefined,
label: string
): LocalRuntimeCardState {
if (!runtime) {
return {
badge: 'Checking',
badgeClassName: 'bg-muted text-muted-foreground',
actionLabel: `Set up ${label}`,
description: 'Checking the local runtime status before showing setup guidance.',
footer: 'Checking the local runtime...',
};
}
if (runtime.status === 'ready') {
return {
badge: 'Ready',
badgeClassName:
'bg-emerald-500/10 text-emerald-700 dark:bg-emerald-500/20 dark:text-emerald-200',
actionLabel: `Use ${label}`,
description: 'Best for private prompts, offline workflows, and cheaper batch transforms.',
footer: `Endpoint ready at ${runtime.endpoint}`,
};
}
if (runtime.status === 'missing-model') {
return {
badge: 'Needs model',
badgeClassName: 'bg-amber-500/10 text-amber-700 dark:bg-amber-500/20 dark:text-amber-200',
actionLabel: `Finish ${label} setup`,
description:
'The runtime is up, but the recommended local model still needs to be pulled or loaded.',
footer: `Run \`${runtime.commandHint}\` to finish local setup.`,
};
}
return {
badge: 'Offline',
badgeClassName: 'bg-muted text-muted-foreground',
actionLabel: `Set up ${label}`,
description:
'Use this lane for privacy-sensitive work, cheap local transforms, and offline sessions.',
footer: `Run \`${runtime.commandHint}\` to bring the local endpoint online.`,
};
}
export function OpenRouterQuickStart({
hasProfiles,
profileCount,
@@ -91,31 +161,37 @@ export function OpenRouterQuickStart({
onAlibabaCodingPlanClick,
onCliproxyClick,
onCustomClick,
onOllamaClick,
onLlamacppClick,
}: OpenRouterQuickStartProps) {
const { t } = useTranslation();
const { modelCount, isLoading } = useOpenRouterReady();
const { data: localRuntimeData } = useLocalRuntimeReadiness();
const modelCountLabel = isLoading ? '300+' : `${modelCount}+`;
const profileSummaryLabel = hasProfiles
? t('openrouterQuickStart.profileCount', { count: profileCount })
: t('openrouterQuickStart.recommended');
? `${profileCount} ${profileCount === 1 ? 'profile' : 'profiles'}`
: 'Premium quality + local control';
const summaryTitle = hasProfiles
? t('openrouterQuickStart.selectProfileTitle')
: t('apiProfiles.noProfilesYet');
? 'Choose a profile or add another lane'
: 'Choose your first profile lane';
const summaryDescription = hasProfiles
? t('openrouterQuickStart.summaryDescriptionWithProfiles', { count: profileCount })
: t('openrouterQuickStart.summaryDescriptionNoProfiles');
? `You already have ${profileCount} profile${profileCount === 1 ? '' : 's'} in this workspace. Keep premium-quality providers for serious coding and add local runtimes when privacy, cost, or offline work matters.`
: 'Pick the lane that matches the work. Premium providers stay the default for reliable coding. Local runtimes are best for privacy, cheaper transforms, and experimentation.';
const ollamaRuntime = localRuntimeData?.runtimes.find((runtime) => runtime.id === 'ollama');
const llamacppRuntime = localRuntimeData?.runtimes.find((runtime) => runtime.id === 'llamacpp');
const ollamaCardState = getLocalRuntimeCardState(ollamaRuntime, 'Ollama');
const llamacppCardState = getLocalRuntimeCardState(llamacppRuntime, 'llama.cpp');
return (
<div className="flex h-full min-h-0 flex-col overflow-auto bg-muted/20 p-4 sm:p-6">
<div className="mx-auto flex w-full max-w-7xl flex-col gap-4">
<div className="mx-auto flex w-full max-w-7xl flex-col gap-5">
<Card className="border-dashed bg-background/90 shadow-sm">
<CardContent className="flex flex-col gap-4 p-5 lg:flex-row lg:items-center lg:justify-between">
<div className="space-y-2">
<div className="flex flex-wrap items-center gap-2">
<Badge variant="secondary">{profileSummaryLabel}</Badge>
<Badge variant="outline">
{t('openrouterQuickStart.openrouterModelsBadge', { modelCountLabel })}
</Badge>
<Badge variant="outline">Best quality by default</Badge>
<Badge variant="outline">Local lane when task fit wins</Badge>
</div>
<div className="space-y-1">
<h2 className="text-xl font-semibold">{summaryTitle}</h2>
@@ -130,50 +206,171 @@ export function OpenRouterQuickStart({
</CardContent>
</Card>
<div className="grid gap-4 lg:grid-cols-2">
<QuickStartCard
badge={t('openrouterQuickStart.recommended')}
title={t('openrouterQuickStart.title')}
description={t('openrouterQuickStart.description', { modelCountLabel })}
visual={
<div className="rounded-lg bg-accent/10 p-2">
<img src="/icons/openrouter.svg" alt="OpenRouter" className="h-5 w-5" />
</div>
}
highlights={[
{
icon: <Zap className="h-3.5 w-3.5 text-accent" />,
label: t('openrouterQuickStart.featureOneApi'),
},
{
icon: <Sparkles className="h-3.5 w-3.5 text-accent" />,
label: t('openrouterQuickStart.featureTierMapping'),
},
]}
actionLabel={t('openrouterQuickStart.createOpenRouterProfile')}
actionClassName="w-full bg-accent text-white hover:bg-accent/90"
onAction={onOpenRouterClick}
footer={
<>
{t('openrouterQuickStart.getApiKeyAt')}{' '}
<a
href="https://openrouter.ai/keys"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1 text-accent hover:underline"
>
openrouter.ai/keys
<ExternalLink className="h-3 w-3" />
</a>
</>
}
/>
<div className="space-y-2">
<div className="flex items-center gap-2">
<BrainCircuit className="h-4 w-4 text-accent" />
<h3 className="text-sm font-semibold uppercase tracking-[0.12em] text-foreground/70">
Best quality lanes
</h3>
</div>
<div className="grid gap-4 lg:grid-cols-2">
<QuickStartCard
badge={t('openrouterQuickStart.recommended')}
title={t('openrouterQuickStart.title')}
description={t('openrouterQuickStart.description', { modelCountLabel })}
visual={
<div className="rounded-lg bg-accent/10 p-2">
<img src="/icons/openrouter.svg" alt="OpenRouter" className="h-5 w-5" />
</div>
}
highlights={[
{
icon: <Zap className="h-3.5 w-3.5 text-accent" />,
label: t('openrouterQuickStart.featureOneApi'),
},
{
icon: <Sparkles className="h-3.5 w-3.5 text-accent" />,
label: 'Best default lane for high-stakes coding quality',
},
]}
actionLabel={t('openrouterQuickStart.createOpenRouterProfile')}
actionClassName="w-full bg-accent text-white hover:bg-accent/90"
onAction={onOpenRouterClick}
footer={
<>
{t('openrouterQuickStart.getApiKeyAt')}{' '}
<a
href="https://openrouter.ai/keys"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1 text-accent hover:underline"
>
openrouter.ai/keys
<ExternalLink className="h-3 w-3" />
</a>
</>
}
/>
<QuickStartCard
badge={t('alibabaCodingPlanQuickStart.recommended')}
badgeClassName="bg-orange-500/10 text-orange-700 dark:bg-orange-500/20 dark:text-orange-200"
title={t('alibabaCodingPlanQuickStart.title')}
description="Strong direct coding profile when you want a dedicated premium lane outside the OpenRouter catalog."
visual={
<div className="rounded-lg bg-orange-500/10 p-2">
<img
src="/assets/providers/alibabacloud-color.svg"
alt="Alibaba Coding Plan"
className="h-5 w-5"
/>
</div>
}
highlights={[
{
icon: <CloudCog className="h-3.5 w-3.5 text-orange-600" />,
label: t('alibabaCodingPlanQuickStart.featureEndpoint'),
},
{
icon: <KeyRound className="h-3.5 w-3.5 text-orange-600" />,
label: 'Good when you want premium quality with a dedicated endpoint',
},
]}
actionLabel={t('alibabaCodingPlanQuickStart.createAlibabaProfile')}
actionClassName="w-full bg-orange-600 text-white hover:bg-orange-600/90"
onAction={onAlibabaCodingPlanClick}
footer={
<>
{t('alibabaCodingPlanQuickStart.readGuideAt')}{' '}
<a
href="https://www.alibabacloud.com/help/en/model-studio/coding-plan"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1 text-orange-700 hover:underline dark:text-orange-400"
>
Alibaba Cloud Model Studio
<ExternalLink className="h-3 w-3" />
</a>
</>
}
/>
</div>
</div>
<div className="space-y-2">
<div className="flex items-center gap-2">
<Laptop className="h-4 w-4 text-emerald-600" />
<h3 className="text-sm font-semibold uppercase tracking-[0.12em] text-foreground/70">
Local runtimes
</h3>
</div>
<div className="grid gap-4 lg:grid-cols-2">
<QuickStartCard
badge={ollamaCardState.badge}
badgeClassName={ollamaCardState.badgeClassName}
title="Ollama + Gemma 4"
description={ollamaCardState.description}
visual={
<div className="rounded-lg bg-emerald-500/10 p-2">
<img src="/icons/ollama.svg" alt="Ollama" className="h-5 w-5" />
</div>
}
highlights={[
{
icon: <HardDriveDownload className="h-3.5 w-3.5 text-emerald-600" />,
label: 'Best for private prompts, local cleanup, and cheap batch transforms',
},
{
icon: <Sparkles className="h-3.5 w-3.5 text-emerald-600" />,
label:
ollamaRuntime?.detectedModelCount && ollamaRuntime.detectedModelCount > 0
? `${ollamaRuntime.detectedModelCount} local model${ollamaRuntime.detectedModelCount === 1 ? '' : 's'} detected`
: 'No local models detected yet',
},
]}
actionLabel={ollamaCardState.actionLabel}
actionClassName="w-full bg-emerald-600 text-white hover:bg-emerald-600/90"
onAction={onOllamaClick}
footer={<span>{ollamaCardState.footer}</span>}
/>
<QuickStartCard
badge={llamacppCardState.badge}
badgeClassName={llamacppCardState.badgeClassName}
title="llama.cpp"
description={llamacppCardState.description}
visual={
<div className="rounded-lg bg-sky-500/10 p-2">
<img src="/assets/providers/llama-cpp.svg" alt="llama.cpp" className="h-5 w-5" />
</div>
}
highlights={[
{
icon: <SlidersHorizontal className="h-3.5 w-3.5 text-sky-600" />,
label: 'Best for custom GGUF setups and advanced self-hosted local workflows',
},
{
icon: <Laptop className="h-3.5 w-3.5 text-sky-600" />,
label:
llamacppRuntime?.detectedModelCount && llamacppRuntime.detectedModelCount > 0
? `${llamacppRuntime.detectedModelCount} local model${llamacppRuntime.detectedModelCount === 1 ? '' : 's'} detected`
: 'Waiting for a local server and model list',
},
]}
actionLabel={llamacppCardState.actionLabel}
actionClassName="w-full bg-sky-600 text-white hover:bg-sky-600/90"
onAction={onLlamacppClick}
footer={<span>{llamacppCardState.footer}</span>}
/>
</div>
</div>
<div className="grid gap-4 lg:grid-cols-2">
<QuickStartCard
badge={t('openrouterQuickStart.runtimeProviderBadge')}
badgeClassName="bg-emerald-500/10 text-emerald-700 dark:bg-emerald-500/20 dark:text-emerald-200"
title={t('openrouterQuickStart.runtimeProviderTitle')}
description={t('openrouterQuickStart.runtimeProviderDescription')}
description="Use this when you need CLIProxy-managed connectors, provider secrets, and advanced routing controls."
visual={
<div className="rounded-lg bg-emerald-500/10 p-2">
<SlidersHorizontal className="h-5 w-5 text-emerald-700 dark:text-emerald-300" />
@@ -194,50 +391,6 @@ export function OpenRouterQuickStart({
onAction={onCliproxyClick}
footer={<span>{t('openrouterQuickStart.runtimeProviderFooter')}</span>}
/>
<QuickStartCard
badge={t('alibabaCodingPlanQuickStart.recommended')}
badgeClassName="bg-orange-500/10 text-orange-700 dark:bg-orange-500/20 dark:text-orange-200"
className="lg:col-span-2"
title={t('alibabaCodingPlanQuickStart.title')}
description={t('alibabaCodingPlanQuickStart.description')}
visual={
<div className="rounded-lg bg-orange-500/10 p-2">
<img
src="/assets/providers/alibabacloud-color.svg"
alt="Alibaba Coding Plan"
className="h-5 w-5"
/>
</div>
}
highlights={[
{
icon: <CloudCog className="h-3.5 w-3.5 text-orange-600" />,
label: t('alibabaCodingPlanQuickStart.featureEndpoint'),
},
{
icon: <KeyRound className="h-3.5 w-3.5 text-orange-600" />,
label: t('alibabaCodingPlanQuickStart.featureKeyFormat'),
},
]}
actionLabel={t('alibabaCodingPlanQuickStart.createAlibabaProfile')}
actionClassName="w-full bg-orange-600 text-white hover:bg-orange-600/90"
onAction={onAlibabaCodingPlanClick}
footer={
<>
{t('alibabaCodingPlanQuickStart.readGuideAt')}{' '}
<a
href="https://www.alibabacloud.com/help/en/model-studio/coding-plan"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1 text-orange-700 hover:underline dark:text-orange-400"
>
Alibaba Cloud Model Studio
<ExternalLink className="h-3 w-3" />
</a>
</>
}
/>
</div>
</div>
</div>
@@ -80,13 +80,14 @@ interface ProfileCreateDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
onSuccess: (name: string) => void;
initialMode?: 'normal' | 'openrouter' | 'alibaba-coding-plan';
initialMode?: ProviderPreset['id'] | 'normal';
}
// Common URL mistakes to warn about
const PROBLEMATIC_PATHS = ['/chat/completions', '/v1/messages', '/messages', '/completions'];
const CUSTOM_PRESET_ID = 'custom';
const DEFAULT_PRESET_ID: ProviderPreset['id'] = 'openrouter';
const LOCAL_PRESET_IDS = new Set<ProviderPreset['id']>(['ollama', 'llamacpp']);
const EMPTY_FORM_VALUES: FormData = {
name: '',
@@ -100,6 +101,12 @@ const EMPTY_FORM_VALUES: FormData = {
};
const RECOMMENDED_PRESETS = getPresetsByCategory('recommended');
const FEATURED_PREMIUM_PRESETS = RECOMMENDED_PRESETS.filter(
(preset) => !LOCAL_PRESET_IDS.has(preset.id)
);
const LOCAL_RUNTIME_PRESETS = RECOMMENDED_PRESETS.filter((preset) =>
LOCAL_PRESET_IDS.has(preset.id)
);
const QUICK_TEMPLATE_PRESETS = PROVIDER_PRESETS.filter(
(preset) => preset.category !== 'recommended'
);
@@ -314,7 +321,7 @@ export function ProfileCreateDialog({
</Label>
<div className="-mx-1 overflow-x-auto pb-1">
<div className="flex min-w-max items-stretch gap-2 px-1">
{RECOMMENDED_PRESETS.map((preset) => (
{FEATURED_PREMIUM_PRESETS.map((preset) => (
<CompactPresetCard
key={preset.id}
preset={preset}
@@ -350,6 +357,23 @@ export function ProfileCreateDialog({
</div>
</div>
</div>
<div className="space-y-2">
<Label className="text-xs font-medium uppercase tracking-[0.12em] text-foreground/70">
Local Runtimes
</Label>
<div className="flex flex-wrap gap-2">
{LOCAL_RUNTIME_PRESETS.map((preset) => (
<CompactPresetCard
key={preset.id}
preset={preset}
isSelected={selectedPreset === preset.id}
onClick={() => handlePresetSelect(preset.id)}
density="compact"
/>
))}
</div>
</div>
</div>
</div>
+2 -2
View File
@@ -19,7 +19,7 @@ const resources = {
home: 'Home',
analytics: 'Analytics',
identityAccess: 'Identity & Access',
apiProfiles: 'API Profiles',
apiProfiles: 'Profiles',
cliproxyPlus: 'CLIProxy Plus',
cliproxyOverview: 'Overview',
controlPanel: 'Control Panel',
@@ -1017,7 +1017,7 @@ const resources = {
tokensSubtitle: '{{value}} tokens',
},
apiProfiles: {
title: 'API Profiles',
title: 'Profiles',
new: 'New',
searchPlaceholder: 'Search profiles...',
loadingProfiles: 'Loading profiles...',
+19 -6
View File
@@ -34,6 +34,7 @@ import {
import { useOpenRouterModels } from '@/hooks/use-openrouter-models';
import { ConfirmDialog } from '@/components/shared/confirm-dialog';
import type { ApiProfileExportBundle, Profile } from '@/lib/api-client';
import type { ProviderPreset } from '@/lib/provider-presets';
import { cn } from '@/lib/utils';
import { CopyButton } from '@/components/ui/copy-button';
import { useTranslation } from 'react-i18next';
@@ -53,9 +54,7 @@ export function ApiPage() {
const [selectedProfile, setSelectedProfile] = useState<string | null>(null);
const [searchQuery, setSearchQuery] = useState('');
const [isCreateDialogOpen, setCreateDialogOpen] = useState(false);
const [createMode, setCreateMode] = useState<'normal' | 'openrouter' | 'alibaba-coding-plan'>(
'normal'
);
const [createMode, setCreateMode] = useState<ProviderPreset['id'] | 'normal'>('normal');
const [deleteConfirm, setDeleteConfirm] = useState<string | null>(null);
const [editorHasChanges, setEditorHasChanges] = useState(false);
const [pendingSwitch, setPendingSwitch] = useState<string | null>(null);
@@ -211,12 +210,14 @@ export function ApiPage() {
<div className="flex-1 flex min-h-0 overflow-hidden">
<div className="w-80 border-r flex flex-col bg-muted/30">
<div className="p-4 border-b bg-background">
<div className="flex items-center justify-between mb-3">
<div className="mb-3 flex items-start justify-between gap-3">
<div className="flex items-center gap-2">
<Server className="w-5 h-5 text-primary" />
<h1 className="font-semibold">{t('apiProfiles.title')}</h1>
<div className="min-w-0">
<h1 className="font-semibold">Profiles</h1>
</div>
</div>
<div className="flex items-center gap-1">
<div className="flex shrink-0 items-center gap-1">
<Button
size="sm"
variant="outline"
@@ -251,6 +252,10 @@ export function ApiPage() {
</div>
</div>
<p className="mb-3 text-xs leading-4 text-muted-foreground">
Premium APIs, local runtimes, custom endpoints
</p>
<div className="relative">
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
@@ -400,6 +405,14 @@ export function ApiPage() {
setCreateMode('alibaba-coding-plan');
setCreateDialogOpen(true);
}}
onOllamaClick={() => {
setCreateMode('ollama');
setCreateDialogOpen(true);
}}
onLlamacppClick={() => {
setCreateMode('llamacpp');
setCreateDialogOpen(true);
}}
onCustomClick={() => {
setCreateMode('normal');
setCreateDialogOpen(true);
@@ -1,7 +1,7 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import i18n from '@/lib/i18n';
import { OpenRouterQuickStart } from '@/components/profiles/openrouter-quick-start';
import { render, screen } from '@tests/setup/test-utils';
import { render, screen, userEvent } from '@tests/setup/test-utils';
vi.mock('@/hooks/use-openrouter-models', () => ({
useOpenRouterReady: () => ({
@@ -10,12 +10,40 @@ vi.mock('@/hooks/use-openrouter-models', () => ({
}),
}));
vi.mock('@/hooks/use-local-runtime-readiness', () => ({
useLocalRuntimeReadiness: () => ({
data: {
runtimes: [
{
id: 'ollama',
status: 'offline',
endpoint: 'http://127.0.0.1:11434',
commandHint: 'ollama serve',
recommendedModel: 'gemma4:e4b',
recommendedModelInstalled: false,
},
{
id: 'llamacpp',
status: 'ready',
endpoint: 'http://127.0.0.1:8080',
commandHint: './server --host 0.0.0.0 --port 8080 -m model.gguf',
recommendedModel: null,
recommendedModelInstalled: true,
},
],
},
isLoading: false,
}),
}));
describe('OpenRouterQuickStart', () => {
const props = {
onOpenRouterClick: vi.fn(),
onAlibabaCodingPlanClick: vi.fn(),
onCliproxyClick: vi.fn(),
onCustomClick: vi.fn(),
onOllamaClick: vi.fn(),
onLlamacppClick: vi.fn(),
};
beforeEach(async () => {
@@ -25,16 +53,28 @@ describe('OpenRouterQuickStart', () => {
it('prompts the user to select an existing API profile instead of showing the empty-state copy', () => {
render(<OpenRouterQuickStart hasProfiles profileCount={1} {...props} />);
expect(screen.getByRole('heading', { name: 'Select an API profile' })).toBeInTheDocument();
expect(
screen.getByRole('heading', { name: 'Choose a profile or add another lane' })
).toBeInTheDocument();
expect(screen.getByText('1 profile')).toBeInTheDocument();
expect(screen.getByText(/You already have 1 profile in this workspace\./)).toBeInTheDocument();
expect(screen.queryByText('No API profiles yet')).not.toBeInTheDocument();
});
it('keeps the original empty-state title when no API profiles exist', () => {
it('shows the new local lane with readiness-aware calls to action', async () => {
render(<OpenRouterQuickStart hasProfiles={false} profileCount={0} {...props} />);
expect(screen.getByRole('heading', { name: 'No API profiles yet' })).toBeInTheDocument();
expect(screen.getAllByText('Recommended')).not.toHaveLength(0);
expect(
screen.getByRole('heading', { name: 'Choose your first profile lane' })
).toBeInTheDocument();
expect(screen.getByText('Ollama + Gemma 4')).toBeInTheDocument();
expect(screen.getByText('llama.cpp')).toBeInTheDocument();
expect(
screen.getByText('Run `ollama serve` to bring the local endpoint online.')
).toBeInTheDocument();
expect(screen.getByText('Endpoint ready at http://127.0.0.1:8080')).toBeInTheDocument();
await userEvent.click(screen.getByRole('button', { name: 'Set up Ollama' }));
expect(props.onOllamaClick).toHaveBeenCalledTimes(1);
});
});
@@ -33,7 +33,7 @@ describe('ProfileCreateDialog', () => {
mutateAsync.mockReset();
});
it('keeps advanced presets collapsed until explicitly opened and deselects custom after choosing a template', async () => {
it('keeps More Presets visible by default and deselects custom after choosing a template', async () => {
render(
<ProfileCreateDialog
open
@@ -44,22 +44,14 @@ describe('ProfileCreateDialog', () => {
);
expect(screen.getByText('Featured Providers')).toBeInTheDocument();
expect(screen.getByText('More Presets')).toBeInTheDocument();
expect(screen.getByText('Local Runtimes')).toBeInTheDocument();
expect(screen.getByText('Alibaba Coding Plan')).toBeVisible();
const morePresetsToggle = screen.getByRole('button', { name: /More Presets/i });
expect(morePresetsToggle).toHaveAttribute('aria-expanded', 'false');
expect(document.body.querySelectorAll('.overflow-x-auto')).toHaveLength(1);
expect(document.body.querySelectorAll('.overflow-x-auto')).toHaveLength(2);
const customButton = screen.getByRole('button', { name: /Custom Endpoint/i });
await userEvent.click(customButton);
expect(morePresetsToggle).toHaveAttribute('aria-expanded', 'false');
expect(document.body.querySelectorAll('.overflow-x-auto')).toHaveLength(1);
await userEvent.click(morePresetsToggle);
expect(morePresetsToggle).toHaveAttribute('aria-expanded', 'true');
expect(document.body.querySelectorAll('.overflow-x-auto')).toHaveLength(2);
const glmButton = screen.getByText('GLM').closest('button');
expect(glmButton).not.toBeNull();
if (!glmButton) {
@@ -67,10 +59,19 @@ describe('ProfileCreateDialog', () => {
}
await userEvent.click(glmButton);
expect(morePresetsToggle).toHaveAttribute('aria-expanded', 'true');
await waitFor(() => {
expect(customButton).not.toHaveClass('border-primary');
});
expect(glmButton).toHaveClass('border-primary');
});
it('supports opening directly into the Ollama preset from the providers landing page', async () => {
render(
<ProfileCreateDialog open onOpenChange={vi.fn()} onSuccess={vi.fn()} initialMode="ollama" />
);
expect(screen.getByDisplayValue('ollama')).toBeInTheDocument();
expect(screen.getByDisplayValue('http://localhost:11434')).toBeInTheDocument();
expect(screen.getByText('Local Runtimes')).toBeInTheDocument();
});
});