mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-03 00:17:47 +00:00
feat(ui): add provider presets and OpenRouter promo components
- add provider-presets.ts with OpenRouter/GLM/GLMT/Kimi configs - add OpenRouterBanner for announcement header - add OpenRouterPromoCard for sidebar promotion - add OpenRouterQuickStart for default right panel - export new components from profiles index
This commit is contained in:
@@ -15,6 +15,9 @@ export type { Settings, SettingsResponse, ProfileEditorProps } from './editor';
|
|||||||
|
|
||||||
// OpenRouter components
|
// OpenRouter components
|
||||||
export { OpenRouterBadge } from './openrouter-badge';
|
export { OpenRouterBadge } from './openrouter-badge';
|
||||||
|
export { OpenRouterBanner } from './openrouter-banner';
|
||||||
export { OpenRouterModelPicker } from './openrouter-model-picker';
|
export { OpenRouterModelPicker } from './openrouter-model-picker';
|
||||||
|
export { OpenRouterPromoCard } from './openrouter-promo-card';
|
||||||
|
export { OpenRouterQuickStart } from './openrouter-quick-start';
|
||||||
export { ModelTierMapping } from './model-tier-mapping';
|
export { ModelTierMapping } from './model-tier-mapping';
|
||||||
export type { TierMapping } from './model-tier-mapping';
|
export type { TierMapping } from './model-tier-mapping';
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
/**
|
||||||
|
* OpenRouter Feature Banner
|
||||||
|
* Dismissible announcement banner for OpenRouter integration
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* eslint-disable react-hooks/set-state-in-effect */
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { X, Sparkles, ExternalLink } from 'lucide-react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { useOpenRouterReady } from '@/hooks/use-openrouter-models';
|
||||||
|
|
||||||
|
const BANNER_DISMISSED_KEY = 'ccs:openrouter-banner-dismissed';
|
||||||
|
|
||||||
|
interface OpenRouterBannerProps {
|
||||||
|
onCreateClick?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function OpenRouterBanner({ onCreateClick }: OpenRouterBannerProps) {
|
||||||
|
const [dismissed, setDismissed] = useState(true); // Start hidden to avoid flash
|
||||||
|
const { modelCount, isLoading } = useOpenRouterReady();
|
||||||
|
|
||||||
|
// Check localStorage on mount
|
||||||
|
useEffect(() => {
|
||||||
|
const isDismissed = localStorage.getItem(BANNER_DISMISSED_KEY) === 'true';
|
||||||
|
setDismissed(isDismissed);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleDismiss = () => {
|
||||||
|
localStorage.setItem(BANNER_DISMISSED_KEY, 'true');
|
||||||
|
setDismissed(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (dismissed) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-gradient-to-r from-orange-500 to-orange-600 text-white px-4 py-3 relative shrink-0">
|
||||||
|
<div className="flex items-center justify-between gap-4 max-w-screen-xl mx-auto">
|
||||||
|
<div className="flex items-center gap-3 flex-1 min-w-0">
|
||||||
|
<div className="p-1.5 bg-white/20 rounded-md shrink-0">
|
||||||
|
<Sparkles className="w-4 h-4" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<p className="font-medium text-sm">NEW: OpenRouter Integration</p>
|
||||||
|
<p className="text-xs text-white/80 truncate">
|
||||||
|
Browse {isLoading ? '300+' : `${modelCount}+`} models from OpenAI, Anthropic, Google,
|
||||||
|
Meta and more.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2 shrink-0">
|
||||||
|
{onCreateClick && (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="secondary"
|
||||||
|
onClick={onCreateClick}
|
||||||
|
className="bg-white text-orange-600 hover:bg-white/90 h-8"
|
||||||
|
>
|
||||||
|
Try it now
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<a
|
||||||
|
href="https://openrouter.ai"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-xs text-white/80 hover:text-white hidden sm:flex items-center gap-1"
|
||||||
|
>
|
||||||
|
Learn more
|
||||||
|
<ExternalLink className="w-3 h-3" />
|
||||||
|
</a>
|
||||||
|
<Button
|
||||||
|
size="icon"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={handleDismiss}
|
||||||
|
className="h-7 w-7 text-white/70 hover:text-white hover:bg-white/20"
|
||||||
|
>
|
||||||
|
<X className="w-4 h-4" />
|
||||||
|
<span className="sr-only">Dismiss</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
/**
|
||||||
|
* OpenRouter Promo Card
|
||||||
|
* Permanent promotional card for OpenRouter - always visible in sidebar footer
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { useOpenRouterReady } from '@/hooks/use-openrouter-models';
|
||||||
|
import { Zap } from 'lucide-react';
|
||||||
|
|
||||||
|
interface OpenRouterPromoCardProps {
|
||||||
|
onCreateClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function OpenRouterPromoCard({ onCreateClick }: OpenRouterPromoCardProps) {
|
||||||
|
const { modelCount, isLoading } = useOpenRouterReady();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-3 border-t bg-gradient-to-r from-orange-50 to-orange-100/50 dark:from-orange-950/30 dark:to-orange-900/20">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="p-1.5 bg-orange-100 dark:bg-orange-900/40 rounded shrink-0">
|
||||||
|
<img src="/icons/openrouter.svg" alt="" className="w-4 h-4" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<p className="text-xs font-medium text-orange-700 dark:text-orange-300">OpenRouter</p>
|
||||||
|
<p className="text-[10px] text-muted-foreground truncate">
|
||||||
|
{isLoading ? '300+' : `${modelCount}+`} models available
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={onCreateClick}
|
||||||
|
className="h-7 px-2 text-orange-600 hover:text-orange-700 hover:bg-orange-100 dark:hover:bg-orange-900/30"
|
||||||
|
>
|
||||||
|
<Zap className="w-3 h-3 mr-1" />
|
||||||
|
<span className="text-xs">Add</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
/**
|
||||||
|
* OpenRouter Quick Start Card
|
||||||
|
* Prominent CTA for new users to create OpenRouter profile
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Separator } from '@/components/ui/separator';
|
||||||
|
import { useOpenRouterReady } from '@/hooks/use-openrouter-models';
|
||||||
|
import { Sparkles, ExternalLink, ArrowRight, Zap } from 'lucide-react';
|
||||||
|
|
||||||
|
interface OpenRouterQuickStartProps {
|
||||||
|
onOpenRouterClick: () => void;
|
||||||
|
onCustomClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function OpenRouterQuickStart({
|
||||||
|
onOpenRouterClick,
|
||||||
|
onCustomClick,
|
||||||
|
}: OpenRouterQuickStartProps) {
|
||||||
|
const { modelCount, isLoading } = useOpenRouterReady();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex-1 flex items-center justify-center bg-muted/20 p-8">
|
||||||
|
<div className="max-w-lg w-full space-y-6">
|
||||||
|
{/* Main OpenRouter Card */}
|
||||||
|
<Card className="border-orange-200 dark:border-orange-800/50 bg-gradient-to-br from-orange-50/50 to-background dark:from-orange-950/20">
|
||||||
|
<CardHeader className="pb-3">
|
||||||
|
<div className="flex items-center gap-3 mb-2">
|
||||||
|
<div className="p-2 rounded-lg bg-orange-100 dark:bg-orange-900/30">
|
||||||
|
<img src="/icons/openrouter.svg" alt="OpenRouter" className="w-6 h-6" />
|
||||||
|
</div>
|
||||||
|
<Badge
|
||||||
|
variant="secondary"
|
||||||
|
className="bg-orange-100 text-orange-700 dark:bg-orange-900/40 dark:text-orange-300"
|
||||||
|
>
|
||||||
|
Recommended
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<CardTitle className="text-xl">Start with OpenRouter</CardTitle>
|
||||||
|
<CardDescription className="text-base">
|
||||||
|
Access {isLoading ? '300+' : `${modelCount}+`} models from OpenAI, Anthropic, Google,
|
||||||
|
Meta and more - all through one API.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
{/* Key Features */}
|
||||||
|
<div className="grid grid-cols-2 gap-3 text-sm">
|
||||||
|
<div className="flex items-center gap-2 text-muted-foreground">
|
||||||
|
<Zap className="w-4 h-4 text-orange-500" />
|
||||||
|
<span>One API, all providers</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 text-muted-foreground">
|
||||||
|
<Sparkles className="w-4 h-4 text-orange-500" />
|
||||||
|
<span>Model tier mapping</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={onOpenRouterClick}
|
||||||
|
className="w-full bg-orange-600 hover:bg-orange-700 text-white"
|
||||||
|
size="lg"
|
||||||
|
>
|
||||||
|
Create OpenRouter Profile
|
||||||
|
<ArrowRight className="w-4 h-4 ml-2" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<p className="text-xs text-center text-muted-foreground">
|
||||||
|
Get your API key at{' '}
|
||||||
|
<a
|
||||||
|
href="https://openrouter.ai/keys"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-orange-600 hover:underline inline-flex items-center gap-1"
|
||||||
|
>
|
||||||
|
openrouter.ai/keys
|
||||||
|
<ExternalLink className="w-3 h-3" />
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Divider */}
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<Separator className="flex-1" />
|
||||||
|
<span className="text-xs text-muted-foreground">or</span>
|
||||||
|
<Separator className="flex-1" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Custom Option */}
|
||||||
|
<Button variant="outline" onClick={onCustomClick} className="w-full">
|
||||||
|
Create Custom API Profile
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
/**
|
||||||
|
* Provider Presets Configuration
|
||||||
|
* Pre-configured templates for common API providers
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface ProviderPreset {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
baseUrl: string;
|
||||||
|
defaultProfileName: string;
|
||||||
|
badge?: string;
|
||||||
|
featured?: boolean;
|
||||||
|
icon?: string;
|
||||||
|
defaultModel?: string;
|
||||||
|
requiresApiKey: boolean;
|
||||||
|
apiKeyPlaceholder: string;
|
||||||
|
apiKeyHint?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const OPENROUTER_BASE_URL = 'https://openrouter.ai/api/v1';
|
||||||
|
|
||||||
|
export const PROVIDER_PRESETS: ProviderPreset[] = [
|
||||||
|
{
|
||||||
|
id: 'openrouter',
|
||||||
|
name: 'OpenRouter',
|
||||||
|
description: '349+ models from OpenAI, Anthropic, Google, Meta',
|
||||||
|
baseUrl: OPENROUTER_BASE_URL,
|
||||||
|
defaultProfileName: 'openrouter',
|
||||||
|
badge: '349+ models',
|
||||||
|
featured: true,
|
||||||
|
icon: '/icons/openrouter.svg',
|
||||||
|
defaultModel: 'anthropic/claude-sonnet-4',
|
||||||
|
requiresApiKey: true,
|
||||||
|
apiKeyPlaceholder: 'sk-or-...',
|
||||||
|
apiKeyHint: 'Get your API key at openrouter.ai/keys',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'glm',
|
||||||
|
name: 'GLM',
|
||||||
|
description: 'Claude via Z.AI (GitHub Copilot)',
|
||||||
|
baseUrl: 'https://api.z.ai/api/anthropic',
|
||||||
|
defaultProfileName: 'glm',
|
||||||
|
badge: 'Free',
|
||||||
|
defaultModel: 'glm-4.6',
|
||||||
|
requiresApiKey: true,
|
||||||
|
apiKeyPlaceholder: 'ghp_...',
|
||||||
|
apiKeyHint: 'Get your API key from Z.AI',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'glmt',
|
||||||
|
name: 'GLMT',
|
||||||
|
description: 'GLM with Thinking mode support',
|
||||||
|
baseUrl: 'https://api.z.ai/api/coding/paas/v4/chat/completions',
|
||||||
|
defaultProfileName: 'glmt',
|
||||||
|
badge: 'Thinking',
|
||||||
|
defaultModel: 'glm-4.6',
|
||||||
|
requiresApiKey: true,
|
||||||
|
apiKeyPlaceholder: 'ghp_...',
|
||||||
|
apiKeyHint: 'Same API key as GLM',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'kimi',
|
||||||
|
name: 'Kimi',
|
||||||
|
description: 'Moonshot AI - Fast reasoning model',
|
||||||
|
baseUrl: 'https://api.kimi.com/coding/',
|
||||||
|
defaultProfileName: 'kimi',
|
||||||
|
badge: 'Reasoning',
|
||||||
|
defaultModel: 'kimi-k2-thinking-turbo',
|
||||||
|
requiresApiKey: true,
|
||||||
|
apiKeyPlaceholder: 'sk-...',
|
||||||
|
apiKeyHint: 'Get your API key from Moonshot AI',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
/** Get preset by ID */
|
||||||
|
export function getPresetById(id: string): ProviderPreset | undefined {
|
||||||
|
return PROVIDER_PRESETS.find((p) => p.id === id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Check if a URL matches a known preset */
|
||||||
|
export function detectPresetFromUrl(baseUrl: string): ProviderPreset | undefined {
|
||||||
|
const normalizedUrl = baseUrl.toLowerCase().trim();
|
||||||
|
return PROVIDER_PRESETS.find((p) => normalizedUrl.includes(p.baseUrl.toLowerCase()));
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user