From 9c90e1dc2cdc8aa11bb82ae4b337f7fed2f4c373 Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Sat, 20 Dec 2025 19:47:58 -0500 Subject: [PATCH] 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 --- ui/src/components/profiles/index.ts | 3 + .../components/profiles/openrouter-banner.tsx | 84 ++++++++++++++++ .../profiles/openrouter-promo-card.tsx | 41 ++++++++ .../profiles/openrouter-quick-start.tsx | 98 +++++++++++++++++++ ui/src/lib/provider-presets.ts | 85 ++++++++++++++++ 5 files changed, 311 insertions(+) create mode 100644 ui/src/components/profiles/openrouter-banner.tsx create mode 100644 ui/src/components/profiles/openrouter-promo-card.tsx create mode 100644 ui/src/components/profiles/openrouter-quick-start.tsx create mode 100644 ui/src/lib/provider-presets.ts diff --git a/ui/src/components/profiles/index.ts b/ui/src/components/profiles/index.ts index db201102..432cbca3 100644 --- a/ui/src/components/profiles/index.ts +++ b/ui/src/components/profiles/index.ts @@ -15,6 +15,9 @@ export type { Settings, SettingsResponse, ProfileEditorProps } from './editor'; // OpenRouter components export { OpenRouterBadge } from './openrouter-badge'; +export { OpenRouterBanner } from './openrouter-banner'; 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 type { TierMapping } from './model-tier-mapping'; diff --git a/ui/src/components/profiles/openrouter-banner.tsx b/ui/src/components/profiles/openrouter-banner.tsx new file mode 100644 index 00000000..1a34f287 --- /dev/null +++ b/ui/src/components/profiles/openrouter-banner.tsx @@ -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 ( +
+
+
+
+ +
+
+

NEW: OpenRouter Integration

+

+ Browse {isLoading ? '300+' : `${modelCount}+`} models from OpenAI, Anthropic, Google, + Meta and more. +

+
+
+ +
+ {onCreateClick && ( + + )} + + Learn more + + + +
+
+
+ ); +} diff --git a/ui/src/components/profiles/openrouter-promo-card.tsx b/ui/src/components/profiles/openrouter-promo-card.tsx new file mode 100644 index 00000000..881f3368 --- /dev/null +++ b/ui/src/components/profiles/openrouter-promo-card.tsx @@ -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 ( +
+
+
+ +
+
+

OpenRouter

+

+ {isLoading ? '300+' : `${modelCount}+`} models available +

+
+ +
+
+ ); +} diff --git a/ui/src/components/profiles/openrouter-quick-start.tsx b/ui/src/components/profiles/openrouter-quick-start.tsx new file mode 100644 index 00000000..c320bbbc --- /dev/null +++ b/ui/src/components/profiles/openrouter-quick-start.tsx @@ -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 ( +
+
+ {/* Main OpenRouter Card */} + + +
+
+ OpenRouter +
+ + Recommended + +
+ Start with OpenRouter + + Access {isLoading ? '300+' : `${modelCount}+`} models from OpenAI, Anthropic, Google, + Meta and more - all through one API. + +
+ + {/* Key Features */} +
+
+ + One API, all providers +
+
+ + Model tier mapping +
+
+ + + +

+ Get your API key at{' '} + + openrouter.ai/keys + + +

+
+
+ + {/* Divider */} +
+ + or + +
+ + {/* Custom Option */} + +
+
+ ); +} diff --git a/ui/src/lib/provider-presets.ts b/ui/src/lib/provider-presets.ts new file mode 100644 index 00000000..8a0ade76 --- /dev/null +++ b/ui/src/lib/provider-presets.ts @@ -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())); +}