mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 02:11:28 +00:00
feat(api): add Alibaba Coding Plan preset and providers promotion
This commit is contained in:
@@ -109,10 +109,14 @@ The dashboard provides visual management for all account types:
|
||||
| **Azure Foundry** | API Key | `ccs foundry` | Claude via Microsoft Azure |
|
||||
| **Minimax** | API Key | `ccs mm` | M2 series, 1M context |
|
||||
| **DeepSeek** | API Key | `ccs deepseek` | V3.2 and R1 reasoning |
|
||||
| **Qwen** | API Key | `ccs qwen` | Alibaba Cloud, qwen3-coder |
|
||||
| **Qwen (OAuth)** | OAuth | `ccs qwen` | Qwen Code via CLIProxy |
|
||||
| **Qwen API** | API Key | `ccs api create --preset qwen` | DashScope Anthropic-compatible API |
|
||||
| **Alibaba Coding Plan** | API Key | `ccs api create --preset alibaba-coding-plan` | Model Studio Coding Plan endpoint |
|
||||
|
||||
**OpenRouter Integration** (v7.0.0): CCS v7.0.0 adds OpenRouter with interactive model picker, dynamic discovery, and tier mapping (opus/sonnet/haiku). Create via `ccs api create --preset openrouter` or dashboard.
|
||||
|
||||
**Alibaba Coding Plan Integration**: Configure via `ccs api create --preset alibaba-coding-plan` (or preset alias `alibaba`) with Coding Plan keys (`sk-sp-...`) and endpoint `https://coding-intl.dashscope.aliyuncs.com/apps/anthropic`.
|
||||
|
||||
**Ollama Integration**: Run local open-source models (qwen3-coder, gpt-oss:20b) with full privacy. Use `ccs api create --preset ollama` - requires [Ollama v0.14.0+](https://ollama.com) installed. For cloud models, use `ccs api create --preset ollama-cloud`.
|
||||
|
||||
**Azure Foundry**: Use `ccs api create --preset foundry` to set up Claude via Microsoft Azure AI Foundry. Requires Azure resource and API key from [ai.azure.com](https://ai.azure.com).
|
||||
@@ -143,9 +147,11 @@ ccs cursor # Cursor IDE integration (token import + local daemon)
|
||||
ccs kiro # Kiro/AWS CodeWhisperer (OAuth)
|
||||
ccs ghcp # GitHub Copilot (OAuth device flow)
|
||||
ccs agy # Antigravity (OAuth)
|
||||
ccs qwen # Qwen Code (OAuth via CLIProxy)
|
||||
ccs ollama # Local Ollama (no API key needed)
|
||||
ccs glm # GLM (API key)
|
||||
ccs km # Kimi API profile (API key)
|
||||
ccs api create --preset alibaba-coding-plan # Alibaba Coding Plan profile
|
||||
```
|
||||
|
||||
### Droid Alias (`argv[0]` pattern)
|
||||
|
||||
@@ -659,6 +659,8 @@ async function showHelp(): Promise<void> {
|
||||
console.log('');
|
||||
console.log(` ${dim('# Quick setup with preset')}`);
|
||||
console.log(` ${color('ccs api create --preset openrouter', 'command')}`);
|
||||
console.log(` ${color('ccs api create --preset alibaba-coding-plan', 'command')}`);
|
||||
console.log(` ${color('ccs api create --preset alibaba', 'command')} ${dim('# alias')}`);
|
||||
console.log(` ${color('ccs api create --preset glm', 'command')}`);
|
||||
console.log('');
|
||||
console.log(` ${dim('# Create with name')}`);
|
||||
|
||||
@@ -133,6 +133,10 @@ Run ${color('ccs config', 'command')} for web dashboard`.trim();
|
||||
['ccs glm', 'GLM 5 (API key required)'],
|
||||
['ccs glmt', 'GLM with thinking mode'],
|
||||
['ccs km', 'Kimi for Coding (API key)'],
|
||||
[
|
||||
'ccs api create --preset alibaba-coding-plan',
|
||||
'Alibaba Coding Plan (Anthropic-compatible API key)',
|
||||
],
|
||||
['ccs ollama', 'Local Ollama (http://localhost:11434)'],
|
||||
['ccs ollama-cloud', 'Ollama Cloud (API key required)'],
|
||||
['', ''], // Spacer
|
||||
@@ -184,7 +188,7 @@ Run ${color('ccs config', 'command')} for web dashboard`.trim();
|
||||
['ccs gemini', 'Google Gemini (gemini-2.5-pro or 3-pro)'],
|
||||
['ccs codex', 'OpenAI Codex (supports -medium/-high/-xhigh model suffixes)'],
|
||||
['ccs agy', 'Antigravity (Claude/Gemini models)'],
|
||||
['ccs qwen', 'Qwen Code (qwen3-coder)'],
|
||||
['ccs qwen', 'Qwen Code OAuth (CLIProxy)'],
|
||||
['ccs kimi', 'Kimi (Moonshot AI K2/K2.5 models)'],
|
||||
['ccs kiro', 'Kiro (AWS CodeWhisperer Claude models)'],
|
||||
['ccs ghcp', 'GitHub Copilot (OAuth via CLIProxy Plus)'],
|
||||
|
||||
@@ -9,6 +9,7 @@ export type PresetCategory = 'recommended' | 'alternative';
|
||||
|
||||
export const PROVIDER_PRESET_IDS = [
|
||||
'openrouter',
|
||||
'alibaba-coding-plan',
|
||||
'ollama',
|
||||
'glm',
|
||||
'glmt',
|
||||
@@ -51,6 +52,8 @@ export const OPENROUTER_BASE_URL = 'https://openrouter.ai/api';
|
||||
*/
|
||||
export const PROVIDER_PRESET_ALIASES: Readonly<Record<string, ProviderPresetId>> = Object.freeze({
|
||||
kimi: 'km',
|
||||
alibaba: 'alibaba-coding-plan',
|
||||
acp: 'alibaba-coding-plan',
|
||||
});
|
||||
|
||||
const RAW_PROVIDER_PRESET_DEFINITIONS: readonly ProviderPresetDefinition[] = [
|
||||
@@ -69,6 +72,21 @@ const RAW_PROVIDER_PRESET_DEFINITIONS: readonly ProviderPresetDefinition[] = [
|
||||
featured: true,
|
||||
icon: '/icons/openrouter.svg',
|
||||
},
|
||||
{
|
||||
id: 'alibaba-coding-plan',
|
||||
name: 'Alibaba Coding Plan',
|
||||
description: 'Alibaba Cloud Coding Plan via Anthropic-compatible endpoint',
|
||||
baseUrl: 'https://coding-intl.dashscope.aliyuncs.com/apps/anthropic',
|
||||
defaultProfileName: 'alibaba-plan',
|
||||
defaultModel: 'qwen3-coder-plus',
|
||||
apiKeyPlaceholder: 'sk-sp-...',
|
||||
apiKeyHint: 'Get your Coding Plan key from Alibaba Cloud Model Studio',
|
||||
category: 'recommended',
|
||||
requiresApiKey: true,
|
||||
badge: 'Coding Plan',
|
||||
featured: true,
|
||||
icon: '/assets/providers/alibabacloud-color.svg',
|
||||
},
|
||||
{
|
||||
id: 'ollama',
|
||||
name: 'Ollama (Local)',
|
||||
@@ -183,7 +201,7 @@ const RAW_PROVIDER_PRESET_DEFINITIONS: readonly ProviderPresetDefinition[] = [
|
||||
name: 'Qwen',
|
||||
description: 'Alibaba Cloud - Qwen3 models (256K-1M context, thinking support)',
|
||||
baseUrl: 'https://dashscope-intl.aliyuncs.com/apps/anthropic',
|
||||
defaultProfileName: 'qwen',
|
||||
defaultProfileName: 'qwen-api',
|
||||
defaultModel: 'qwen3-coder-plus',
|
||||
apiKeyPlaceholder: 'sk-...',
|
||||
apiKeyHint: 'Get your API key from Alibaba Cloud Model Studio',
|
||||
|
||||
@@ -2,6 +2,22 @@ import { describe, expect, it } from 'bun:test';
|
||||
import { getPresetById, isValidPresetId } from '../../../src/api/services/provider-presets';
|
||||
|
||||
describe('provider-presets', () => {
|
||||
it('resolves Alibaba Coding Plan preset id', () => {
|
||||
const preset = getPresetById('alibaba-coding-plan');
|
||||
expect(preset?.id).toBe('alibaba-coding-plan');
|
||||
expect(preset?.baseUrl).toBe('https://coding-intl.dashscope.aliyuncs.com/apps/anthropic');
|
||||
expect(preset?.defaultProfileName).toBe('alibaba-plan');
|
||||
});
|
||||
|
||||
it('resolves alibaba alias to Alibaba Coding Plan preset', () => {
|
||||
const preset = getPresetById('alibaba');
|
||||
expect(preset?.id).toBe('alibaba-coding-plan');
|
||||
});
|
||||
|
||||
it('treats alibaba alias as a valid preset id', () => {
|
||||
expect(isValidPresetId('alibaba')).toBe(true);
|
||||
});
|
||||
|
||||
it('resolves canonical km preset id', () => {
|
||||
const preset = getPresetById('km');
|
||||
expect(preset?.id).toBe('km');
|
||||
@@ -25,4 +41,9 @@ describe('provider-presets', () => {
|
||||
it('treats legacy kimi alias as a valid preset id', () => {
|
||||
expect(isValidPresetId('kimi')).toBe(true);
|
||||
});
|
||||
|
||||
it('uses non-reserved default profile name for qwen API preset', () => {
|
||||
const preset = getPresetById('qwen');
|
||||
expect(preset?.defaultProfileName).toBe('qwen-api');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>AlibabaCloud</title><path d="M14.752 4.64h5.274C22.242 4.64 24 6.475 24 8.691V15.8a3.947 3.947 0 01-3.974 3.975h-5.274l1.299-1.835 3.822-1.222c.688-.23 1.146-.918 1.146-1.605v-5.81c0-.687-.458-1.375-1.146-1.605L16.05 6.475l-1.3-1.835zM2.98 15.111c0 .688.46 1.376 1.147 1.606l3.822 1.146 1.3 1.835H3.974A3.947 3.947 0 010 15.723V8.69c0-2.216 1.758-4.05 3.975-4.05h5.273L7.95 6.474 4.127 7.697c-.688.23-1.146.918-1.146 1.606v5.808z" fill="#FF6A00"></path><path d="M16.051 11.213H8.025v1.835h8.026v-1.835z" fill="#FF6A00"></path></svg>
|
||||
|
After Width: | Height: | Size: 656 B |
@@ -79,7 +79,7 @@ function buildNavGroups(t: (key: string) => string): SidebarGroupDef[] {
|
||||
path: '/providers',
|
||||
icon: Key,
|
||||
label: t('nav.apiProfiles'),
|
||||
badge: { text: 'OpenRouter', icon: '/icons/openrouter.svg' },
|
||||
badge: { text: 'Featured', icon: '/icons/openrouter.svg' },
|
||||
},
|
||||
{
|
||||
path: '/cliproxy',
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Alibaba Coding Plan Promo Card
|
||||
* Permanent promotional card for Alibaba Coding Plan in providers sidebar.
|
||||
*/
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { CloudCog } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface AlibabaCodingPlanPromoCardProps {
|
||||
onCreateClick: () => void;
|
||||
}
|
||||
|
||||
export function AlibabaCodingPlanPromoCard({ onCreateClick }: AlibabaCodingPlanPromoCardProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="p-3 border-t bg-gradient-to-r from-orange-500/5 to-orange-500/10 dark:from-orange-500/10 dark:to-orange-500/15">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="p-1.5 bg-orange-500/10 dark:bg-orange-500/20 rounded shrink-0">
|
||||
<img src="/assets/providers/alibabacloud-color.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">
|
||||
{t('alibabaCodingPlanPromo.title')}
|
||||
</p>
|
||||
<p className="text-[10px] text-muted-foreground truncate">
|
||||
{t('alibabaCodingPlanPromo.subtitle')}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={onCreateClick}
|
||||
className="h-7 px-2 text-orange-700 dark:text-orange-300 hover:text-orange-700 hover:bg-orange-500/10 dark:hover:bg-orange-500/20"
|
||||
>
|
||||
<CloudCog className="w-3 h-3 mr-1" />
|
||||
<span className="text-xs">{t('alibabaCodingPlanPromo.add')}</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -19,5 +19,6 @@ export { OpenRouterBanner } from './openrouter-banner';
|
||||
export { OpenRouterModelPicker } from './openrouter-model-picker';
|
||||
export { OpenRouterPromoCard } from './openrouter-promo-card';
|
||||
export { OpenRouterQuickStart } from './openrouter-quick-start';
|
||||
export { AlibabaCodingPlanPromoCard } from './alibaba-coding-plan-promo-card';
|
||||
export { ModelTierMapping } from './model-tier-mapping';
|
||||
export type { TierMapping } from './model-tier-mapping';
|
||||
|
||||
@@ -8,16 +8,18 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
|
||||
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 { Sparkles, ExternalLink, ArrowRight, Zap, CloudCog, KeyRound } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface OpenRouterQuickStartProps {
|
||||
onOpenRouterClick: () => void;
|
||||
onAlibabaCodingPlanClick: () => void;
|
||||
onCustomClick: () => void;
|
||||
}
|
||||
|
||||
export function OpenRouterQuickStart({
|
||||
onOpenRouterClick,
|
||||
onAlibabaCodingPlanClick,
|
||||
onCustomClick,
|
||||
}: OpenRouterQuickStartProps) {
|
||||
const { t } = useTranslation();
|
||||
@@ -84,6 +86,65 @@ export function OpenRouterQuickStart({
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Alibaba Coding Plan Card */}
|
||||
<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">
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="p-2 rounded-lg bg-orange-500/10 dark:bg-orange-500/20">
|
||||
<img
|
||||
src="/assets/providers/alibabacloud-color.svg"
|
||||
alt="Alibaba Coding Plan"
|
||||
className="w-6 h-6"
|
||||
/>
|
||||
</div>
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="bg-orange-500/10 text-orange-700 dark:bg-orange-500/20 dark:text-orange-200"
|
||||
>
|
||||
{t('alibabaCodingPlanQuickStart.recommended')}
|
||||
</Badge>
|
||||
</div>
|
||||
<CardTitle className="text-xl">{t('alibabaCodingPlanQuickStart.title')}</CardTitle>
|
||||
<CardDescription className="text-base">
|
||||
{t('alibabaCodingPlanQuickStart.description')}
|
||||
</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">
|
||||
<CloudCog className="w-4 h-4 text-orange-600" />
|
||||
<span>{t('alibabaCodingPlanQuickStart.featureEndpoint')}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-muted-foreground">
|
||||
<KeyRound className="w-4 h-4 text-orange-600" />
|
||||
<span>{t('alibabaCodingPlanQuickStart.featureKeyFormat')}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={onAlibabaCodingPlanClick}
|
||||
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>
|
||||
|
||||
{/* Divider */}
|
||||
<div className="flex items-center gap-4">
|
||||
<Separator className="flex-1" />
|
||||
|
||||
@@ -72,7 +72,7 @@ interface ProfileCreateDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onSuccess: (name: string) => void;
|
||||
initialMode?: 'normal' | 'openrouter';
|
||||
initialMode?: 'normal' | 'openrouter' | 'alibaba-coding-plan';
|
||||
}
|
||||
|
||||
// Common URL mistakes to warn about
|
||||
@@ -180,7 +180,8 @@ export function ProfileCreateDialog({
|
||||
setSelectedPreset(CUSTOM_PRESET_ID);
|
||||
applyPresetToForm(null);
|
||||
} else {
|
||||
const defaultPreset = getPresetById(DEFAULT_PRESET_ID);
|
||||
const presetId = initialMode === 'openrouter' ? DEFAULT_PRESET_ID : initialMode;
|
||||
const defaultPreset = getPresetById(presetId);
|
||||
if (defaultPreset) {
|
||||
setSelectedPreset(defaultPreset.id);
|
||||
applyPresetToForm(defaultPreset);
|
||||
|
||||
+47
-3
@@ -32,7 +32,7 @@ const resources = {
|
||||
system: 'System',
|
||||
health: 'Health',
|
||||
settings: 'Settings',
|
||||
openrouterTooltip: '349+ models via OpenRouter',
|
||||
openrouterTooltip: 'Featured: OpenRouter + Alibaba Coding Plan',
|
||||
},
|
||||
home: {
|
||||
profiles: 'Profiles',
|
||||
@@ -240,6 +240,21 @@ const resources = {
|
||||
or: 'or',
|
||||
createCustomProfile: 'Create Custom API Profile',
|
||||
},
|
||||
alibabaCodingPlanQuickStart: {
|
||||
recommended: 'Recommended',
|
||||
title: 'Try Alibaba Coding Plan',
|
||||
description:
|
||||
'Use Alibaba Cloud Coding Plan through an Anthropic-compatible endpoint for coding workloads.',
|
||||
featureEndpoint: 'Anthropic-compatible endpoint',
|
||||
featureKeyFormat: 'Dedicated sk-sp API keys',
|
||||
createAlibabaProfile: 'Create Alibaba Coding Plan Profile',
|
||||
readGuideAt: 'Read setup guide at',
|
||||
},
|
||||
alibabaCodingPlanPromo: {
|
||||
title: 'Alibaba Coding Plan',
|
||||
subtitle: 'Model Studio Coding Plan via Anthropic endpoint',
|
||||
add: 'Add',
|
||||
},
|
||||
credentialHealth: {
|
||||
title: 'Credential Health',
|
||||
ready: 'Ready',
|
||||
@@ -1164,7 +1179,7 @@ const resources = {
|
||||
system: '系统',
|
||||
health: '健康',
|
||||
settings: '设置',
|
||||
openrouterTooltip: '通过 OpenRouter 提供 349+ 模型',
|
||||
openrouterTooltip: '精选:OpenRouter + Alibaba Coding Plan',
|
||||
},
|
||||
home: {
|
||||
profiles: '配置',
|
||||
@@ -1368,6 +1383,20 @@ const resources = {
|
||||
or: '或',
|
||||
createCustomProfile: '创建自定义 API 配置',
|
||||
},
|
||||
alibabaCodingPlanQuickStart: {
|
||||
recommended: '推荐',
|
||||
title: '试用阿里云 Coding Plan',
|
||||
description: '通过 Anthropic 兼容端点接入阿里云 Model Studio Coding Plan。',
|
||||
featureEndpoint: 'Anthropic 兼容端点',
|
||||
featureKeyFormat: '专用 sk-sp API Key',
|
||||
createAlibabaProfile: '创建 Alibaba Coding Plan 配置',
|
||||
readGuideAt: '查看配置文档:',
|
||||
},
|
||||
alibabaCodingPlanPromo: {
|
||||
title: 'Alibaba Coding Plan',
|
||||
subtitle: '通过 Anthropic 端点接入 Model Studio Coding Plan',
|
||||
add: '添加',
|
||||
},
|
||||
credentialHealth: {
|
||||
title: '凭据健康状态',
|
||||
ready: '就绪',
|
||||
@@ -2254,7 +2283,7 @@ const resources = {
|
||||
system: 'Hệ thống',
|
||||
health: 'Sức khỏe',
|
||||
settings: 'Cài đặt',
|
||||
openrouterTooltip: 'Hơn 349 mô hình qua OpenRouter',
|
||||
openrouterTooltip: 'Nổi bật: OpenRouter + Alibaba Coding Plan',
|
||||
},
|
||||
home: {
|
||||
profiles: 'Hồ sơ',
|
||||
@@ -2464,6 +2493,21 @@ const resources = {
|
||||
or: 'hoặc',
|
||||
createCustomProfile: 'Tạo hồ sơ API tùy chỉnh',
|
||||
},
|
||||
alibabaCodingPlanQuickStart: {
|
||||
recommended: 'Đề xuất',
|
||||
title: 'Dùng thử Alibaba Coding Plan',
|
||||
description:
|
||||
'Sử dụng Alibaba Cloud Coding Plan qua endpoint tương thích Anthropic cho tác vụ lập trình.',
|
||||
featureEndpoint: 'Endpoint tương thích Anthropic',
|
||||
featureKeyFormat: 'Khóa API sk-sp chuyên dụng',
|
||||
createAlibabaProfile: 'Tạo hồ sơ Alibaba Coding Plan',
|
||||
readGuideAt: 'Xem hướng dẫn tại',
|
||||
},
|
||||
alibabaCodingPlanPromo: {
|
||||
title: 'Alibaba Coding Plan',
|
||||
subtitle: 'Model Studio Coding Plan qua endpoint Anthropic',
|
||||
add: 'Thêm',
|
||||
},
|
||||
credentialHealth: {
|
||||
title: 'Tình trạng thông tin xác thực',
|
||||
ready: 'Sẵn sàng',
|
||||
|
||||
+14
-37
@@ -1,8 +1,3 @@
|
||||
/**
|
||||
* API Profiles Page - Master-Detail Layout
|
||||
* Comprehensive profile management with inline editing
|
||||
*/
|
||||
|
||||
import { useState, useMemo } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
@@ -23,6 +18,7 @@ import { ProfileCreateDialog } from '@/components/profiles/profile-create-dialog
|
||||
import { OpenRouterBanner } from '@/components/profiles/openrouter-banner';
|
||||
import { OpenRouterQuickStart } from '@/components/profiles/openrouter-quick-start';
|
||||
import { OpenRouterPromoCard } from '@/components/profiles/openrouter-promo-card';
|
||||
import { AlibabaCodingPlanPromoCard } from '@/components/profiles/alibaba-coding-plan-promo-card';
|
||||
import { useProfiles, useDeleteProfile } from '@/hooks/use-profiles';
|
||||
import { useOpenRouterModels } from '@/hooks/use-openrouter-models';
|
||||
import { ConfirmDialog } from '@/components/shared/confirm-dialog';
|
||||
@@ -38,30 +34,22 @@ export function ApiPage() {
|
||||
const [selectedProfile, setSelectedProfile] = useState<string | null>(null);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [isCreateDialogOpen, setCreateDialogOpen] = useState(false);
|
||||
const [createMode, setCreateMode] = useState<'normal' | 'openrouter'>('normal');
|
||||
const [createMode, setCreateMode] = useState<'normal' | 'openrouter' | 'alibaba-coding-plan'>(
|
||||
'normal'
|
||||
);
|
||||
const [deleteConfirm, setDeleteConfirm] = useState<string | null>(null);
|
||||
const [editorHasChanges, setEditorHasChanges] = useState(false);
|
||||
const [pendingSwitch, setPendingSwitch] = useState<string | null>(null);
|
||||
|
||||
// Prefetch OpenRouter models when page loads (lazy - won't block render)
|
||||
useOpenRouterModels();
|
||||
|
||||
// Memoize profiles to maintain stable reference
|
||||
const profiles = useMemo(() => data?.profiles || [], [data?.profiles]);
|
||||
|
||||
// Filter profiles by search
|
||||
const filteredProfiles = useMemo(
|
||||
() => profiles.filter((p) => p.name.toLowerCase().includes(searchQuery.toLowerCase())),
|
||||
[profiles, searchQuery]
|
||||
);
|
||||
|
||||
// selectedProfile is null by default - user must click to select
|
||||
// This allows OpenRouterQuickStart to show as the default right panel
|
||||
const selectedProfileData = selectedProfile
|
||||
? profiles.find((p) => p.name === selectedProfile)
|
||||
: null;
|
||||
|
||||
// Handle profile deletion
|
||||
const handleDelete = (name: string) => {
|
||||
deleteMutation.mutate(name, {
|
||||
onSuccess: () => {
|
||||
@@ -73,18 +61,14 @@ export function ApiPage() {
|
||||
});
|
||||
};
|
||||
|
||||
// Handle create success
|
||||
const handleCreateSuccess = (name: string) => {
|
||||
setCreateDialogOpen(false);
|
||||
// Use the same unsaved changes check as profile selection
|
||||
if (editorHasChanges && selectedProfile !== null) {
|
||||
setPendingSwitch(name);
|
||||
} else {
|
||||
setSelectedProfile(name);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle profile selection with unsaved changes check
|
||||
const handleProfileSelect = (name: string) => {
|
||||
if (editorHasChanges && selectedProfile !== name) {
|
||||
setPendingSwitch(name);
|
||||
@@ -95,14 +79,9 @@ export function ApiPage() {
|
||||
|
||||
return (
|
||||
<div className="h-[calc(100vh-100px)] flex flex-col">
|
||||
{/* OpenRouter Announcement Banner */}
|
||||
<OpenRouterBanner onCreateClick={() => setCreateDialogOpen(true)} />
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="flex-1 flex min-h-0">
|
||||
{/* Left Panel - Profiles List */}
|
||||
<div className="w-80 border-r flex flex-col bg-muted/30">
|
||||
{/* Header */}
|
||||
<div className="p-4 border-b bg-background">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -120,7 +99,6 @@ export function ApiPage() {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Search */}
|
||||
<div className="relative">
|
||||
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
@@ -132,7 +110,6 @@ export function ApiPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Profile List */}
|
||||
<ScrollArea className="flex-1">
|
||||
{isLoading ? (
|
||||
<div className="p-4 text-sm text-muted-foreground">
|
||||
@@ -197,7 +174,6 @@ export function ApiPage() {
|
||||
)}
|
||||
</ScrollArea>
|
||||
|
||||
{/* Footer Stats */}
|
||||
{profiles.length > 0 && (
|
||||
<div className="p-3 border-t bg-background text-xs text-muted-foreground">
|
||||
<div className="flex items-center justify-between">
|
||||
@@ -212,16 +188,20 @@ export function ApiPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* OpenRouter Promo - always visible */}
|
||||
<OpenRouterPromoCard
|
||||
onCreateClick={() => {
|
||||
setCreateMode('openrouter');
|
||||
setCreateDialogOpen(true);
|
||||
}}
|
||||
/>
|
||||
<AlibabaCodingPlanPromoCard
|
||||
onCreateClick={() => {
|
||||
setCreateMode('alibaba-coding-plan');
|
||||
setCreateDialogOpen(true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Right Panel - Editor or QuickStart */}
|
||||
<div className="flex-1 flex flex-col min-w-0">
|
||||
{selectedProfileData ? (
|
||||
<ProfileEditor
|
||||
@@ -237,6 +217,10 @@ export function ApiPage() {
|
||||
setCreateMode('openrouter');
|
||||
setCreateDialogOpen(true);
|
||||
}}
|
||||
onAlibabaCodingPlanClick={() => {
|
||||
setCreateMode('alibaba-coding-plan');
|
||||
setCreateDialogOpen(true);
|
||||
}}
|
||||
onCustomClick={() => {
|
||||
setCreateMode('normal');
|
||||
setCreateDialogOpen(true);
|
||||
@@ -246,7 +230,6 @@ export function ApiPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Create Dialog */}
|
||||
<ProfileCreateDialog
|
||||
open={isCreateDialogOpen}
|
||||
onOpenChange={setCreateDialogOpen}
|
||||
@@ -254,7 +237,6 @@ export function ApiPage() {
|
||||
initialMode={createMode}
|
||||
/>
|
||||
|
||||
{/* Delete Confirmation */}
|
||||
<ConfirmDialog
|
||||
open={!!deleteConfirm}
|
||||
title={t('apiProfiles.deleteProfileTitle')}
|
||||
@@ -265,7 +247,6 @@ export function ApiPage() {
|
||||
onCancel={() => setDeleteConfirm(null)}
|
||||
/>
|
||||
|
||||
{/* Unsaved Changes Confirmation */}
|
||||
<ConfirmDialog
|
||||
open={!!pendingSwitch}
|
||||
title={t('apiProfiles.unsavedChangesTitle')}
|
||||
@@ -286,7 +267,6 @@ export function ApiPage() {
|
||||
);
|
||||
}
|
||||
|
||||
/** Profile list item component */
|
||||
function ProfileListItem({
|
||||
profile,
|
||||
isSelected,
|
||||
@@ -308,14 +288,12 @@ function ProfileListItem({
|
||||
)}
|
||||
onClick={onSelect}
|
||||
>
|
||||
{/* Status indicator */}
|
||||
{profile.configured ? (
|
||||
<CheckCircle2 className="w-4 h-4 text-green-600 shrink-0" />
|
||||
) : (
|
||||
<AlertCircle className="w-4 h-4 text-yellow-600 shrink-0" />
|
||||
)}
|
||||
|
||||
{/* Profile info */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<div className="font-medium text-sm truncate">{profile.name}</div>
|
||||
@@ -335,7 +313,6 @@ function ProfileListItem({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
|
||||
Reference in New Issue
Block a user