From aef58d66ce69777cf8aba6f43f615560ef820b62 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Mon, 23 Mar 2026 11:43:06 -0400 Subject: [PATCH] fix(dashboard): refine api profile quick start state - prevent the providers page from introducing nested overflow - show a select-profile state when profiles already exist - localize the new quick-start and AI Providers copy --- .../profiles/openrouter-quick-start.tsx | 372 ++++++++++-------- ui/src/lib/i18n.ts | 76 ++++ ui/src/pages/api.tsx | 10 +- 3 files changed, 281 insertions(+), 177 deletions(-) diff --git a/ui/src/components/profiles/openrouter-quick-start.tsx b/ui/src/components/profiles/openrouter-quick-start.tsx index 7dc74afd..9fc4e746 100644 --- a/ui/src/components/profiles/openrouter-quick-start.tsx +++ b/ui/src/components/profiles/openrouter-quick-start.tsx @@ -1,32 +1,92 @@ -/** - * OpenRouter Quick Start Card - * Prominent CTA for new users to create OpenRouter profile - */ - +import type { ReactNode } from 'react'; 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 { cn } from '@/lib/utils'; import { - Sparkles, - ExternalLink, ArrowRight, - Zap, CloudCog, + ExternalLink, KeyRound, SlidersHorizontal, + Sparkles, + Zap, } from 'lucide-react'; import { useTranslation } from 'react-i18next'; interface OpenRouterQuickStartProps { + hasProfiles: boolean; + profileCount: number; onOpenRouterClick: () => void; onAlibabaCodingPlanClick: () => void; onCliproxyClick: () => void; onCustomClick: () => void; } +interface QuickStartCardProps { + badge: string; + badgeClassName?: string; + className?: string; + title: string; + description: string; + visual: ReactNode; + highlights: Array<{ icon: ReactNode; label: string }>; + actionLabel: string; + actionClassName: string; + onAction: () => void; + footer?: ReactNode; +} + +function QuickStartCard({ + badge, + badgeClassName, + className, + title, + description, + visual, + highlights, + actionLabel, + actionClassName, + onAction, + footer, +}: QuickStartCardProps) { + return ( + + +
+ {visual} + + {badge} + +
+
+ {title} + {description} +
+
+ +
+ {highlights.map((item) => ( +
+ {item.icon} + {item.label} +
+ ))} +
+ + {footer ?
{footer}
: null} +
+
+ ); +} + export function OpenRouterQuickStart({ + hasProfiles, + profileCount, onOpenRouterClick, onAlibabaCodingPlanClick, onCliproxyClick, @@ -34,185 +94,151 @@ export function OpenRouterQuickStart({ }: OpenRouterQuickStartProps) { const { t } = useTranslation(); const { modelCount, isLoading } = useOpenRouterReady(); + const modelCountLabel = isLoading ? '300+' : `${modelCount}+`; + const profileSummaryLabel = hasProfiles + ? t('openrouterQuickStart.profileCount', { count: profileCount }) + : t('openrouterQuickStart.recommended'); + const summaryTitle = hasProfiles + ? t('openrouterQuickStart.selectProfileTitle') + : t('apiProfiles.noProfilesYet'); + const summaryDescription = hasProfiles + ? t('openrouterQuickStart.summaryDescriptionWithProfiles', { count: profileCount }) + : t('openrouterQuickStart.summaryDescriptionNoProfiles'); return ( -
-
- {/* Main OpenRouter Card */} - - -
-
- OpenRouter +
+
+ + +
+
+ {profileSummaryLabel} + + {t('openrouterQuickStart.openrouterModelsBadge', { modelCountLabel })} +
- - {t('openrouterQuickStart.recommended')} - -
- {t('openrouterQuickStart.title')} - - {t('openrouterQuickStart.description', { - modelCountLabel: isLoading ? '300+' : `${modelCount}+`, - })} - - - - {/* Key Features */} -
-
- - {t('openrouterQuickStart.featureOneApi')} -
-
- - {t('openrouterQuickStart.featureTierMapping')} +
+

{summaryTitle}

+

+ {summaryDescription} +

- - - -

- {t('openrouterQuickStart.getApiKeyAt')}{' '} - - openrouter.ai/keys - - -

- {/* Alibaba Coding Plan Card */} - - -
-
+
+ + OpenRouter +
+ } + highlights={[ + { + icon: , + label: t('openrouterQuickStart.featureOneApi'), + }, + { + icon: , + 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')}{' '} + + openrouter.ai/keys + + + + } + /> + + + +
+ } + highlights={[ + { + icon: , + label: t('openrouterQuickStart.runtimeProviderFeatureConnectors'), + }, + { + icon: , + label: t('openrouterQuickStart.runtimeProviderFeatureSecrets'), + }, + ]} + actionLabel={t('openrouterQuickStart.runtimeProviderTitle')} + actionClassName="w-full bg-emerald-600 text-white hover:bg-emerald-600/90" + onAction={onCliproxyClick} + footer={{t('openrouterQuickStart.runtimeProviderFooter')}} + /> + + Alibaba Coding Plan
- - {t('alibabaCodingPlanQuickStart.recommended')} - -
- {t('alibabaCodingPlanQuickStart.title')} - - {t('alibabaCodingPlanQuickStart.description')} - - - -
-
- - {t('alibabaCodingPlanQuickStart.featureEndpoint')} -
-
- - {t('alibabaCodingPlanQuickStart.featureKeyFormat')} -
-
- - - -

- {t('alibabaCodingPlanQuickStart.readGuideAt')}{' '} - - Alibaba Cloud Model Studio - - -

-
-
- - - -
-
- -
- - Configure in AI Providers - -
- Manage CLIProxy AI providers - - Configure Gemini, Codex, Claude, Vertex, and OpenAI-compatible connectors directly in - the dedicated CLIProxy AI Providers page. - -
- -
-
- - Dedicated /cliproxy/ai-providers workspace -
-
- - Manage provider secrets outside API Profiles -
-
- - - -

- Keep runtime provider configuration in CLIProxy, then create API Profiles only when - you need standalone Anthropic-compatible endpoints. -

-
-
- - {/* Divider */} -
- - {t('openrouterQuickStart.or')} - + } + highlights={[ + { + icon: , + label: t('alibabaCodingPlanQuickStart.featureEndpoint'), + }, + { + icon: , + 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')}{' '} + + Alibaba Cloud Model Studio + + + + } + />
- - {/* Custom Option */} -
); diff --git a/ui/src/lib/i18n.ts b/ui/src/lib/i18n.ts index 038d5ee0..a411699b 100644 --- a/ui/src/lib/i18n.ts +++ b/ui/src/lib/i18n.ts @@ -241,6 +241,25 @@ const resources = { 'Access {{modelCountLabel}} models from OpenAI, Anthropic, Google, Meta and more - all through one API.', featureOneApi: 'One API, all providers', featureTierMapping: 'Model tier mapping', + profileCount: '{{count}} profile', + profileCount_other: '{{count}} profiles', + selectProfileTitle: 'Select an API profile', + summaryDescriptionWithProfiles: + 'You already have {{count}} profile in this workspace. Select one from the left rail to edit it, or create another profile from here.', + summaryDescriptionWithProfiles_other: + 'You already have {{count}} profiles in this workspace. Select one from the left rail to edit it, or create another profile from here.', + summaryDescriptionNoProfiles: + 'Use API Profiles for Anthropic-compatible endpoints. Use AI Providers for Gemini, Codex, Claude, Vertex, and OpenAI-compatible connectors.', + openrouterModelsBadge: '{{modelCountLabel}} OpenRouter models', + runtimeProviderBadge: 'Runtime provider setup', + runtimeProviderTitle: 'Open AI Providers', + runtimeProviderDescription: + 'Manage shared provider keys and runtime connectors before creating standalone API Profiles.', + runtimeProviderFeatureConnectors: + 'Gemini, Codex, Claude, Vertex, and OpenAI-compatible connectors', + runtimeProviderFeatureSecrets: 'Shared secrets stay outside individual API profiles', + runtimeProviderFooter: + 'Best when multiple profiles should share one provider configuration.', createOpenRouterProfile: 'Create OpenRouter Profile', getApiKeyAt: 'Get your API key at', or: 'or', @@ -1420,6 +1439,24 @@ const resources = { '通过一个 API 即可访问来自 OpenAI、Anthropic、Google、Meta 等的 {{modelCountLabel}} 个模型。', featureOneApi: '一个 API,接入全部提供商', featureTierMapping: '模型档位映射', + profileCount: '{{count}} 个 API 配置', + profileCount_other: '{{count}} 个 API 配置', + selectProfileTitle: '选择一个 API 配置', + summaryDescriptionWithProfiles: + '此工作区中已存在 {{count}} 个 API 配置。请从左侧栏选择一个进行编辑,或在此继续创建新的配置。', + summaryDescriptionWithProfiles_other: + '此工作区中已存在 {{count}} 个 API 配置。请从左侧栏选择一个进行编辑,或在此继续创建新的配置。', + summaryDescriptionNoProfiles: + 'API 配置适用于 Anthropic 兼容端点。AI Providers 适用于 Gemini、Codex、Claude、Vertex 以及 OpenAI 兼容连接器。', + openrouterModelsBadge: '{{modelCountLabel}} 个 OpenRouter 模型', + runtimeProviderBadge: '运行时提供商设置', + runtimeProviderTitle: '打开 AI Providers', + runtimeProviderDescription: + '先管理共享的提供商密钥和运行时连接器,再创建独立的 API 配置。', + runtimeProviderFeatureConnectors: + 'Gemini、Codex、Claude、Vertex 以及 OpenAI 兼容连接器', + runtimeProviderFeatureSecrets: '共享密钥与单个 API 配置分离存放', + runtimeProviderFooter: '当多个配置需要共享同一提供商设置时最适合使用。', createOpenRouterProfile: '创建 OpenRouter 配置', getApiKeyAt: '在此获取 API Key:', or: '或', @@ -2566,6 +2603,25 @@ const resources = { 'Truy cập các mô hình {{modelCountLabel}} từ OpenAI, Anthropic, Google, Meta, v.v. - tất cả đều thông qua một API.', featureOneApi: 'Một API cho mọi nhà cung cấp', featureTierMapping: 'Ánh xạ tầng mô hình', + profileCount: '{{count}} hồ sơ', + profileCount_other: '{{count}} hồ sơ', + selectProfileTitle: 'Chọn một hồ sơ API', + summaryDescriptionWithProfiles: + 'Bạn đã có {{count}} hồ sơ trong workspace này. Chọn một hồ sơ ở thanh bên trái để chỉnh sửa, hoặc tạo thêm hồ sơ mới tại đây.', + summaryDescriptionWithProfiles_other: + 'Bạn đã có {{count}} hồ sơ trong workspace này. Chọn một hồ sơ ở thanh bên trái để chỉnh sửa, hoặc tạo thêm hồ sơ mới tại đây.', + summaryDescriptionNoProfiles: + 'Dùng API Profiles cho các endpoint tương thích Anthropic. Dùng AI Providers cho Gemini, Codex, Claude, Vertex và các connector tương thích OpenAI.', + openrouterModelsBadge: '{{modelCountLabel}} mô hình OpenRouter', + runtimeProviderBadge: 'Thiết lập nhà cung cấp runtime', + runtimeProviderTitle: 'Mở AI Providers', + runtimeProviderDescription: + 'Quản lý khóa nhà cung cấp dùng chung và các connector runtime trước khi tạo API Profiles độc lập.', + runtimeProviderFeatureConnectors: + 'Gemini, Codex, Claude, Vertex và các connector tương thích OpenAI', + runtimeProviderFeatureSecrets: 'Khóa dùng chung tách biệt khỏi từng API Profile', + runtimeProviderFooter: + 'Phù hợp nhất khi nhiều hồ sơ cần dùng chung một cấu hình nhà cung cấp.', createOpenRouterProfile: 'Tạo hồ sơ OpenRouter', getApiKeyAt: 'Nhận khóa API của bạn tại', or: 'hoặc', @@ -3767,6 +3823,26 @@ const resources = { 'OpenAI、Anthropic、Google、Meta などの {{modelCountLabel}} モデルを、1 つの API で利用できます。', featureOneApi: '1 つの API で全プロバイダー', featureTierMapping: 'モデルティアマッピング', + profileCount: '{{count}} 個の API プロファイル', + profileCount_other: '{{count}} 個の API プロファイル', + selectProfileTitle: 'API プロファイルを選択', + summaryDescriptionWithProfiles: + 'このワークスペースには既に {{count}} 個の API プロファイルがあります。左側レールから 1 つ選んで編集するか、ここから新しいプロファイルを作成してください。', + summaryDescriptionWithProfiles_other: + 'このワークスペースには既に {{count}} 個の API プロファイルがあります。左側レールから 1 つ選んで編集するか、ここから新しいプロファイルを作成してください。', + summaryDescriptionNoProfiles: + 'API Profiles は Anthropic 互換エンドポイント向けです。AI Providers は Gemini、Codex、Claude、Vertex、OpenAI 互換コネクタ向けです。', + openrouterModelsBadge: '{{modelCountLabel}} 個の OpenRouter モデル', + runtimeProviderBadge: 'ランタイムプロバイダー設定', + runtimeProviderTitle: 'AI Providers を開く', + runtimeProviderDescription: + '個別の API プロファイルを作成する前に、共有プロバイダーキーとランタイムコネクタを管理します。', + runtimeProviderFeatureConnectors: + 'Gemini、Codex、Claude、Vertex、OpenAI 互換コネクタ', + runtimeProviderFeatureSecrets: + '共有シークレットを個別の API プロファイルから切り離して管理', + runtimeProviderFooter: + '複数のプロファイルで 1 つのプロバイダー設定を共有したい場合に最適です。', createOpenRouterProfile: 'OpenRouter プロファイルを作成', getApiKeyAt: 'API キー取得:', or: 'または', diff --git a/ui/src/pages/api.tsx b/ui/src/pages/api.tsx index 47cdaf84..3fb37b33 100644 --- a/ui/src/pages/api.tsx +++ b/ui/src/pages/api.tsx @@ -206,9 +206,9 @@ export function ApiPage() { }; return ( -
+
setCreateDialogOpen(true)} /> -
+
@@ -262,7 +262,7 @@ export function ApiPage() {
- + {isLoading ? (
{t('apiProfiles.loadingProfiles')} @@ -354,7 +354,7 @@ export function ApiPage() { />
-
+
{selectedProfileData ? ( <>
@@ -387,6 +387,8 @@ export function ApiPage() { ) : ( 0} + profileCount={profiles.length} onCliproxyClick={() => { navigate('/cliproxy/ai-providers'); }}