mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-02 12:19:35 +00:00
feat(cliproxy): add multi-account support phases 02-03
Phase 02 - CLI Account Selector in Variant Wizard: - Add --account flag to cliproxy create command - Implement inline OAuth flow when no accounts exist - Add account selector for multiple accounts - Auto-select single account Phase 03 - Dashboard Quick Setup Wizard: - Create quick-setup-wizard.tsx component - Add step-by-step wizard: Provider → Auth → Account → Variant → Success - Add Quick Setup button to CLIProxy page
This commit is contained in:
@@ -23,7 +23,8 @@ import {
|
|||||||
isCLIProxyInstalled,
|
isCLIProxyInstalled,
|
||||||
getCLIProxyPath,
|
getCLIProxyPath,
|
||||||
} from '../cliproxy';
|
} from '../cliproxy';
|
||||||
import { getAllAuthStatus, getOAuthConfig } from '../cliproxy/auth-handler';
|
import { getAllAuthStatus, getOAuthConfig, triggerOAuth } from '../cliproxy/auth-handler';
|
||||||
|
import { getProviderAccounts } from '../cliproxy/account-manager';
|
||||||
import { CLIPROXY_FALLBACK_VERSION } from '../cliproxy/platform-detector';
|
import { CLIPROXY_FALLBACK_VERSION } from '../cliproxy/platform-detector';
|
||||||
import { CLIPROXY_PROFILES, CLIProxyProfileName } from '../auth/profile-detector';
|
import { CLIPROXY_PROFILES, CLIProxyProfileName } from '../auth/profile-detector';
|
||||||
import { getCcsDir, getConfigPath, loadConfig } from '../utils/config-manager';
|
import { getCcsDir, getConfigPath, loadConfig } from '../utils/config-manager';
|
||||||
@@ -53,6 +54,7 @@ interface CliproxyProfileArgs {
|
|||||||
name?: string;
|
name?: string;
|
||||||
provider?: CLIProxyProfileName;
|
provider?: CLIProxyProfileName;
|
||||||
model?: string;
|
model?: string;
|
||||||
|
account?: string;
|
||||||
force?: boolean;
|
force?: boolean;
|
||||||
yes?: boolean;
|
yes?: boolean;
|
||||||
}
|
}
|
||||||
@@ -70,6 +72,8 @@ function parseProfileArgs(args: string[]): CliproxyProfileArgs {
|
|||||||
result.provider = args[++i] as CLIProxyProfileName;
|
result.provider = args[++i] as CLIProxyProfileName;
|
||||||
} else if (arg === '--model' && args[i + 1]) {
|
} else if (arg === '--model' && args[i + 1]) {
|
||||||
result.model = args[++i];
|
result.model = args[++i];
|
||||||
|
} else if (arg === '--account' && args[i + 1]) {
|
||||||
|
result.account = args[++i];
|
||||||
} else if (arg === '--force') {
|
} else if (arg === '--force') {
|
||||||
result.force = true;
|
result.force = true;
|
||||||
} else if (arg === '--yes' || arg === '-y') {
|
} else if (arg === '--yes' || arg === '-y') {
|
||||||
@@ -136,7 +140,8 @@ function cliproxyVariantExists(name: string): boolean {
|
|||||||
function createCliproxySettingsFile(
|
function createCliproxySettingsFile(
|
||||||
name: string,
|
name: string,
|
||||||
provider: CLIProxyProfileName,
|
provider: CLIProxyProfileName,
|
||||||
model: string
|
model: string,
|
||||||
|
_account?: string
|
||||||
): string {
|
): string {
|
||||||
const ccsDir = getCcsDir();
|
const ccsDir = getCcsDir();
|
||||||
const settingsPath = path.join(ccsDir, `${provider}-${name}.settings.json`);
|
const settingsPath = path.join(ccsDir, `${provider}-${name}.settings.json`);
|
||||||
@@ -170,7 +175,8 @@ function createCliproxySettingsFile(
|
|||||||
function addCliproxyVariant(
|
function addCliproxyVariant(
|
||||||
name: string,
|
name: string,
|
||||||
provider: CLIProxyProfileName,
|
provider: CLIProxyProfileName,
|
||||||
settingsPath: string
|
settingsPath: string,
|
||||||
|
account?: string
|
||||||
): void {
|
): void {
|
||||||
const configPath = getConfigPath();
|
const configPath = getConfigPath();
|
||||||
|
|
||||||
@@ -189,10 +195,16 @@ function addCliproxyVariant(
|
|||||||
|
|
||||||
// Use relative path with ~ for portability
|
// Use relative path with ~ for portability
|
||||||
const relativePath = `~/.ccs/${path.basename(settingsPath)}`;
|
const relativePath = `~/.ccs/${path.basename(settingsPath)}`;
|
||||||
config.cliproxy[name] = {
|
|
||||||
|
// Build variant config with optional account
|
||||||
|
const variantConfig: { provider: string; settings: string; account?: string } = {
|
||||||
provider,
|
provider,
|
||||||
settings: relativePath,
|
settings: relativePath,
|
||||||
};
|
};
|
||||||
|
if (account) {
|
||||||
|
variantConfig.account = account;
|
||||||
|
}
|
||||||
|
config.cliproxy[name] = variantConfig;
|
||||||
|
|
||||||
// Write config atomically
|
// Write config atomically
|
||||||
const tempPath = configPath + '.tmp';
|
const tempPath = configPath + '.tmp';
|
||||||
@@ -290,6 +302,103 @@ async function handleCreate(args: string[]): Promise<void> {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Step 2.5: Account selection
|
||||||
|
let account = parsedArgs.account;
|
||||||
|
const providerAccounts = getProviderAccounts(provider as CLIProxyProvider);
|
||||||
|
|
||||||
|
if (!account) {
|
||||||
|
if (providerAccounts.length === 0) {
|
||||||
|
// No accounts - prompt to authenticate first
|
||||||
|
console.log('');
|
||||||
|
console.log(warn(`No accounts authenticated for ${provider}`));
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
const shouldAuth = await InteractivePrompt.confirm(`Authenticate with ${provider} now?`, {
|
||||||
|
default: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!shouldAuth) {
|
||||||
|
console.log('');
|
||||||
|
console.log(info('Run authentication first:'));
|
||||||
|
console.log(` ${color(`ccs ${provider} --auth`, 'command')}`);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trigger OAuth inline
|
||||||
|
console.log('');
|
||||||
|
const newAccount = await triggerOAuth(provider as CLIProxyProvider, {
|
||||||
|
add: true,
|
||||||
|
verbose: args.includes('--verbose'),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!newAccount) {
|
||||||
|
console.log(fail('Authentication failed'));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
account = newAccount.id;
|
||||||
|
console.log('');
|
||||||
|
console.log(ok(`Authenticated as ${newAccount.email || newAccount.id}`));
|
||||||
|
} else if (providerAccounts.length === 1) {
|
||||||
|
// Single account - auto-select
|
||||||
|
account = providerAccounts[0].id;
|
||||||
|
} else {
|
||||||
|
// Multiple accounts - show selector with "Add new" option
|
||||||
|
const ADD_NEW_ID = '__add_new__';
|
||||||
|
|
||||||
|
const accountOptions = [
|
||||||
|
...providerAccounts.map((acc) => ({
|
||||||
|
id: acc.id,
|
||||||
|
label: `${acc.email || acc.id}${acc.isDefault ? ' (default)' : ''}`,
|
||||||
|
})),
|
||||||
|
{
|
||||||
|
id: ADD_NEW_ID,
|
||||||
|
label: color('[+ Add new account...]', 'info'),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const defaultIdx = providerAccounts.findIndex((a) => a.isDefault);
|
||||||
|
|
||||||
|
const selectedAccount = await InteractivePrompt.selectFromList(
|
||||||
|
'Select account:',
|
||||||
|
accountOptions,
|
||||||
|
{ defaultIndex: defaultIdx >= 0 ? defaultIdx : 0 }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (selectedAccount === ADD_NEW_ID) {
|
||||||
|
// Add new account inline
|
||||||
|
console.log('');
|
||||||
|
const newAccount = await triggerOAuth(provider as CLIProxyProvider, {
|
||||||
|
add: true,
|
||||||
|
verbose: args.includes('--verbose'),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!newAccount) {
|
||||||
|
console.log(fail('Authentication failed'));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
account = newAccount.id;
|
||||||
|
console.log('');
|
||||||
|
console.log(ok(`Authenticated as ${newAccount.email || newAccount.id}`));
|
||||||
|
} else {
|
||||||
|
account = selectedAccount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Validate provided account exists
|
||||||
|
const exists = providerAccounts.find((a) => a.id === account);
|
||||||
|
if (!exists) {
|
||||||
|
console.log(fail(`Account '${account}' not found for ${provider}`));
|
||||||
|
console.log('');
|
||||||
|
console.log('Available accounts:');
|
||||||
|
providerAccounts.forEach((a) => {
|
||||||
|
console.log(` - ${a.email || a.id}${a.isDefault ? ' (default)' : ''}`);
|
||||||
|
});
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Step 3: Model selection
|
// Step 3: Model selection
|
||||||
let model = parsedArgs.model;
|
let model = parsedArgs.model;
|
||||||
if (!model) {
|
if (!model) {
|
||||||
@@ -323,8 +432,8 @@ async function handleCreate(args: string[]): Promise<void> {
|
|||||||
console.log(info('Creating CLIProxy variant...'));
|
console.log(info('Creating CLIProxy variant...'));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const settingsPath = createCliproxySettingsFile(name, provider, model);
|
const settingsPath = createCliproxySettingsFile(name, provider, model, account);
|
||||||
addCliproxyVariant(name, provider, settingsPath);
|
addCliproxyVariant(name, provider, settingsPath, account);
|
||||||
|
|
||||||
console.log('');
|
console.log('');
|
||||||
console.log(
|
console.log(
|
||||||
@@ -332,6 +441,7 @@ async function handleCreate(args: string[]): Promise<void> {
|
|||||||
`Variant: ${name}\n` +
|
`Variant: ${name}\n` +
|
||||||
`Provider: ${provider}\n` +
|
`Provider: ${provider}\n` +
|
||||||
`Model: ${model}\n` +
|
`Model: ${model}\n` +
|
||||||
|
(account ? `Account: ${account}\n` : '') +
|
||||||
`Settings: ~/.ccs/${path.basename(settingsPath)}`,
|
`Settings: ~/.ccs/${path.basename(settingsPath)}`,
|
||||||
'CLIProxy Variant Created'
|
'CLIProxy Variant Created'
|
||||||
)
|
)
|
||||||
@@ -551,6 +661,7 @@ async function showHelp(): Promise<void> {
|
|||||||
const createOpts: [string, string][] = [
|
const createOpts: [string, string][] = [
|
||||||
['--provider <name>', 'Provider (gemini, codex, agy, qwen)'],
|
['--provider <name>', 'Provider (gemini, codex, agy, qwen)'],
|
||||||
['--model <model>', 'Model name'],
|
['--model <model>', 'Model name'],
|
||||||
|
['--account <id>', 'Account ID (email or default)'],
|
||||||
['--force', 'Overwrite existing variant'],
|
['--force', 'Overwrite existing variant'],
|
||||||
['--yes, -y', 'Skip confirmation prompts'],
|
['--yes, -y', 'Skip confirmation prompts'],
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -0,0 +1,359 @@
|
|||||||
|
/**
|
||||||
|
* Quick Setup Wizard Component
|
||||||
|
* Phase 03: Multi-account dashboard support
|
||||||
|
*
|
||||||
|
* Step-by-step wizard: Provider -> Auth -> Account -> Variant -> Success
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogDescription,
|
||||||
|
} from '@/components/ui/dialog';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
|
import {
|
||||||
|
Copy,
|
||||||
|
Check,
|
||||||
|
RefreshCw,
|
||||||
|
ChevronRight,
|
||||||
|
ArrowLeft,
|
||||||
|
Terminal,
|
||||||
|
User,
|
||||||
|
Sparkles,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { useCliproxyAuth, useCreateVariant } from '@/hooks/use-cliproxy';
|
||||||
|
import type { AuthStatus, OAuthAccount } from '@/lib/api-client';
|
||||||
|
|
||||||
|
interface QuickSetupWizardProps {
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
type WizardStep = 'provider' | 'auth' | 'account' | 'variant' | 'success';
|
||||||
|
|
||||||
|
const providers = [
|
||||||
|
{ id: 'gemini', name: 'Google Gemini', description: 'Gemini Pro/Flash models' },
|
||||||
|
{ id: 'codex', name: 'OpenAI Codex', description: 'GPT-4 and codex models' },
|
||||||
|
{ id: 'agy', name: 'Antigravity', description: 'Antigravity AI models' },
|
||||||
|
{ id: 'qwen', name: 'Alibaba Qwen', description: 'Qwen Code models' },
|
||||||
|
{ id: 'iflow', name: 'iFlow', description: 'iFlow AI models' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function QuickSetupWizard({ open, onClose }: QuickSetupWizardProps) {
|
||||||
|
const [step, setStep] = useState<WizardStep>('provider');
|
||||||
|
const [selectedProvider, setSelectedProvider] = useState<string>('');
|
||||||
|
const [selectedAccount, setSelectedAccount] = useState<OAuthAccount | null>(null);
|
||||||
|
const [variantName, setVariantName] = useState('');
|
||||||
|
const [modelName, setModelName] = useState('');
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||||
|
|
||||||
|
const { data: authData, refetch } = useCliproxyAuth();
|
||||||
|
const createMutation = useCreateVariant();
|
||||||
|
|
||||||
|
// Get auth status for selected provider
|
||||||
|
const providerAuth = authData?.authStatus.find(
|
||||||
|
(s: AuthStatus) => s.provider === selectedProvider
|
||||||
|
);
|
||||||
|
const accounts = providerAuth?.accounts || [];
|
||||||
|
|
||||||
|
// Reset on close - use timeout to avoid synchronous setState in effect
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setStep('provider');
|
||||||
|
setSelectedProvider('');
|
||||||
|
setSelectedAccount(null);
|
||||||
|
setVariantName('');
|
||||||
|
setModelName('');
|
||||||
|
setCopied(false);
|
||||||
|
}, 0);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
|
// Auto-advance from auth step when account detected
|
||||||
|
// Use timeout to avoid synchronous setState in effect (React lint rule)
|
||||||
|
useEffect(() => {
|
||||||
|
if (step === 'auth' && accounts.length > 0) {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
if (accounts.length === 1) {
|
||||||
|
setSelectedAccount(accounts[0]);
|
||||||
|
setStep('variant');
|
||||||
|
} else {
|
||||||
|
setStep('account');
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}, [step, accounts]);
|
||||||
|
|
||||||
|
const copyCommand = async (cmd: string) => {
|
||||||
|
await navigator.clipboard.writeText(cmd);
|
||||||
|
setCopied(true);
|
||||||
|
setTimeout(() => setCopied(false), 2000);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRefresh = async () => {
|
||||||
|
setIsRefreshing(true);
|
||||||
|
await refetch();
|
||||||
|
setIsRefreshing(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleProviderSelect = (providerId: string) => {
|
||||||
|
setSelectedProvider(providerId);
|
||||||
|
const auth = authData?.authStatus.find((s: AuthStatus) => s.provider === providerId);
|
||||||
|
const provAccounts = auth?.accounts || [];
|
||||||
|
|
||||||
|
if (provAccounts.length === 0) {
|
||||||
|
setStep('auth');
|
||||||
|
} else if (provAccounts.length === 1) {
|
||||||
|
setSelectedAccount(provAccounts[0]);
|
||||||
|
setStep('variant');
|
||||||
|
} else {
|
||||||
|
setStep('account');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAccountSelect = (account: OAuthAccount) => {
|
||||||
|
setSelectedAccount(account);
|
||||||
|
setStep('variant');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreateVariant = async () => {
|
||||||
|
if (!variantName || !selectedProvider) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await createMutation.mutateAsync({
|
||||||
|
name: variantName,
|
||||||
|
provider: selectedProvider as 'gemini' | 'codex' | 'agy' | 'qwen' | 'iflow',
|
||||||
|
model: modelName || undefined,
|
||||||
|
account: selectedAccount?.id,
|
||||||
|
});
|
||||||
|
setStep('success');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to create variant:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const authCommand = `ccs ${selectedProvider} --auth --add`;
|
||||||
|
|
||||||
|
// Progress steps for indicator
|
||||||
|
const allSteps = ['provider', 'auth', 'variant', 'success'];
|
||||||
|
const getStepProgress = (s: WizardStep) => {
|
||||||
|
if (s === 'account') return 1; // Same as auth
|
||||||
|
return allSteps.indexOf(s);
|
||||||
|
};
|
||||||
|
const currentProgress = getStepProgress(step);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onClose}>
|
||||||
|
<DialogContent className="sm:max-w-lg">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="flex items-center gap-2">
|
||||||
|
<Sparkles className="w-5 h-5 text-primary" />
|
||||||
|
Quick Setup Wizard
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
{step === 'provider' && 'Select a provider to get started'}
|
||||||
|
{step === 'auth' && 'Authenticate with your provider'}
|
||||||
|
{step === 'account' && 'Select which account to use'}
|
||||||
|
{step === 'variant' && 'Create your custom variant'}
|
||||||
|
{step === 'success' && 'Setup complete!'}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-4 py-4">
|
||||||
|
{/* Step: Provider Selection */}
|
||||||
|
{step === 'provider' && (
|
||||||
|
<div className="grid gap-2">
|
||||||
|
{providers.map((p) => (
|
||||||
|
<button
|
||||||
|
key={p.id}
|
||||||
|
onClick={() => handleProviderSelect(p.id)}
|
||||||
|
className="flex items-center justify-between p-3 border rounded-lg hover:bg-muted/50 transition-colors text-left"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div className="font-medium">{p.name}</div>
|
||||||
|
<div className="text-xs text-muted-foreground">{p.description}</div>
|
||||||
|
</div>
|
||||||
|
<ChevronRight className="w-4 h-4 text-muted-foreground" />
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Step: Authentication */}
|
||||||
|
{step === 'auth' && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-4 space-y-3">
|
||||||
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||||
|
<Terminal className="w-4 h-4" />
|
||||||
|
Run this command in your terminal:
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<code className="flex-1 px-3 py-2 bg-muted rounded-md font-mono text-sm">
|
||||||
|
{authCommand}
|
||||||
|
</code>
|
||||||
|
<Button variant="outline" size="icon" onClick={() => copyCommand(authCommand)}>
|
||||||
|
{copied ? (
|
||||||
|
<Check className="w-4 h-4 text-green-500" />
|
||||||
|
) : (
|
||||||
|
<Copy className="w-4 h-4" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-muted-foreground">
|
||||||
|
This will open your browser to authenticate with{' '}
|
||||||
|
{providers.find((p) => p.id === selectedProvider)?.name}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Button variant="ghost" onClick={() => setStep('provider')}>
|
||||||
|
<ArrowLeft className="w-4 h-4 mr-2" />
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleRefresh} disabled={isRefreshing}>
|
||||||
|
<RefreshCw className={`w-4 h-4 mr-2 ${isRefreshing ? 'animate-spin' : ''}`} />
|
||||||
|
{isRefreshing ? 'Checking...' : 'I ran the command'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Step: Account Selection */}
|
||||||
|
{step === 'account' && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="grid gap-2">
|
||||||
|
{accounts.map((acc: OAuthAccount) => (
|
||||||
|
<button
|
||||||
|
key={acc.id}
|
||||||
|
onClick={() => handleAccountSelect(acc)}
|
||||||
|
className="flex items-center justify-between p-3 border rounded-lg hover:bg-muted/50 transition-colors text-left"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<User className="w-5 h-5 text-muted-foreground" />
|
||||||
|
<div>
|
||||||
|
<div className="font-medium">{acc.email || acc.id}</div>
|
||||||
|
{acc.isDefault && (
|
||||||
|
<div className="text-xs text-muted-foreground">Default account</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ChevronRight className="w-4 h-4 text-muted-foreground" />
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Button variant="ghost" onClick={() => setStep('auth')}>
|
||||||
|
<ArrowLeft className="w-4 h-4 mr-2" />
|
||||||
|
Add different account
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Step: Create Variant */}
|
||||||
|
{step === 'variant' && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{selectedAccount && (
|
||||||
|
<div className="flex items-center gap-2 p-2 bg-muted/50 rounded-md text-sm">
|
||||||
|
<User className="w-4 h-4" />
|
||||||
|
<span>Using: {selectedAccount.email || selectedAccount.id}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="variant-name">Variant Name *</Label>
|
||||||
|
<Input
|
||||||
|
id="variant-name"
|
||||||
|
value={variantName}
|
||||||
|
onChange={(e) => setVariantName(e.target.value)}
|
||||||
|
placeholder="e.g., my-gemini, g3, flash"
|
||||||
|
/>
|
||||||
|
<div className="text-xs text-muted-foreground">
|
||||||
|
Use this name to invoke: ccs {variantName || '<name>'} "prompt"
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="model-name">Model (optional)</Label>
|
||||||
|
<Input
|
||||||
|
id="model-name"
|
||||||
|
value={modelName}
|
||||||
|
onChange={(e) => setModelName(e.target.value)}
|
||||||
|
placeholder="e.g., gemini-2.5-pro"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between pt-2">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => (accounts.length > 1 ? setStep('account') : setStep('auth'))}
|
||||||
|
>
|
||||||
|
<ArrowLeft className="w-4 h-4 mr-2" />
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleCreateVariant}
|
||||||
|
disabled={!variantName || createMutation.isPending}
|
||||||
|
>
|
||||||
|
{createMutation.isPending ? 'Creating...' : 'Create Variant'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Step: Success */}
|
||||||
|
{step === 'success' && (
|
||||||
|
<div className="space-y-4 text-center">
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<div className="w-16 h-16 bg-green-100 dark:bg-green-900/30 rounded-full flex items-center justify-center">
|
||||||
|
<Check className="w-8 h-8 text-green-600 dark:text-green-400" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="font-semibold text-lg">Variant Created!</div>
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
Your custom variant is ready to use
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-4 space-y-2">
|
||||||
|
<div className="text-sm text-muted-foreground">Usage:</div>
|
||||||
|
<code className="block px-3 py-2 bg-muted rounded-md font-mono text-sm">
|
||||||
|
ccs {variantName} "your prompt here"
|
||||||
|
</code>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Button onClick={onClose} className="w-full">
|
||||||
|
Done
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Progress indicator */}
|
||||||
|
<div className="flex justify-center gap-1 pt-2">
|
||||||
|
{allSteps.map((s, i) => (
|
||||||
|
<div
|
||||||
|
key={s}
|
||||||
|
className={`w-2 h-2 rounded-full transition-colors ${
|
||||||
|
currentProgress >= i ? 'bg-primary' : 'bg-muted'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -14,9 +14,10 @@ import {
|
|||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from '@/components/ui/dropdown-menu';
|
} from '@/components/ui/dropdown-menu';
|
||||||
import { Plus, Check, X, User, ChevronDown, Star, Trash2 } from 'lucide-react';
|
import { Plus, Check, X, User, ChevronDown, Star, Trash2, Sparkles } from 'lucide-react';
|
||||||
import { CliproxyTable } from '@/components/cliproxy-table';
|
import { CliproxyTable } from '@/components/cliproxy-table';
|
||||||
import { CliproxyDialog } from '@/components/cliproxy-dialog';
|
import { CliproxyDialog } from '@/components/cliproxy-dialog';
|
||||||
|
import { QuickSetupWizard } from '@/components/quick-setup-wizard';
|
||||||
import {
|
import {
|
||||||
useCliproxy,
|
useCliproxy,
|
||||||
useCliproxyAuth,
|
useCliproxyAuth,
|
||||||
@@ -175,6 +176,7 @@ function ProviderRow({
|
|||||||
|
|
||||||
export function CliproxyPage() {
|
export function CliproxyPage() {
|
||||||
const [dialogOpen, setDialogOpen] = useState(false);
|
const [dialogOpen, setDialogOpen] = useState(false);
|
||||||
|
const [wizardOpen, setWizardOpen] = useState(false);
|
||||||
const { data, isLoading } = useCliproxy();
|
const { data, isLoading } = useCliproxy();
|
||||||
const { data: authData, isLoading: authLoading } = useCliproxyAuth();
|
const { data: authData, isLoading: authLoading } = useCliproxyAuth();
|
||||||
const setDefaultMutation = useSetDefaultAccount();
|
const setDefaultMutation = useSetDefaultAccount();
|
||||||
@@ -189,10 +191,16 @@ export function CliproxyPage() {
|
|||||||
Manage OAuth-based provider variants and multi-account configurations
|
Manage OAuth-based provider variants and multi-account configurations
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button onClick={() => setDialogOpen(true)}>
|
<div className="flex items-center gap-2">
|
||||||
<Plus className="w-4 h-4 mr-2" />
|
<Button variant="outline" onClick={() => setWizardOpen(true)}>
|
||||||
Create Variant
|
<Sparkles className="w-4 h-4 mr-2" />
|
||||||
</Button>
|
Quick Setup
|
||||||
|
</Button>
|
||||||
|
<Button onClick={() => setDialogOpen(true)}>
|
||||||
|
<Plus className="w-4 h-4 mr-2" />
|
||||||
|
Create Variant
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Built-in Profiles with Account Management */}
|
{/* Built-in Profiles with Account Management */}
|
||||||
@@ -245,6 +253,7 @@ export function CliproxyPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<CliproxyDialog open={dialogOpen} onClose={() => setDialogOpen(false)} />
|
<CliproxyDialog open={dialogOpen} onClose={() => setDialogOpen(false)} />
|
||||||
|
<QuickSetupWizard open={wizardOpen} onClose={() => setWizardOpen(false)} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user