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 committed 2025-12-20 19:49:12 -05:00
1 parent adcc3235f0
commit 05380e21b4
1 file changed
+140 -187
+140 -187
View File
@@ -7,22 +7,21 @@ import { useState, useMemo } from 'react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { ScrollArea } from '@/components/ui/scroll-area'; import { ScrollArea } from '@/components/ui/scroll-area';
import { Badge } from '@/components/ui/badge';
import { Separator } from '@/components/ui/separator';
import { import {
Plus, Plus,
Search, Search,
Settings2,
Trash2, Trash2,
CheckCircle2, CheckCircle2,
AlertCircle, AlertCircle,
Server, Server,
ExternalLink,
FileJson, FileJson,
RefreshCw, RefreshCw,
} from 'lucide-react'; } from 'lucide-react';
import { ProfileEditor } from '@/components/profile-editor'; import { ProfileEditor } from '@/components/profile-editor';
import { ProfileCreateDialog } from '@/components/profiles/profile-create-dialog'; 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 { useProfiles, useDeleteProfile } from '@/hooks/use-profiles';
import { useOpenRouterModels } from '@/hooks/use-openrouter-models'; import { useOpenRouterModels } from '@/hooks/use-openrouter-models';
import { ConfirmDialog } from '@/components/shared/confirm-dialog'; import { ConfirmDialog } from '@/components/shared/confirm-dialog';
@@ -36,6 +35,7 @@ export function ApiPage() {
const [selectedProfile, setSelectedProfile] = useState<string | null>(null); const [selectedProfile, setSelectedProfile] = useState<string | null>(null);
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
const [isCreateDialogOpen, setCreateDialogOpen] = useState(false); const [isCreateDialogOpen, setCreateDialogOpen] = useState(false);
const [createMode, setCreateMode] = useState<'normal' | 'openrouter'>('normal');
const [deleteConfirm, setDeleteConfirm] = useState<string | null>(null); const [deleteConfirm, setDeleteConfirm] = useState<string | null>(null);
// Prefetch OpenRouter models when page loads (lazy - won't block render) // Prefetch OpenRouter models when page loads (lazy - won't block render)
@@ -50,13 +50,11 @@ export function ApiPage() {
[profiles, searchQuery] [profiles, searchQuery]
); );
// Compute effective selected profile (auto-select first if none selected) // selectedProfile is null by default - user must click to select
const effectiveSelectedProfile = useMemo(() => { // This allows OpenRouterQuickStart to show as the default right panel
if (selectedProfile && profiles.some((p) => p.name === selectedProfile)) { const selectedProfileData = selectedProfile
return selectedProfile; ? profiles.find((p) => p.name === selectedProfile)
} : null;
return profiles.length > 0 ? profiles[0].name : null;
}, [selectedProfile, profiles]);
// Handle profile deletion // Handle profile deletion
const handleDelete = (name: string) => { const handleDelete = (name: string) => {
@@ -76,137 +74,154 @@ export function ApiPage() {
setSelectedProfile(name); setSelectedProfile(name);
}; };
const selectedProfileData = profiles.find((p) => p.name === effectiveSelectedProfile);
return ( return (
<div className="h-[calc(100vh-100px)] flex"> <div className="h-[calc(100vh-100px)] flex flex-col">
{/* Left Panel - Profiles List */} {/* OpenRouter Announcement Banner */}
<div className="w-80 border-r flex flex-col bg-muted/30"> <OpenRouterBanner onCreateClick={() => setCreateDialogOpen(true)} />
{/* 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>
{/* Search */} {/* Main Content */}
<div className="relative"> <div className="flex-1 flex min-h-0">
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" /> {/* Left Panel - Profiles List */}
<Input <div className="w-80 border-r flex flex-col bg-muted/30">
placeholder="Search profiles..." {/* Header */}
className="pl-8 h-9" <div className="p-4 border-b bg-background">
value={searchQuery} <div className="flex items-center justify-between mb-3">
onChange={(e) => setSearchQuery(e.target.value)} <div className="flex items-center gap-2">
/> <Server className="w-5 h-5 text-primary" />
</div> <h1 className="font-semibold">API Profiles</h1>
</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>
</div> </div>
<Button
size="sm"
onClick={() => {
setCreateDialogOpen(true);
}}
>
<Plus className="w-4 h-4 mr-1" />
New
</Button>
</div> </div>
) : filteredProfiles.length === 0 ? (
<div className="p-4 text-center"> {/* Search */}
{profiles.length === 0 ? ( <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"> <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> <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"> <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> </p>
</div> </div>
<Button <Button size="sm" variant="outline" onClick={() => refetch()}>
size="sm" <RefreshCw className="w-4 h-4 mr-1" />
variant="outline" Retry
onClick={() => {
setCreateDialogOpen(true);
}}
>
<Plus className="w-4 h-4 mr-1" />
Create Profile
</Button> </Button>
</div> </div>
) : ( </div>
<p className="text-sm text-muted-foreground py-4"> ) : filteredProfiles.length === 0 ? (
No profiles match "{searchQuery}" <div className="p-4 text-center">
</p> {profiles.length === 0 ? (
)} <div className="space-y-3 py-8">
</div> <FileJson className="w-12 h-12 mx-auto text-muted-foreground/50" />
) : ( <div>
<div className="p-2 space-y-1"> <p className="text-sm font-medium">No API profiles yet</p>
{filteredProfiles.map((profile) => ( <p className="text-xs text-muted-foreground mt-1">
<ProfileListItem Create your first profile to connect to custom API endpoints
key={profile.name} </p>
profile={profile} </div>
isSelected={effectiveSelectedProfile === profile.name} <Button
onSelect={() => { size="sm"
setSelectedProfile(profile.name); variant="outline"
}} onClick={() => {
onDelete={() => setDeleteConfirm(profile.name)} 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> </div>
)} )}
</ScrollArea>
{/* Footer Stats */} {/* OpenRouter Promo - always visible */}
{profiles.length > 0 && ( <OpenRouterPromoCard
<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
onCreateClick={() => { onCreateClick={() => {
setCreateMode('openrouter');
setCreateDialogOpen(true); 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> </div>
{/* Create Dialog */} {/* Create Dialog */}
@@ -214,6 +229,7 @@ export function ApiPage() {
open={isCreateDialogOpen} open={isCreateDialogOpen}
onOpenChange={setCreateDialogOpen} onOpenChange={setCreateDialogOpen}
onSuccess={handleCreateSuccess} onSuccess={handleCreateSuccess}
initialMode={createMode}
/> />
{/* Delete Confirmation */} {/* Delete Confirmation */}
@@ -289,66 +305,3 @@ function ProfileListItem({
</div> </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>
);
}