mirror of
https://github.com/tiennm99/ccs.git
synced 2026-08-23 02:23:38 +00:00
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
This commit is contained in:
@@ -1,32 +1,92 @@
|
|||||||
/**
|
import type { ReactNode } from 'react';
|
||||||
* OpenRouter Quick Start Card
|
|
||||||
* Prominent CTA for new users to create OpenRouter profile
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Separator } from '@/components/ui/separator';
|
|
||||||
import { useOpenRouterReady } from '@/hooks/use-openrouter-models';
|
import { useOpenRouterReady } from '@/hooks/use-openrouter-models';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
import {
|
import {
|
||||||
Sparkles,
|
|
||||||
ExternalLink,
|
|
||||||
ArrowRight,
|
ArrowRight,
|
||||||
Zap,
|
|
||||||
CloudCog,
|
CloudCog,
|
||||||
|
ExternalLink,
|
||||||
KeyRound,
|
KeyRound,
|
||||||
SlidersHorizontal,
|
SlidersHorizontal,
|
||||||
|
Sparkles,
|
||||||
|
Zap,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
interface OpenRouterQuickStartProps {
|
interface OpenRouterQuickStartProps {
|
||||||
|
hasProfiles: boolean;
|
||||||
|
profileCount: number;
|
||||||
onOpenRouterClick: () => void;
|
onOpenRouterClick: () => void;
|
||||||
onAlibabaCodingPlanClick: () => void;
|
onAlibabaCodingPlanClick: () => void;
|
||||||
onCliproxyClick: () => void;
|
onCliproxyClick: () => void;
|
||||||
onCustomClick: () => 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 (
|
||||||
|
<Card className={cn('flex h-full flex-col border shadow-sm', className)}>
|
||||||
|
<CardHeader className="space-y-3 pb-3">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
{visual}
|
||||||
|
<Badge variant="secondary" className={badgeClassName}>
|
||||||
|
{badge}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<CardTitle className="text-base">{title}</CardTitle>
|
||||||
|
<CardDescription className="text-sm leading-6">{description}</CardDescription>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="mt-auto flex flex-1 flex-col gap-4 pt-0">
|
||||||
|
<div className="space-y-2 text-xs text-muted-foreground">
|
||||||
|
{highlights.map((item) => (
|
||||||
|
<div key={item.label} className="flex items-center gap-2">
|
||||||
|
{item.icon}
|
||||||
|
<span>{item.label}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<Button onClick={onAction} className={actionClassName}>
|
||||||
|
{actionLabel}
|
||||||
|
<ArrowRight className="ml-2 h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
{footer ? <div className="text-xs text-muted-foreground">{footer}</div> : null}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function OpenRouterQuickStart({
|
export function OpenRouterQuickStart({
|
||||||
|
hasProfiles,
|
||||||
|
profileCount,
|
||||||
onOpenRouterClick,
|
onOpenRouterClick,
|
||||||
onAlibabaCodingPlanClick,
|
onAlibabaCodingPlanClick,
|
||||||
onCliproxyClick,
|
onCliproxyClick,
|
||||||
@@ -34,185 +94,151 @@ export function OpenRouterQuickStart({
|
|||||||
}: OpenRouterQuickStartProps) {
|
}: OpenRouterQuickStartProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { modelCount, isLoading } = useOpenRouterReady();
|
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 (
|
return (
|
||||||
<div className="flex-1 flex items-center justify-center bg-muted/20 p-8">
|
<div className="flex h-full min-h-0 flex-col overflow-auto bg-muted/20 p-4 sm:p-6">
|
||||||
<div className="max-w-lg w-full space-y-6">
|
<div className="mx-auto flex w-full max-w-7xl flex-col gap-4">
|
||||||
{/* Main OpenRouter Card */}
|
<Card className="border-dashed bg-background/90 shadow-sm">
|
||||||
<Card className="border-accent/30 dark:border-accent/40 bg-gradient-to-br from-accent/5 to-background dark:from-accent/10">
|
<CardContent className="flex flex-col gap-4 p-5 lg:flex-row lg:items-center lg:justify-between">
|
||||||
<CardHeader className="pb-3">
|
<div className="space-y-2">
|
||||||
<div className="flex items-center gap-3 mb-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<div className="p-2 rounded-lg bg-accent/10 dark:bg-accent/20">
|
<Badge variant="secondary">{profileSummaryLabel}</Badge>
|
||||||
<img src="/icons/openrouter.svg" alt="OpenRouter" className="w-6 h-6" />
|
<Badge variant="outline">
|
||||||
|
{t('openrouterQuickStart.openrouterModelsBadge', { modelCountLabel })}
|
||||||
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<Badge
|
<div className="space-y-1">
|
||||||
variant="secondary"
|
<h2 className="text-xl font-semibold">{summaryTitle}</h2>
|
||||||
className="bg-accent/10 text-accent dark:bg-accent/20 dark:text-accent-foreground"
|
<p className="max-w-3xl text-sm leading-6 text-muted-foreground">
|
||||||
>
|
{summaryDescription}
|
||||||
{t('openrouterQuickStart.recommended')}
|
</p>
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
<CardTitle className="text-xl">{t('openrouterQuickStart.title')}</CardTitle>
|
|
||||||
<CardDescription className="text-base">
|
|
||||||
{t('openrouterQuickStart.description', {
|
|
||||||
modelCountLabel: isLoading ? '300+' : `${modelCount}+`,
|
|
||||||
})}
|
|
||||||
</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-accent" />
|
|
||||||
<span>{t('openrouterQuickStart.featureOneApi')}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2 text-muted-foreground">
|
|
||||||
<Sparkles className="w-4 h-4 text-accent" />
|
|
||||||
<span>{t('openrouterQuickStart.featureTierMapping')}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<Button variant="outline" onClick={onCustomClick} className="shrink-0">
|
||||||
<Button
|
{t('openrouterQuickStart.createCustomProfile')}
|
||||||
onClick={onOpenRouterClick}
|
|
||||||
className="w-full bg-accent hover:bg-accent/90 text-white"
|
|
||||||
size="lg"
|
|
||||||
>
|
|
||||||
{t('openrouterQuickStart.createOpenRouterProfile')}
|
|
||||||
<ArrowRight className="w-4 h-4 ml-2" />
|
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<p className="text-xs text-center text-muted-foreground">
|
|
||||||
{t('openrouterQuickStart.getApiKeyAt')}{' '}
|
|
||||||
<a
|
|
||||||
href="https://openrouter.ai/keys"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="text-accent hover:underline inline-flex items-center gap-1"
|
|
||||||
>
|
|
||||||
openrouter.ai/keys
|
|
||||||
<ExternalLink className="w-3 h-3" />
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Alibaba Coding Plan Card */}
|
<div className="grid gap-4 lg:grid-cols-2">
|
||||||
<Card className="border-orange-500/30 dark:border-orange-500/40 bg-gradient-to-br from-orange-500/5 to-background dark:from-orange-500/10">
|
<QuickStartCard
|
||||||
<CardHeader className="pb-3">
|
badge={t('openrouterQuickStart.recommended')}
|
||||||
<div className="flex items-center gap-3 mb-2">
|
title={t('openrouterQuickStart.title')}
|
||||||
<div className="p-2 rounded-lg bg-orange-500/10 dark:bg-orange-500/20">
|
description={t('openrouterQuickStart.description', { modelCountLabel })}
|
||||||
|
visual={
|
||||||
|
<div className="rounded-lg bg-accent/10 p-2">
|
||||||
|
<img src="/icons/openrouter.svg" alt="OpenRouter" className="h-5 w-5" />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
highlights={[
|
||||||
|
{
|
||||||
|
icon: <Zap className="h-3.5 w-3.5 text-accent" />,
|
||||||
|
label: t('openrouterQuickStart.featureOneApi'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <Sparkles className="h-3.5 w-3.5 text-accent" />,
|
||||||
|
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')}{' '}
|
||||||
|
<a
|
||||||
|
href="https://openrouter.ai/keys"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="inline-flex items-center gap-1 text-accent hover:underline"
|
||||||
|
>
|
||||||
|
openrouter.ai/keys
|
||||||
|
<ExternalLink className="h-3 w-3" />
|
||||||
|
</a>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<QuickStartCard
|
||||||
|
badge={t('openrouterQuickStart.runtimeProviderBadge')}
|
||||||
|
badgeClassName="bg-emerald-500/10 text-emerald-700 dark:bg-emerald-500/20 dark:text-emerald-200"
|
||||||
|
title={t('openrouterQuickStart.runtimeProviderTitle')}
|
||||||
|
description={t('openrouterQuickStart.runtimeProviderDescription')}
|
||||||
|
visual={
|
||||||
|
<div className="rounded-lg bg-emerald-500/10 p-2">
|
||||||
|
<SlidersHorizontal className="h-5 w-5 text-emerald-700 dark:text-emerald-300" />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
highlights={[
|
||||||
|
{
|
||||||
|
icon: <SlidersHorizontal className="h-3.5 w-3.5 text-emerald-600" />,
|
||||||
|
label: t('openrouterQuickStart.runtimeProviderFeatureConnectors'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <KeyRound className="h-3.5 w-3.5 text-emerald-600" />,
|
||||||
|
label: t('openrouterQuickStart.runtimeProviderFeatureSecrets'),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
actionLabel={t('openrouterQuickStart.runtimeProviderTitle')}
|
||||||
|
actionClassName="w-full bg-emerald-600 text-white hover:bg-emerald-600/90"
|
||||||
|
onAction={onCliproxyClick}
|
||||||
|
footer={<span>{t('openrouterQuickStart.runtimeProviderFooter')}</span>}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<QuickStartCard
|
||||||
|
badge={t('alibabaCodingPlanQuickStart.recommended')}
|
||||||
|
badgeClassName="bg-orange-500/10 text-orange-700 dark:bg-orange-500/20 dark:text-orange-200"
|
||||||
|
className="lg:col-span-2"
|
||||||
|
title={t('alibabaCodingPlanQuickStart.title')}
|
||||||
|
description={t('alibabaCodingPlanQuickStart.description')}
|
||||||
|
visual={
|
||||||
|
<div className="rounded-lg bg-orange-500/10 p-2">
|
||||||
<img
|
<img
|
||||||
src="/assets/providers/alibabacloud-color.svg"
|
src="/assets/providers/alibabacloud-color.svg"
|
||||||
alt="Alibaba Coding Plan"
|
alt="Alibaba Coding Plan"
|
||||||
className="w-6 h-6"
|
className="h-5 w-5"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Badge
|
}
|
||||||
variant="secondary"
|
highlights={[
|
||||||
className="bg-orange-500/10 text-orange-700 dark:bg-orange-500/20 dark:text-orange-200"
|
{
|
||||||
>
|
icon: <CloudCog className="h-3.5 w-3.5 text-orange-600" />,
|
||||||
{t('alibabaCodingPlanQuickStart.recommended')}
|
label: t('alibabaCodingPlanQuickStart.featureEndpoint'),
|
||||||
</Badge>
|
},
|
||||||
</div>
|
{
|
||||||
<CardTitle className="text-xl">{t('alibabaCodingPlanQuickStart.title')}</CardTitle>
|
icon: <KeyRound className="h-3.5 w-3.5 text-orange-600" />,
|
||||||
<CardDescription className="text-base">
|
label: t('alibabaCodingPlanQuickStart.featureKeyFormat'),
|
||||||
{t('alibabaCodingPlanQuickStart.description')}
|
},
|
||||||
</CardDescription>
|
]}
|
||||||
</CardHeader>
|
actionLabel={t('alibabaCodingPlanQuickStart.createAlibabaProfile')}
|
||||||
<CardContent className="space-y-4">
|
actionClassName="w-full bg-orange-600 text-white hover:bg-orange-600/90"
|
||||||
<div className="grid grid-cols-2 gap-3 text-sm">
|
onAction={onAlibabaCodingPlanClick}
|
||||||
<div className="flex items-center gap-2 text-muted-foreground">
|
footer={
|
||||||
<CloudCog className="w-4 h-4 text-orange-600" />
|
<>
|
||||||
<span>{t('alibabaCodingPlanQuickStart.featureEndpoint')}</span>
|
{t('alibabaCodingPlanQuickStart.readGuideAt')}{' '}
|
||||||
</div>
|
<a
|
||||||
<div className="flex items-center gap-2 text-muted-foreground">
|
href="https://www.alibabacloud.com/help/en/model-studio/coding-plan"
|
||||||
<KeyRound className="w-4 h-4 text-orange-600" />
|
target="_blank"
|
||||||
<span>{t('alibabaCodingPlanQuickStart.featureKeyFormat')}</span>
|
rel="noopener noreferrer"
|
||||||
</div>
|
className="inline-flex items-center gap-1 text-orange-700 hover:underline dark:text-orange-400"
|
||||||
</div>
|
>
|
||||||
|
Alibaba Cloud Model Studio
|
||||||
<Button
|
<ExternalLink className="h-3 w-3" />
|
||||||
onClick={onAlibabaCodingPlanClick}
|
</a>
|
||||||
className="w-full bg-orange-600 hover:bg-orange-600/90 text-white"
|
</>
|
||||||
size="lg"
|
}
|
||||||
>
|
/>
|
||||||
{t('alibabaCodingPlanQuickStart.createAlibabaProfile')}
|
|
||||||
<ArrowRight className="w-4 h-4 ml-2" />
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<p className="text-xs text-center text-muted-foreground">
|
|
||||||
{t('alibabaCodingPlanQuickStart.readGuideAt')}{' '}
|
|
||||||
<a
|
|
||||||
href="https://www.alibabacloud.com/help/en/model-studio/coding-plan"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="text-orange-700 dark:text-orange-400 hover:underline inline-flex items-center gap-1"
|
|
||||||
>
|
|
||||||
Alibaba Cloud Model Studio
|
|
||||||
<ExternalLink className="w-3 h-3" />
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card className="border-emerald-500/30 dark:border-emerald-500/40 bg-gradient-to-br from-emerald-500/5 to-background dark:from-emerald-500/10">
|
|
||||||
<CardHeader className="pb-3">
|
|
||||||
<div className="flex items-center gap-3 mb-2">
|
|
||||||
<div className="p-2 rounded-lg bg-emerald-500/10 dark:bg-emerald-500/20">
|
|
||||||
<SlidersHorizontal className="w-6 h-6 text-emerald-700 dark:text-emerald-300" />
|
|
||||||
</div>
|
|
||||||
<Badge
|
|
||||||
variant="secondary"
|
|
||||||
className="bg-emerald-500/10 text-emerald-700 dark:bg-emerald-500/20 dark:text-emerald-200"
|
|
||||||
>
|
|
||||||
Configure in AI Providers
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
<CardTitle className="text-xl">Manage CLIProxy AI providers</CardTitle>
|
|
||||||
<CardDescription className="text-base">
|
|
||||||
Configure Gemini, Codex, Claude, Vertex, and OpenAI-compatible connectors directly in
|
|
||||||
the dedicated CLIProxy AI Providers page.
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-4">
|
|
||||||
<div className="grid grid-cols-2 gap-3 text-sm">
|
|
||||||
<div className="flex items-center gap-2 text-muted-foreground">
|
|
||||||
<SlidersHorizontal className="w-4 h-4 text-emerald-600" />
|
|
||||||
<span>Dedicated /cliproxy/ai-providers workspace</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2 text-muted-foreground">
|
|
||||||
<KeyRound className="w-4 h-4 text-emerald-600" />
|
|
||||||
<span>Manage provider secrets outside API Profiles</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
onClick={onCliproxyClick}
|
|
||||||
className="w-full bg-emerald-600 hover:bg-emerald-600/90 text-white"
|
|
||||||
size="lg"
|
|
||||||
>
|
|
||||||
Open AI Providers
|
|
||||||
<ArrowRight className="w-4 h-4 ml-2" />
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<p className="text-xs text-center text-muted-foreground">
|
|
||||||
Keep runtime provider configuration in CLIProxy, then create API Profiles only when
|
|
||||||
you need standalone Anthropic-compatible endpoints.
|
|
||||||
</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Divider */}
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<Separator className="flex-1" />
|
|
||||||
<span className="text-xs text-muted-foreground">{t('openrouterQuickStart.or')}</span>
|
|
||||||
<Separator className="flex-1" />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Custom Option */}
|
|
||||||
<Button variant="outline" onClick={onCustomClick} className="w-full">
|
|
||||||
{t('openrouterQuickStart.createCustomProfile')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -241,6 +241,25 @@ const resources = {
|
|||||||
'Access {{modelCountLabel}} models from OpenAI, Anthropic, Google, Meta and more - all through one API.',
|
'Access {{modelCountLabel}} models from OpenAI, Anthropic, Google, Meta and more - all through one API.',
|
||||||
featureOneApi: 'One API, all providers',
|
featureOneApi: 'One API, all providers',
|
||||||
featureTierMapping: 'Model tier mapping',
|
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',
|
createOpenRouterProfile: 'Create OpenRouter Profile',
|
||||||
getApiKeyAt: 'Get your API key at',
|
getApiKeyAt: 'Get your API key at',
|
||||||
or: 'or',
|
or: 'or',
|
||||||
@@ -1420,6 +1439,24 @@ const resources = {
|
|||||||
'通过一个 API 即可访问来自 OpenAI、Anthropic、Google、Meta 等的 {{modelCountLabel}} 个模型。',
|
'通过一个 API 即可访问来自 OpenAI、Anthropic、Google、Meta 等的 {{modelCountLabel}} 个模型。',
|
||||||
featureOneApi: '一个 API,接入全部提供商',
|
featureOneApi: '一个 API,接入全部提供商',
|
||||||
featureTierMapping: '模型档位映射',
|
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 配置',
|
createOpenRouterProfile: '创建 OpenRouter 配置',
|
||||||
getApiKeyAt: '在此获取 API Key:',
|
getApiKeyAt: '在此获取 API Key:',
|
||||||
or: '或',
|
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.',
|
'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',
|
featureOneApi: 'Một API cho mọi nhà cung cấp',
|
||||||
featureTierMapping: 'Ánh xạ tầng mô hình',
|
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',
|
createOpenRouterProfile: 'Tạo hồ sơ OpenRouter',
|
||||||
getApiKeyAt: 'Nhận khóa API của bạn tại',
|
getApiKeyAt: 'Nhận khóa API của bạn tại',
|
||||||
or: 'hoặc',
|
or: 'hoặc',
|
||||||
@@ -3767,6 +3823,26 @@ const resources = {
|
|||||||
'OpenAI、Anthropic、Google、Meta などの {{modelCountLabel}} モデルを、1 つの API で利用できます。',
|
'OpenAI、Anthropic、Google、Meta などの {{modelCountLabel}} モデルを、1 つの API で利用できます。',
|
||||||
featureOneApi: '1 つの API で全プロバイダー',
|
featureOneApi: '1 つの API で全プロバイダー',
|
||||||
featureTierMapping: 'モデルティアマッピング',
|
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 プロファイルを作成',
|
createOpenRouterProfile: 'OpenRouter プロファイルを作成',
|
||||||
getApiKeyAt: 'API キー取得:',
|
getApiKeyAt: 'API キー取得:',
|
||||||
or: 'または',
|
or: 'または',
|
||||||
|
|||||||
@@ -206,9 +206,9 @@ export function ApiPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full flex flex-col">
|
<div className="flex h-full flex-col overflow-hidden">
|
||||||
<OpenRouterBanner onCreateClick={() => setCreateDialogOpen(true)} />
|
<OpenRouterBanner onCreateClick={() => setCreateDialogOpen(true)} />
|
||||||
<div className="flex-1 flex min-h-0">
|
<div className="flex-1 flex min-h-0 overflow-hidden">
|
||||||
<div className="w-80 border-r flex flex-col bg-muted/30">
|
<div className="w-80 border-r flex flex-col bg-muted/30">
|
||||||
<div className="p-4 border-b bg-background">
|
<div className="p-4 border-b bg-background">
|
||||||
<div className="flex items-center justify-between mb-3">
|
<div className="flex items-center justify-between mb-3">
|
||||||
@@ -262,7 +262,7 @@ export function ApiPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ScrollArea className="flex-1">
|
<ScrollArea className="flex-1 min-h-0">
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="p-4 text-sm text-muted-foreground">
|
<div className="p-4 text-sm text-muted-foreground">
|
||||||
{t('apiProfiles.loadingProfiles')}
|
{t('apiProfiles.loadingProfiles')}
|
||||||
@@ -354,7 +354,7 @@ export function ApiPage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 flex flex-col min-w-0">
|
<div className="flex-1 flex flex-col min-w-0 overflow-hidden">
|
||||||
{selectedProfileData ? (
|
{selectedProfileData ? (
|
||||||
<>
|
<>
|
||||||
<div className="px-4 py-2 border-b bg-background flex items-center justify-end gap-2">
|
<div className="px-4 py-2 border-b bg-background flex items-center justify-end gap-2">
|
||||||
@@ -387,6 +387,8 @@ export function ApiPage() {
|
|||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<OpenRouterQuickStart
|
<OpenRouterQuickStart
|
||||||
|
hasProfiles={profiles.length > 0}
|
||||||
|
profileCount={profiles.length}
|
||||||
onCliproxyClick={() => {
|
onCliproxyClick={() => {
|
||||||
navigate('/cliproxy/ai-providers');
|
navigate('/cliproxy/ai-providers');
|
||||||
}}
|
}}
|
||||||
|
|||||||
Reference in New Issue
Block a user