mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-04 06:19:31 +00:00
feat(i18n): Added support for Chinese language pack
This commit is contained in:
@@ -12,6 +12,7 @@ import { MaskedInput } from '@/components/ui/masked-input';
|
||||
import { Plus } from 'lucide-react';
|
||||
import { isSensitiveKey } from './utils';
|
||||
import type { Settings } from './types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface EnvEditorSectionProps {
|
||||
currentSettings: Settings | undefined;
|
||||
@@ -32,6 +33,7 @@ export function EnvEditorSection({
|
||||
onEnvValueChange,
|
||||
onAddEnvVar,
|
||||
}: EnvEditorSectionProps) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<>
|
||||
{/* Scrollable Environment Variables List */}
|
||||
@@ -45,7 +47,7 @@ export function EnvEditorSection({
|
||||
{key}
|
||||
{isSensitiveKey(key) && (
|
||||
<Badge variant="secondary" className="text-[10px] px-1 py-0 h-4">
|
||||
sensitive
|
||||
{t('envEditor.sensitive')}
|
||||
</Badge>
|
||||
)}
|
||||
</Label>
|
||||
@@ -67,10 +69,8 @@ export function EnvEditorSection({
|
||||
</>
|
||||
) : (
|
||||
<div className="py-8 text-center text-muted-foreground bg-muted/30 rounded-lg border border-dashed text-sm">
|
||||
<p>No environment variables configured.</p>
|
||||
<p className="text-xs mt-1 opacity-70">
|
||||
Add variables using the input below or edit the JSON directly.
|
||||
</p>
|
||||
<p>{t('envEditor.none')}</p>
|
||||
<p className="text-xs mt-1 opacity-70">{t('envEditor.noneHint')}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -79,18 +79,18 @@ export function EnvEditorSection({
|
||||
{/* Fixed Add Input at Bottom */}
|
||||
<div className="p-4 border-t bg-background shrink-0">
|
||||
<Label className="text-xs font-medium text-muted-foreground">
|
||||
Add Environment Variable
|
||||
{t('envEditor.addVariable')}
|
||||
</Label>
|
||||
<div className="flex gap-2 mt-2">
|
||||
<Input
|
||||
placeholder="VARIABLE_NAME"
|
||||
placeholder={t('envEditor.keyPlaceholder')}
|
||||
value={newEnvKey}
|
||||
onChange={(e) => onNewEnvKeyChange(e.target.value.toUpperCase())}
|
||||
className="font-mono text-sm h-8 w-2/5"
|
||||
onKeyDown={(e) => e.key === 'Enter' && newEnvKey.trim() && onAddEnvVar()}
|
||||
/>
|
||||
<Input
|
||||
placeholder="value"
|
||||
placeholder={t('envEditor.valuePlaceholder')}
|
||||
value={newEnvValue}
|
||||
onChange={(e) => onNewEnvValueChange(e.target.value)}
|
||||
className="font-mono text-sm h-8 flex-1"
|
||||
|
||||
@@ -19,6 +19,7 @@ import { ChevronRight, Settings2, Plus } from 'lucide-react';
|
||||
import { isOpenRouterProfile, extractTierMapping, applyTierMapping } from './utils';
|
||||
import { toast } from 'sonner';
|
||||
import { cn } from '@/lib/utils';
|
||||
import i18n from '@/lib/i18n';
|
||||
import type { Settings, SettingsResponse } from './types';
|
||||
import type { CliTarget } from '@/lib/api-client';
|
||||
|
||||
@@ -81,7 +82,7 @@ export function FriendlyUISection({
|
||||
onEnvValueChange('ANTHROPIC_DEFAULT_HAIKU_MODEL', modelId);
|
||||
}
|
||||
// Show feedback toast
|
||||
toast.success('Applied model to all tiers', { duration: 2000 });
|
||||
toast.success(i18n.t('commonToast.appliedModelAllTiers'), { duration: 2000 });
|
||||
};
|
||||
|
||||
// Handle tier mapping change
|
||||
|
||||
@@ -16,6 +16,7 @@ import { FriendlyUISection } from './friendly-ui-section';
|
||||
import { RawEditorSection } from './raw-editor-section';
|
||||
import type { ProfileEditorProps, Settings, SettingsResponse } from './types';
|
||||
import { api, type CliTarget } from '@/lib/api-client';
|
||||
import i18n from '@/lib/i18n';
|
||||
|
||||
export function ProfileEditor({
|
||||
profileName,
|
||||
@@ -143,7 +144,7 @@ export function ProfileEditor({
|
||||
queryClient.invalidateQueries({ queryKey: ['profiles'] });
|
||||
setLocalEdits({});
|
||||
setRawJsonEdits(null);
|
||||
toast.success('Settings saved');
|
||||
toast.success(i18n.t('commonToast.settingsSaved'));
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
if (error.message === 'CONFLICT') setConflictDialog(true);
|
||||
@@ -158,12 +159,12 @@ export function ProfileEditor({
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['profiles'] });
|
||||
toast.success('Default target updated');
|
||||
toast.success(i18n.t('commonToast.defaultTargetUpdated'));
|
||||
},
|
||||
onError: (error: Error, target: CliTarget) => {
|
||||
const targetLabel = target === 'droid' ? 'Factory Droid' : 'Claude Code';
|
||||
const suffix = error.message.trim() ? `: ${error.message}` : '';
|
||||
toast.error(`Failed to update default target to ${targetLabel}${suffix}`);
|
||||
toast.error(i18n.t('commonToast.failedUpdateDefaultTarget', { target: targetLabel, suffix }));
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* Displays profile information and usage commands
|
||||
*/
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { CopyButton } from '@/components/ui/copy-button';
|
||||
@@ -17,6 +18,7 @@ interface InfoSectionProps {
|
||||
}
|
||||
|
||||
export function InfoSection({ profileName, target, data }: InfoSectionProps) {
|
||||
const { t } = useTranslation();
|
||||
const isDroidTarget = target === 'droid';
|
||||
|
||||
return (
|
||||
@@ -26,17 +28,21 @@ export function InfoSection({ profileName, target, data }: InfoSectionProps) {
|
||||
<div>
|
||||
<h3 className="text-sm font-medium flex items-center gap-2 mb-3">
|
||||
<Info className="w-4 h-4" />
|
||||
Profile Information
|
||||
{t('profileEditor.profileInfo')}
|
||||
</h3>
|
||||
<div className="space-y-3 bg-card rounded-lg border p-4 shadow-sm">
|
||||
{data && (
|
||||
<>
|
||||
<div className="grid grid-cols-[100px_1fr] gap-2 text-sm items-center">
|
||||
<span className="font-medium text-muted-foreground">Profile Name</span>
|
||||
<span className="font-medium text-muted-foreground">
|
||||
{t('profileEditor.profileName')}
|
||||
</span>
|
||||
<span className="font-mono">{data.profile}</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-[100px_1fr] gap-2 text-sm items-center">
|
||||
<span className="font-medium text-muted-foreground">File Path</span>
|
||||
<span className="font-medium text-muted-foreground">
|
||||
{t('profileEditor.filePath')}
|
||||
</span>
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<code className="bg-muted px-1.5 py-0.5 rounded text-xs break-all">
|
||||
{data.path}
|
||||
@@ -45,11 +51,15 @@ export function InfoSection({ profileName, target, data }: InfoSectionProps) {
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-[100px_1fr] gap-2 text-sm items-center">
|
||||
<span className="font-medium text-muted-foreground">Last Modified</span>
|
||||
<span className="font-medium text-muted-foreground">
|
||||
{t('profileEditor.lastModified')}
|
||||
</span>
|
||||
<span className="text-xs">{new Date(data.mtime).toLocaleString()}</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-[100px_1fr] gap-2 text-sm items-center">
|
||||
<span className="font-medium text-muted-foreground">Default Target</span>
|
||||
<span className="font-medium text-muted-foreground">
|
||||
{t('profileEditor.defaultTarget')}
|
||||
</span>
|
||||
<span className="font-mono">{target}</span>
|
||||
</div>
|
||||
</>
|
||||
@@ -59,10 +69,12 @@ export function InfoSection({ profileName, target, data }: InfoSectionProps) {
|
||||
|
||||
{/* Usage */}
|
||||
<div>
|
||||
<h3 className="text-sm font-medium mb-3">Quick Usage</h3>
|
||||
<h3 className="text-sm font-medium mb-3">{t('profileEditor.quickUsage')}</h3>
|
||||
<div className="space-y-3 bg-card rounded-lg border p-4 shadow-sm">
|
||||
<div>
|
||||
<Label className="text-xs text-muted-foreground">Run with profile</Label>
|
||||
<Label className="text-xs text-muted-foreground">
|
||||
{t('profileEditor.runWithProfile')}
|
||||
</Label>
|
||||
<div className="mt-1 flex gap-2">
|
||||
<code className="flex-1 px-2 py-1.5 bg-muted rounded text-xs font-mono truncate">
|
||||
ccs {profileName} "prompt"
|
||||
@@ -72,7 +84,9 @@ export function InfoSection({ profileName, target, data }: InfoSectionProps) {
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-xs text-muted-foreground">
|
||||
{isDroidTarget ? 'Droid alias (explicit)' : 'Run on Droid'}
|
||||
{isDroidTarget
|
||||
? t('profileEditor.droidAliasExplicit')
|
||||
: t('profileEditor.runOnDroid')}
|
||||
</Label>
|
||||
<div className="mt-1 flex gap-2">
|
||||
<code className="flex-1 px-2 py-1.5 bg-muted rounded text-xs font-mono truncate">
|
||||
@@ -93,7 +107,9 @@ export function InfoSection({ profileName, target, data }: InfoSectionProps) {
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-xs text-muted-foreground">
|
||||
{isDroidTarget ? 'Override to Claude' : 'Override to Claude (explicit)'}
|
||||
{isDroidTarget
|
||||
? t('profileEditor.overrideToClaude')
|
||||
: t('profileEditor.overrideToClaudeExplicit')}
|
||||
</Label>
|
||||
<div className="mt-1 flex gap-2">
|
||||
<code className="flex-1 px-2 py-1.5 bg-muted rounded text-xs font-mono truncate">
|
||||
@@ -107,7 +123,9 @@ export function InfoSection({ profileName, target, data }: InfoSectionProps) {
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-xs text-muted-foreground">Set as default</Label>
|
||||
<Label className="text-xs text-muted-foreground">
|
||||
{t('profileEditor.setAsDefault')}
|
||||
</Label>
|
||||
<div className="mt-1 flex gap-2">
|
||||
<code className="flex-1 px-2 py-1.5 bg-muted rounded text-xs font-mono truncate">
|
||||
ccs default {profileName}
|
||||
|
||||
@@ -12,6 +12,7 @@ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/component
|
||||
import { useOpenRouterCatalog } from '@/hooks/use-openrouter-models';
|
||||
import { suggestTierMappings } from '@/lib/openrouter-utils';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export interface TierMapping {
|
||||
opus?: string;
|
||||
@@ -32,6 +33,7 @@ export function ModelTierMapping({
|
||||
onChange,
|
||||
className,
|
||||
}: ModelTierMappingProps) {
|
||||
const { t } = useTranslation();
|
||||
const { models } = useOpenRouterCatalog();
|
||||
|
||||
const suggestions = useMemo(() => {
|
||||
@@ -53,18 +55,18 @@ export function ModelTierMapping({
|
||||
<Collapsible className={cn('group', className)}>
|
||||
<CollapsibleTrigger className="flex items-center gap-2 text-sm font-medium hover:underline">
|
||||
<ChevronRight className="h-4 w-4 transition-transform group-data-[state=open]:rotate-90" />
|
||||
Model Tier Mapping
|
||||
<span className="text-muted-foreground font-normal">(Advanced)</span>
|
||||
{t('modelTierMapping.title')}
|
||||
<span className="text-muted-foreground font-normal">
|
||||
({t('modelTierMapping.advanced')})
|
||||
</span>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="space-y-3 pt-3">
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Configure different models for Claude Code's opus/sonnet/haiku tiers.
|
||||
</p>
|
||||
<p className="text-muted-foreground text-sm">{t('modelTierMapping.description')}</p>
|
||||
|
||||
{hasSuggestions && (
|
||||
<Button type="button" variant="outline" size="sm" onClick={handleAutoSuggest}>
|
||||
<Wand2 className="mr-1 h-4 w-4" />
|
||||
Auto-suggest based on {selectedModel?.split('/')[0]}
|
||||
{t('modelTierMapping.autoSuggest', { provider: selectedModel?.split('/')[0] })}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@@ -77,7 +79,7 @@ export function ModelTierMapping({
|
||||
id="tier-opus"
|
||||
value={value.opus ?? ''}
|
||||
onChange={(e) => updateTier('opus', e.target.value)}
|
||||
placeholder="e.g., anthropic/claude-opus-4"
|
||||
placeholder={t('modelTierMapping.opusPlaceholder')}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-[80px_1fr] items-center gap-2">
|
||||
@@ -88,7 +90,7 @@ export function ModelTierMapping({
|
||||
id="tier-sonnet"
|
||||
value={value.sonnet ?? ''}
|
||||
onChange={(e) => updateTier('sonnet', e.target.value)}
|
||||
placeholder="e.g., anthropic/claude-sonnet-4"
|
||||
placeholder={t('modelTierMapping.sonnetPlaceholder')}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-[80px_1fr] items-center gap-2">
|
||||
@@ -99,15 +101,12 @@ export function ModelTierMapping({
|
||||
id="tier-haiku"
|
||||
value={value.haiku ?? ''}
|
||||
onChange={(e) => updateTier('haiku', e.target.value)}
|
||||
placeholder="e.g., anthropic/claude-3.5-haiku"
|
||||
placeholder={t('modelTierMapping.haikuPlaceholder')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-muted-foreground text-xs">
|
||||
These set ANTHROPIC_DEFAULT_OPUS_MODEL, ANTHROPIC_DEFAULT_SONNET_MODEL,
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL.
|
||||
</p>
|
||||
<p className="text-muted-foreground text-xs">{t('modelTierMapping.footer')}</p>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
);
|
||||
|
||||
@@ -9,6 +9,7 @@ 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';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface OpenRouterQuickStartProps {
|
||||
onOpenRouterClick: () => void;
|
||||
@@ -19,6 +20,7 @@ export function OpenRouterQuickStart({
|
||||
onOpenRouterClick,
|
||||
onCustomClick,
|
||||
}: OpenRouterQuickStartProps) {
|
||||
const { t } = useTranslation();
|
||||
const { modelCount, isLoading } = useOpenRouterReady();
|
||||
|
||||
return (
|
||||
@@ -35,13 +37,14 @@ export function OpenRouterQuickStart({
|
||||
variant="secondary"
|
||||
className="bg-accent/10 text-accent dark:bg-accent/20 dark:text-accent-foreground"
|
||||
>
|
||||
Recommended
|
||||
{t('openrouterQuickStart.recommended')}
|
||||
</Badge>
|
||||
</div>
|
||||
<CardTitle className="text-xl">Start with OpenRouter</CardTitle>
|
||||
<CardTitle className="text-xl">{t('openrouterQuickStart.title')}</CardTitle>
|
||||
<CardDescription className="text-base">
|
||||
Access {isLoading ? '300+' : `${modelCount}+`} models from OpenAI, Anthropic, Google,
|
||||
Meta and more - all through one API.
|
||||
{t('openrouterQuickStart.description', {
|
||||
modelCountLabel: isLoading ? '300+' : `${modelCount}+`,
|
||||
})}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
@@ -49,11 +52,11 @@ export function OpenRouterQuickStart({
|
||||
<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>One API, all providers</span>
|
||||
<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>Model tier mapping</span>
|
||||
<span>{t('openrouterQuickStart.featureTierMapping')}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -62,12 +65,12 @@ export function OpenRouterQuickStart({
|
||||
className="w-full bg-accent hover:bg-accent/90 text-white"
|
||||
size="lg"
|
||||
>
|
||||
Create OpenRouter Profile
|
||||
{t('openrouterQuickStart.createOpenRouterProfile')}
|
||||
<ArrowRight className="w-4 h-4 ml-2" />
|
||||
</Button>
|
||||
|
||||
<p className="text-xs text-center text-muted-foreground">
|
||||
Get your API key at{' '}
|
||||
{t('openrouterQuickStart.getApiKeyAt')}{' '}
|
||||
<a
|
||||
href="https://openrouter.ai/keys"
|
||||
target="_blank"
|
||||
@@ -84,13 +87,13 @@ export function OpenRouterQuickStart({
|
||||
{/* Divider */}
|
||||
<div className="flex items-center gap-4">
|
||||
<Separator className="flex-1" />
|
||||
<span className="text-xs text-muted-foreground">or</span>
|
||||
<span className="text-xs text-muted-foreground">{t('openrouterQuickStart.or')}</span>
|
||||
<Separator className="flex-1" />
|
||||
</div>
|
||||
|
||||
{/* Custom Option */}
|
||||
<Button variant="outline" onClick={onCustomClick} className="w-full">
|
||||
Create Custom API Profile
|
||||
{t('openrouterQuickStart.createCustomProfile')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -32,6 +32,7 @@ import { Badge } from '@/components/ui/badge';
|
||||
import { useCreateProfile } from '@/hooks/use-profiles';
|
||||
import { useOpenRouterCatalog } from '@/hooks/use-openrouter-models';
|
||||
import { Loader2, Plus, AlertTriangle, Info, Eye, EyeOff, Settings2, Sparkles } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { toast } from 'sonner';
|
||||
import { cn } from '@/lib/utils';
|
||||
import {
|
||||
@@ -49,6 +50,7 @@ import {
|
||||
} from '@/lib/openrouter-utils';
|
||||
import type { CategorizedModel } from '@/lib/openrouter-types';
|
||||
import type { CliTarget } from '@/lib/api-client';
|
||||
import i18n from '@/lib/i18n';
|
||||
|
||||
const schema = z.object({
|
||||
name: z
|
||||
@@ -104,6 +106,7 @@ export function ProfileCreateDialog({
|
||||
onSuccess,
|
||||
initialMode = 'openrouter',
|
||||
}: ProfileCreateDialogProps) {
|
||||
const { t } = useTranslation();
|
||||
const createMutation = useCreateProfile();
|
||||
const [activeTab, setActiveTab] = useState('basic');
|
||||
const [urlWarning, setUrlWarning] = useState<string | null>(null);
|
||||
@@ -242,7 +245,7 @@ export function ProfileCreateDialog({
|
||||
const onSubmit = async (data: FormData) => {
|
||||
// Validate API key - required unless preset has requiresApiKey: false
|
||||
if (currentPreset?.requiresApiKey !== false && !data.apiKey) {
|
||||
toast.error('API key is required');
|
||||
toast.error(i18n.t('commonToast.apiKeyRequired'));
|
||||
return;
|
||||
}
|
||||
// Use user-provided baseUrl (allows customization of preset URLs)
|
||||
@@ -290,7 +293,9 @@ export function ProfileCreateDialog({
|
||||
<div className="px-6 py-3 border-b bg-muted/30 space-y-2">
|
||||
{/* Main Options: OpenRouter + Custom */}
|
||||
<div>
|
||||
<Label className="text-xs text-muted-foreground mb-1.5 block">Provider</Label>
|
||||
<Label className="text-xs text-muted-foreground mb-1.5 block">
|
||||
{t('profileEditor.provider')}
|
||||
</Label>
|
||||
<div className="flex gap-2">
|
||||
{RECOMMENDED_PRESETS.map((preset) => (
|
||||
<CompactPresetCard
|
||||
@@ -312,7 +317,7 @@ export function ProfileCreateDialog({
|
||||
)}
|
||||
>
|
||||
<Settings2 className="w-4 h-4" />
|
||||
<span>Custom</span>
|
||||
<span>{t('profileEditor.custom')}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -16,6 +16,7 @@ import { Label } from '@/components/ui/label';
|
||||
import { useCreateProfile, useUpdateProfile } from '@/hooks/use-profiles';
|
||||
import type { Profile } from '@/lib/api-client';
|
||||
import { ChevronDown, ChevronRight } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const DEFAULT_MODEL = 'claude-sonnet-4-5-20250929';
|
||||
|
||||
@@ -41,6 +42,7 @@ interface ProfileDialogProps {
|
||||
}
|
||||
|
||||
export function ProfileDialog({ open, onClose, profile }: ProfileDialogProps) {
|
||||
const { t } = useTranslation();
|
||||
const createMutation = useCreateProfile();
|
||||
const updateMutation = useUpdateProfile();
|
||||
const [showModelMapping, setShowModelMapping] = useState(false);
|
||||
@@ -115,34 +117,45 @@ export function ProfileDialog({ open, onClose, profile }: ProfileDialogProps) {
|
||||
<Dialog open={open} onOpenChange={onClose}>
|
||||
<DialogContent className="max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{profile ? 'Edit Profile' : 'Create API Profile'}</DialogTitle>
|
||||
<DialogTitle>
|
||||
{profile ? t('profileDialog.editTitle') : t('profileDialog.createTitle')}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="name">Name</Label>
|
||||
<Input id="name" {...register('name')} placeholder="my-api" disabled={!!profile} />
|
||||
<Label htmlFor="name">{t('profileDialog.name')}</Label>
|
||||
<Input
|
||||
id="name"
|
||||
{...register('name')}
|
||||
placeholder={t('profileDialog.namePlaceholder')}
|
||||
disabled={!!profile}
|
||||
/>
|
||||
{errors.name && <span className="text-xs text-red-500">{errors.name.message}</span>}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="baseUrl">Base URL</Label>
|
||||
<Input id="baseUrl" {...register('baseUrl')} placeholder="https://api.example.com" />
|
||||
<Label htmlFor="baseUrl">{t('profileDialog.baseUrl')}</Label>
|
||||
<Input
|
||||
id="baseUrl"
|
||||
{...register('baseUrl')}
|
||||
placeholder={t('profileDialog.baseUrlPlaceholder')}
|
||||
/>
|
||||
{errors.baseUrl && (
|
||||
<span className="text-xs text-red-500">{errors.baseUrl.message}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="apiKey">API Key</Label>
|
||||
<Label htmlFor="apiKey">{t('profileDialog.apiKey')}</Label>
|
||||
<Input id="apiKey" type="password" {...register('apiKey')} />
|
||||
{errors.apiKey && <span className="text-xs text-red-500">{errors.apiKey.message}</span>}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="model">Default Model (ANTHROPIC_MODEL)</Label>
|
||||
<Label htmlFor="model">{t('profileDialog.defaultModel')}</Label>
|
||||
<Input id="model" {...register('model')} placeholder={DEFAULT_MODEL} />
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Leave blank to use: {DEFAULT_MODEL}
|
||||
{t('profileDialog.defaultModelHint', { model: DEFAULT_MODEL })}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -153,7 +166,7 @@ export function ProfileDialog({ open, onClose, profile }: ProfileDialogProps) {
|
||||
className="w-full flex items-center justify-between p-3 text-sm font-medium hover:bg-muted/50 transition-colors"
|
||||
onClick={() => setShowModelMapping(!showModelMapping)}
|
||||
>
|
||||
<span>Model Mapping (Opus/Sonnet/Haiku)</span>
|
||||
<span>{t('profileDialog.modelMappingTitle')}</span>
|
||||
{showModelMapping ? (
|
||||
<ChevronDown className="w-4 h-4" />
|
||||
) : (
|
||||
@@ -164,13 +177,12 @@ export function ProfileDialog({ open, onClose, profile }: ProfileDialogProps) {
|
||||
{showModelMapping && (
|
||||
<div className="p-3 pt-0 space-y-3 border-t">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Configure different model IDs for each tier. Useful for API proxies that route
|
||||
different model types to different backends.
|
||||
{t('profileDialog.modelMappingDesc')}
|
||||
</p>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="opusModel" className="text-xs">
|
||||
Opus Model (ANTHROPIC_DEFAULT_OPUS_MODEL)
|
||||
{t('profileDialog.opusModel')}
|
||||
</Label>
|
||||
<Input
|
||||
id="opusModel"
|
||||
@@ -182,7 +194,7 @@ export function ProfileDialog({ open, onClose, profile }: ProfileDialogProps) {
|
||||
|
||||
<div>
|
||||
<Label htmlFor="sonnetModel" className="text-xs">
|
||||
Sonnet Model (ANTHROPIC_DEFAULT_SONNET_MODEL)
|
||||
{t('profileDialog.sonnetModel')}
|
||||
</Label>
|
||||
<Input
|
||||
id="sonnetModel"
|
||||
@@ -194,7 +206,7 @@ export function ProfileDialog({ open, onClose, profile }: ProfileDialogProps) {
|
||||
|
||||
<div>
|
||||
<Label htmlFor="haikuModel" className="text-xs">
|
||||
Haiku Model (ANTHROPIC_DEFAULT_HAIKU_MODEL)
|
||||
{t('profileDialog.haikuModel')}
|
||||
</Label>
|
||||
<Input
|
||||
id="haikuModel"
|
||||
@@ -209,14 +221,14 @@ export function ProfileDialog({ open, onClose, profile }: ProfileDialogProps) {
|
||||
|
||||
<div className="flex justify-end gap-2 pt-2">
|
||||
<Button type="button" variant="outline" onClick={onClose}>
|
||||
Cancel
|
||||
{t('profileDialog.cancel')}
|
||||
</Button>
|
||||
<Button type="submit" disabled={createMutation.isPending || updateMutation.isPending}>
|
||||
{createMutation.isPending || updateMutation.isPending
|
||||
? 'Saving...'
|
||||
? t('profileDialog.saving')
|
||||
: profile
|
||||
? 'Update'
|
||||
: 'Create'}
|
||||
? t('profileDialog.update')
|
||||
: t('profileDialog.create')}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
Reference in New Issue
Block a user