fix(ui): show OpenRouterQuickStart by default on API page

- remove auto-select of first profile behavior
- show QuickStart panel when no profile selected
- users now click profile to see details
- ensures OpenRouter promo visible for existing users
This commit is contained in:
kaitranntt
2025-12-20 19:49:12 -05:00
parent adcc3235f0
commit 05380e21b4
+140 -187
View File
@@ -7,22 +7,21 @@ import { useState, useMemo } from 'react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Badge } from '@/components/ui/badge';
import { Separator } from '@/components/ui/separator';
import {
Plus,
Search,
Settings2,
Trash2,
CheckCircle2,
AlertCircle,
Server,
ExternalLink,
FileJson,
RefreshCw,
} from 'lucide-react';
import { ProfileEditor } from '@/components/profile-editor';
import { ProfileCreateDialog } from '@/components/profiles/profile-create-dialog';
import { OpenRouterBanner } from '@/components/profiles/openrouter-banner';
import { OpenRouterQuickStart } from '@/components/profiles/openrouter-quick-start';
import { OpenRouterPromoCard } from '@/components/profiles/openrouter-promo-card';
import { useProfiles, useDeleteProfile } from '@/hooks/use-profiles';
import { useOpenRouterModels } from '@/hooks/use-openrouter-models';
import { ConfirmDialog } from '@/components/shared/confirm-dialog';
@@ -36,6 +35,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'>('normal');
const [deleteConfirm, setDeleteConfirm] = useState<string | null>(null);
// Prefetch OpenRouter models when page loads (lazy - won't block render)
@@ -50,13 +50,11 @@ export function ApiPage() {
[profiles, searchQuery]
);
// Compute effective selected profile (auto-select first if none selected)
const effectiveSelectedProfile = useMemo(() => {
if (selectedProfile && profiles.some((p) => p.name === selectedProfile)) {
return selectedProfile;
}
return profiles.length > 0 ? profiles[0].name : null;
}, [selectedProfile, profiles]);
// selectedProfile is null by default - user must click to select
// This allows OpenRouterQuickStart to show as the default right panel
const selectedProfileData = selectedProfile
? profiles.find((p) => p.name === selectedProfile)
: null;
// Handle profile deletion
const handleDelete = (name: string) => {
@@ -76,137 +74,154 @@ export function ApiPage() {
setSelectedProfile(name);
};
const selectedProfileData = profiles.find((p) => p.name === effectiveSelectedProfile);
return (
<div className="h-[calc(100vh-100px)] flex">
{/* Left Panel - Profiles List */}
<div className="w-80 border-r flex flex-col bg-muted/30">
{/* Header */}
<div className="p-4 border-b bg-background">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<Server className="w-5 h-5 text-primary" />
<h1 className="font-semibold">API Profiles</h1>
</div>
<Button
size="sm"
onClick={() => {
setCreateDialogOpen(true);
}}
>
<Plus className="w-4 h-4 mr-1" />
New
</Button>
</div>
<div className="h-[calc(100vh-100px)] flex flex-col">
{/* OpenRouter Announcement Banner */}
<OpenRouterBanner onCreateClick={() => setCreateDialogOpen(true)} />
{/* Search */}
<div className="relative">
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search profiles..."
className="pl-8 h-9"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
</div>
{/* Profile List */}
<ScrollArea className="flex-1">
{isLoading ? (
<div className="p-4 text-sm text-muted-foreground">Loading profiles...</div>
) : isError ? (
<div className="p-4 text-center">
<div className="space-y-3 py-8">
<AlertCircle className="w-12 h-12 mx-auto text-destructive/50" />
<div>
<p className="text-sm font-medium">Failed to load profiles</p>
<p className="text-xs text-muted-foreground mt-1">
Unable to fetch API profiles. Please try again.
</p>
</div>
<Button size="sm" variant="outline" onClick={() => refetch()}>
<RefreshCw className="w-4 h-4 mr-1" />
Retry
</Button>
{/* Main Content */}
<div className="flex-1 flex min-h-0">
{/* Left Panel - Profiles List */}
<div className="w-80 border-r flex flex-col bg-muted/30">
{/* Header */}
<div className="p-4 border-b bg-background">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<Server className="w-5 h-5 text-primary" />
<h1 className="font-semibold">API Profiles</h1>
</div>
<Button
size="sm"
onClick={() => {
setCreateDialogOpen(true);
}}
>
<Plus className="w-4 h-4 mr-1" />
New
</Button>
</div>
) : filteredProfiles.length === 0 ? (
<div className="p-4 text-center">
{profiles.length === 0 ? (
{/* Search */}
<div className="relative">
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search profiles..."
className="pl-8 h-9"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
</div>
{/* Profile List */}
<ScrollArea className="flex-1">
{isLoading ? (
<div className="p-4 text-sm text-muted-foreground">Loading profiles...</div>
) : isError ? (
<div className="p-4 text-center">
<div className="space-y-3 py-8">
<FileJson className="w-12 h-12 mx-auto text-muted-foreground/50" />
<AlertCircle className="w-12 h-12 mx-auto text-destructive/50" />
<div>
<p className="text-sm font-medium">No API profiles yet</p>
<p className="text-sm font-medium">Failed to load profiles</p>
<p className="text-xs text-muted-foreground mt-1">
Create your first profile to connect to custom API endpoints
Unable to fetch API profiles. Please try again.
</p>
</div>
<Button
size="sm"
variant="outline"
onClick={() => {
setCreateDialogOpen(true);
}}
>
<Plus className="w-4 h-4 mr-1" />
Create Profile
<Button size="sm" variant="outline" onClick={() => refetch()}>
<RefreshCw className="w-4 h-4 mr-1" />
Retry
</Button>
</div>
) : (
<p className="text-sm text-muted-foreground py-4">
No profiles match "{searchQuery}"
</p>
)}
</div>
) : (
<div className="p-2 space-y-1">
{filteredProfiles.map((profile) => (
<ProfileListItem
key={profile.name}
profile={profile}
isSelected={effectiveSelectedProfile === profile.name}
onSelect={() => {
setSelectedProfile(profile.name);
}}
onDelete={() => setDeleteConfirm(profile.name)}
/>
))}
</div>
) : filteredProfiles.length === 0 ? (
<div className="p-4 text-center">
{profiles.length === 0 ? (
<div className="space-y-3 py-8">
<FileJson className="w-12 h-12 mx-auto text-muted-foreground/50" />
<div>
<p className="text-sm font-medium">No API profiles yet</p>
<p className="text-xs text-muted-foreground mt-1">
Create your first profile to connect to custom API endpoints
</p>
</div>
<Button
size="sm"
variant="outline"
onClick={() => {
setCreateDialogOpen(true);
}}
>
<Plus className="w-4 h-4 mr-1" />
Create Profile
</Button>
</div>
) : (
<p className="text-sm text-muted-foreground py-4">
No profiles match "{searchQuery}"
</p>
)}
</div>
) : (
<div className="p-2 space-y-1">
{filteredProfiles.map((profile) => (
<ProfileListItem
key={profile.name}
profile={profile}
isSelected={selectedProfile === profile.name}
onSelect={() => {
setSelectedProfile(profile.name);
}}
onDelete={() => setDeleteConfirm(profile.name)}
/>
))}
</div>
)}
</ScrollArea>
{/* Footer Stats */}
{profiles.length > 0 && (
<div className="p-3 border-t bg-background text-xs text-muted-foreground">
<div className="flex items-center justify-between">
<span>
{profiles.length} profile{profiles.length !== 1 ? 's' : ''}
</span>
<span className="flex items-center gap-1">
<CheckCircle2 className="w-3 h-3 text-green-600" />
{profiles.filter((p) => p.configured).length} configured
</span>
</div>
</div>
)}
</ScrollArea>
{/* Footer Stats */}
{profiles.length > 0 && (
<div className="p-3 border-t bg-background text-xs text-muted-foreground">
<div className="flex items-center justify-between">
<span>
{profiles.length} profile{profiles.length !== 1 ? 's' : ''}
</span>
<span className="flex items-center gap-1">
<CheckCircle2 className="w-3 h-3 text-green-600" />
{profiles.filter((p) => p.configured).length} configured
</span>
</div>
</div>
)}
</div>
{/* Right Panel - Editor */}
<div className="flex-1 flex flex-col min-w-0">
{selectedProfileData ? (
<ProfileEditor
profileName={selectedProfileData.name}
onDelete={() => setDeleteConfirm(selectedProfileData.name)}
/>
) : (
<EmptyState
{/* OpenRouter Promo - always visible */}
<OpenRouterPromoCard
onCreateClick={() => {
setCreateMode('openrouter');
setCreateDialogOpen(true);
}}
/>
)}
</div>
{/* Right Panel - Editor or QuickStart */}
<div className="flex-1 flex flex-col min-w-0">
{selectedProfileData ? (
<ProfileEditor
profileName={selectedProfileData.name}
onDelete={() => setDeleteConfirm(selectedProfileData.name)}
/>
) : (
<OpenRouterQuickStart
onOpenRouterClick={() => {
setCreateMode('openrouter');
setCreateDialogOpen(true);
}}
onCustomClick={() => {
setCreateMode('normal');
setCreateDialogOpen(true);
}}
/>
)}
</div>
</div>
{/* Create Dialog */}
@@ -214,6 +229,7 @@ export function ApiPage() {
open={isCreateDialogOpen}
onOpenChange={setCreateDialogOpen}
onSuccess={handleCreateSuccess}
initialMode={createMode}
/>
{/* Delete Confirmation */}
@@ -289,66 +305,3 @@ function ProfileListItem({
</div>
);
}
/** Empty state when no profile is selected */
function EmptyState({ onCreateClick }: { onCreateClick: () => void }) {
return (
<div className="flex-1 flex items-center justify-center bg-muted/20">
<div className="text-center max-w-md px-8">
<Settings2 className="w-16 h-16 mx-auto text-muted-foreground/30 mb-6" />
<h2 className="text-xl font-semibold mb-2">API Profile Manager</h2>
<p className="text-muted-foreground mb-6">
Configure custom API endpoints for Claude CLI. Connect to proxy services like copilot-api,
OpenRouter, or your own API backend.
</p>
<div className="space-y-3">
<Button onClick={onCreateClick} className="w-full">
<Plus className="w-4 h-4 mr-2" />
Create Your First Profile
</Button>
<Separator className="my-4" />
<div className="text-left space-y-2">
<p className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
What you can configure:
</p>
<ul className="text-sm text-muted-foreground space-y-1.5">
<li className="flex items-start gap-2">
<Badge variant="outline" className="text-xs shrink-0 mt-0.5">
URL
</Badge>
<span>Custom API base URL endpoint</span>
</li>
<li className="flex items-start gap-2">
<Badge variant="outline" className="text-xs shrink-0 mt-0.5">
Auth
</Badge>
<span>API key or authentication token</span>
</li>
<li className="flex items-start gap-2">
<Badge variant="outline" className="text-xs shrink-0 mt-0.5">
Models
</Badge>
<span>Model mapping for Opus/Sonnet/Haiku</span>
</li>
</ul>
</div>
<div className="pt-4">
<a
href="https://github.com/kaitranntt/ccs#api-profiles"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center text-xs text-primary hover:underline"
>
Learn more about API profiles
<ExternalLink className="w-3 h-3 ml-1" />
</a>
</div>
</div>
</div>
</div>
);
}