mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-13 00:14:15 +00:00
refactor(ui): split quick-setup-wizard into setup/wizard/ directory
- extract 541-line component into step modules - wrap accounts in useMemo for stable deps - add barrel export in setup/index.ts
This commit is contained in:
1 parent
6778c4d637
commit
81196b0ff1
11 files changed
+755
-537
No files matched your search
@@ -1,541 +1,30 @@
|
|||||||
/**
|
/**
|
||||||
* Quick Setup Wizard Component
|
* Quick Setup Wizard - Re-export from modular structure
|
||||||
* Phase 03: Multi-account dashboard support
|
* @deprecated Import from '@/components/setup/wizard' directly
|
||||||
*
|
|
||||||
* Step-by-step wizard: Provider -> Auth -> Account -> Variant -> Success
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
/* eslint-disable react-refresh/only-export-components */
|
||||||
import {
|
export {
|
||||||
Dialog,
|
QuickSetupWizard,
|
||||||
DialogContent,
|
ProgressIndicator,
|
||||||
DialogHeader,
|
ProviderStep,
|
||||||
DialogTitle,
|
AuthStep,
|
||||||
DialogDescription,
|
AccountStep,
|
||||||
} from '@/components/ui/dialog';
|
VariantStep,
|
||||||
import { Button } from '@/components/ui/button';
|
SuccessStep,
|
||||||
import { Input } from '@/components/ui/input';
|
PROVIDERS,
|
||||||
import { Label } from '@/components/ui/label';
|
ALL_STEPS,
|
||||||
import { Card, CardContent } from '@/components/ui/card';
|
getStepProgress,
|
||||||
import {
|
} from './setup/wizard';
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from '@/components/ui/select';
|
|
||||||
import {
|
|
||||||
Copy,
|
|
||||||
Check,
|
|
||||||
RefreshCw,
|
|
||||||
ChevronRight,
|
|
||||||
ArrowLeft,
|
|
||||||
Terminal,
|
|
||||||
User,
|
|
||||||
Sparkles,
|
|
||||||
ExternalLink,
|
|
||||||
Loader2,
|
|
||||||
} from 'lucide-react';
|
|
||||||
import { useCliproxyAuth, useCreateVariant, useStartAuth } from '@/hooks/use-cliproxy';
|
|
||||||
import type { AuthStatus, OAuthAccount } from '@/lib/api-client';
|
|
||||||
import { MODEL_CATALOGS } from '@/lib/model-catalogs';
|
|
||||||
import { applyDefaultPreset } from '@/lib/preset-utils';
|
|
||||||
import { cn } from '@/lib/utils';
|
|
||||||
import { usePrivacy, PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context';
|
|
||||||
import { toast } from 'sonner';
|
|
||||||
|
|
||||||
interface QuickSetupWizardProps {
|
export type {
|
||||||
open: boolean;
|
WizardStep,
|
||||||
onClose: () => void;
|
QuickSetupWizardProps,
|
||||||
}
|
ProviderOption,
|
||||||
|
ProviderStepProps,
|
||||||
type WizardStep = 'provider' | 'auth' | 'account' | 'variant' | 'success';
|
AuthStepProps,
|
||||||
|
AccountStepProps,
|
||||||
const providers = [
|
VariantStepProps,
|
||||||
{ id: 'gemini', name: 'Google Gemini', description: 'Gemini Pro/Flash models' },
|
SuccessStepProps,
|
||||||
{ id: 'codex', name: 'OpenAI Codex', description: 'GPT-4 and codex models' },
|
ProgressIndicatorProps,
|
||||||
{ id: 'agy', name: 'Antigravity', description: 'Antigravity AI models' },
|
} from './setup/wizard';
|
||||||
{ 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 [isAddingNewAccount, setIsAddingNewAccount] = useState(false); // Track explicit "add account" action
|
|
||||||
|
|
||||||
const { data: authData, refetch } = useCliproxyAuth();
|
|
||||||
const createMutation = useCreateVariant();
|
|
||||||
const startAuthMutation = useStartAuth();
|
|
||||||
const { privacyMode } = usePrivacy();
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
setIsAddingNewAccount(false);
|
|
||||||
}, 0);
|
|
||||||
return () => clearTimeout(timer);
|
|
||||||
}
|
|
||||||
}, [open]);
|
|
||||||
|
|
||||||
// Auto-advance from auth step when account detected
|
|
||||||
// BUT only if user didn't explicitly click "Add new account"
|
|
||||||
useEffect(() => {
|
|
||||||
if (step === 'auth' && accounts.length > 0 && !isAddingNewAccount) {
|
|
||||||
const timer = setTimeout(() => {
|
|
||||||
setStep('account');
|
|
||||||
}, 0);
|
|
||||||
return () => clearTimeout(timer);
|
|
||||||
}
|
|
||||||
}, [step, accounts, isAddingNewAccount]);
|
|
||||||
|
|
||||||
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 handleStartAuth = () => {
|
|
||||||
// Check if this is the first account before auth
|
|
||||||
const isFirstAccount = (providerAuth?.accounts?.length || 0) === 0;
|
|
||||||
|
|
||||||
startAuthMutation.mutate(
|
|
||||||
{ provider: selectedProvider },
|
|
||||||
{
|
|
||||||
onSuccess: async (data) => {
|
|
||||||
// Apply default preset if this was the first account
|
|
||||||
if (isFirstAccount) {
|
|
||||||
const result = await applyDefaultPreset(selectedProvider);
|
|
||||||
if (result.success && result.presetName) {
|
|
||||||
toast.success(`Applied "${result.presetName}" preset`);
|
|
||||||
} else if (!result.success) {
|
|
||||||
toast.warning('Account added, but failed to apply default preset');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Account created, select it and advance to variant step
|
|
||||||
if (data.account) {
|
|
||||||
setSelectedAccount(data.account as OAuthAccount);
|
|
||||||
setStep('variant');
|
|
||||||
}
|
|
||||||
refetch(); // Refresh auth status
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleProviderSelect = (providerId: string) => {
|
|
||||||
setSelectedProvider(providerId);
|
|
||||||
const auth = authData?.authStatus.find((s: AuthStatus) => s.provider === providerId);
|
|
||||||
const provAccounts = auth?.accounts || [];
|
|
||||||
|
|
||||||
if (provAccounts.length === 0) {
|
|
||||||
// No accounts - go to auth step to add first account
|
|
||||||
setStep('auth');
|
|
||||||
} else {
|
|
||||||
// Has accounts - always show account selection (includes "Add new account" option)
|
|
||||||
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);
|
|
||||||
|
|
||||||
// Prevent accidental close when user has made progress
|
|
||||||
const handleOpenChange = (isOpen: boolean) => {
|
|
||||||
if (!isOpen) {
|
|
||||||
// Allow closing from success step or provider step (no progress yet)
|
|
||||||
if (step === 'success' || step === 'provider') {
|
|
||||||
onClose();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// For other steps, require explicit close via Cancel/Back
|
|
||||||
// The X button still works, but clicking outside doesn't close
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
|
||||||
<DialogContent
|
|
||||||
className="sm:max-w-lg"
|
|
||||||
onPointerDownOutside={(e) => {
|
|
||||||
// Prevent closing on outside click when user has made progress
|
|
||||||
if (step !== 'success' && step !== 'provider') {
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onEscapeKeyDown={(e) => {
|
|
||||||
// Prevent ESC close during auth or variant creation
|
|
||||||
if (startAuthMutation.isPending || createMutation.isPending) {
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<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">
|
|
||||||
{/* Primary: OAuth Button */}
|
|
||||||
<div className="text-center space-y-3">
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
Authenticate with {providers.find((p) => p.id === selectedProvider)?.name} to add
|
|
||||||
an account
|
|
||||||
</p>
|
|
||||||
<Button
|
|
||||||
onClick={handleStartAuth}
|
|
||||||
disabled={startAuthMutation.isPending}
|
|
||||||
className="w-full gap-2"
|
|
||||||
size="lg"
|
|
||||||
>
|
|
||||||
{startAuthMutation.isPending ? (
|
|
||||||
<>
|
|
||||||
<Loader2 className="w-4 h-4 animate-spin" />
|
|
||||||
Authenticating...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<ExternalLink className="w-4 h-4" />
|
|
||||||
Authenticate in Browser
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
{startAuthMutation.isPending && (
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
Complete the OAuth flow in your browser...
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Divider */}
|
|
||||||
<div className="relative">
|
|
||||||
<div className="absolute inset-0 flex items-center">
|
|
||||||
<span className="w-full border-t" />
|
|
||||||
</div>
|
|
||||||
<div className="relative flex justify-center text-xs uppercase">
|
|
||||||
<span className="bg-background px-2 text-muted-foreground">Or use terminal</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Secondary: CLI Command */}
|
|
||||||
<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>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
onClick={() => setStep('provider')}
|
|
||||||
disabled={startAuthMutation.isPending}
|
|
||||||
>
|
|
||||||
<ArrowLeft className="w-4 h-4 mr-2" />
|
|
||||||
Back
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
onClick={handleRefresh}
|
|
||||||
disabled={isRefreshing || startAuthMutation.isPending}
|
|
||||||
>
|
|
||||||
<RefreshCw className={`w-4 h-4 mr-2 ${isRefreshing ? 'animate-spin' : ''}`} />
|
|
||||||
{isRefreshing ? 'Checking...' : 'Refresh Status'}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Step: Account Selection */}
|
|
||||||
{step === 'account' && (
|
|
||||||
<div className="space-y-4">
|
|
||||||
{/* Existing accounts header */}
|
|
||||||
<div className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
|
|
||||||
Select an account ({accounts.length})
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid gap-2">
|
|
||||||
{accounts.map((acc: OAuthAccount) => (
|
|
||||||
<button
|
|
||||||
key={acc.id}
|
|
||||||
type="button"
|
|
||||||
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">
|
|
||||||
<div className="w-8 h-8 rounded-full bg-muted flex items-center justify-center">
|
|
||||||
<User className="w-4 h-4 text-muted-foreground" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className={cn('font-medium', privacyMode && PRIVACY_BLUR_CLASS)}>
|
|
||||||
{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>
|
|
||||||
|
|
||||||
{/* Divider */}
|
|
||||||
<div className="relative">
|
|
||||||
<div className="absolute inset-0 flex items-center">
|
|
||||||
<span className="w-full border-t" />
|
|
||||||
</div>
|
|
||||||
<div className="relative flex justify-center text-xs uppercase">
|
|
||||||
<span className="bg-background px-2 text-muted-foreground">Or</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Add new account button - more prominent */}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="w-full flex items-center gap-3 p-3 border-2 border-dashed border-primary/50 rounded-lg hover:border-primary hover:bg-primary/5 transition-colors text-left"
|
|
||||||
onClick={() => {
|
|
||||||
setIsAddingNewAccount(true);
|
|
||||||
setStep('auth');
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center shrink-0">
|
|
||||||
<ExternalLink className="w-4 h-4 text-primary" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="font-medium text-primary">Add new account</div>
|
|
||||||
<div className="text-xs text-muted-foreground">
|
|
||||||
Authenticate with a different account
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div className="flex items-center justify-between pt-2">
|
|
||||||
<Button variant="ghost" onClick={() => setStep('provider')}>
|
|
||||||
<ArrowLeft className="w-4 h-4 mr-2" />
|
|
||||||
Back
|
|
||||||
</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:{' '}
|
|
||||||
<span className={cn(privacyMode && PRIVACY_BLUR_CLASS)}>
|
|
||||||
{selectedAccount.email || selectedAccount.id}
|
|
||||||
</span>
|
|
||||||
</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>Model</Label>
|
|
||||||
<Select value={modelName} onValueChange={setModelName}>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder="Select a model" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{MODEL_CATALOGS[selectedProvider]?.models.map((m) => (
|
|
||||||
<SelectItem key={m.id} value={m.id}>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span>{m.name}</span>
|
|
||||||
{m.description && (
|
|
||||||
<span className="text-xs text-muted-foreground">- {m.description}</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<div className="text-xs text-muted-foreground">
|
|
||||||
Default: {MODEL_CATALOGS[selectedProvider]?.defaultModel || 'provider default'}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center justify-between pt-2">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
onClick={() => (accounts.length > 0 ? setStep('account') : setStep('provider'))}
|
|
||||||
>
|
|
||||||
<ArrowLeft className="w-4 h-4 mr-2" />
|
|
||||||
Back
|
|
||||||
</Button>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Button variant="ghost" onClick={onClose}>
|
|
||||||
Skip
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={handleCreateVariant}
|
|
||||||
disabled={!variantName || createMutation.isPending}
|
|
||||||
>
|
|
||||||
{createMutation.isPending ? 'Creating...' : 'Create Variant'}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p className="text-xs text-center text-muted-foreground">
|
|
||||||
Skip if you just wanted to add an account without creating a variant
|
|
||||||
</p>
|
|
||||||
</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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
/**
|
||||||
|
* Setup Components Barrel Export
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Quick setup wizard (from subdirectory)
|
||||||
|
export { QuickSetupWizard } from './wizard';
|
||||||
|
export type {
|
||||||
|
WizardStep,
|
||||||
|
QuickSetupWizardProps,
|
||||||
|
ProviderOption,
|
||||||
|
ProviderStepProps,
|
||||||
|
AuthStepProps,
|
||||||
|
AccountStepProps,
|
||||||
|
VariantStepProps,
|
||||||
|
SuccessStepProps,
|
||||||
|
ProgressIndicatorProps,
|
||||||
|
} from './wizard';
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
/**
|
||||||
|
* Constants for Quick Setup Wizard
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { ProviderOption } from './types';
|
||||||
|
|
||||||
|
export const PROVIDERS: ProviderOption[] = [
|
||||||
|
{ 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 const ALL_STEPS = ['provider', 'auth', 'variant', 'success'];
|
||||||
|
|
||||||
|
export function getStepProgress(step: string): number {
|
||||||
|
if (step === 'account') return 1; // Same as auth
|
||||||
|
return ALL_STEPS.indexOf(step);
|
||||||
|
}
|
||||||
@@ -0,0 +1,259 @@
|
|||||||
|
/**
|
||||||
|
* Quick Setup Wizard Component
|
||||||
|
* Phase 03: Multi-account dashboard support
|
||||||
|
*
|
||||||
|
* Step-by-step wizard: Provider -> Auth -> Account -> Variant -> Success
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* eslint-disable react-refresh/only-export-components */
|
||||||
|
import { useState, useEffect, useMemo } from 'react';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogDescription,
|
||||||
|
} from '@/components/ui/dialog';
|
||||||
|
import { Sparkles } from 'lucide-react';
|
||||||
|
import { useCliproxyAuth, useCreateVariant, useStartAuth } from '@/hooks/use-cliproxy';
|
||||||
|
import type { AuthStatus, OAuthAccount } from '@/lib/api-client';
|
||||||
|
import { applyDefaultPreset } from '@/lib/preset-utils';
|
||||||
|
import { usePrivacy } from '@/contexts/privacy-context';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
import { PROVIDERS, ALL_STEPS, getStepProgress } from './constants';
|
||||||
|
import { ProgressIndicator } from './progress-indicator';
|
||||||
|
import { ProviderStep } from './steps/provider-step';
|
||||||
|
import { AuthStep } from './steps/auth-step';
|
||||||
|
import { AccountStep } from './steps/account-step';
|
||||||
|
import { VariantStep } from './steps/variant-step';
|
||||||
|
import { SuccessStep } from './steps/success-step';
|
||||||
|
import type { WizardStep, QuickSetupWizardProps } from './types';
|
||||||
|
|
||||||
|
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 [isRefreshing, setIsRefreshing] = useState(false);
|
||||||
|
const [isAddingNewAccount, setIsAddingNewAccount] = useState(false);
|
||||||
|
|
||||||
|
const { data: authData, refetch } = useCliproxyAuth();
|
||||||
|
const createMutation = useCreateVariant();
|
||||||
|
const startAuthMutation = useStartAuth();
|
||||||
|
const { privacyMode } = usePrivacy();
|
||||||
|
|
||||||
|
// Get auth status for selected provider
|
||||||
|
const providerAuth = authData?.authStatus.find(
|
||||||
|
(s: AuthStatus) => s.provider === selectedProvider
|
||||||
|
);
|
||||||
|
const accounts = useMemo(() => providerAuth?.accounts || [], [providerAuth?.accounts]);
|
||||||
|
|
||||||
|
// Reset on close
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setStep('provider');
|
||||||
|
setSelectedProvider('');
|
||||||
|
setSelectedAccount(null);
|
||||||
|
setVariantName('');
|
||||||
|
setModelName('');
|
||||||
|
setIsAddingNewAccount(false);
|
||||||
|
}, 0);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
|
// Auto-advance from auth step when account detected
|
||||||
|
useEffect(() => {
|
||||||
|
if (step === 'auth' && accounts.length > 0 && !isAddingNewAccount) {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setStep('account');
|
||||||
|
}, 0);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}, [step, accounts, isAddingNewAccount]);
|
||||||
|
|
||||||
|
const handleRefresh = async () => {
|
||||||
|
setIsRefreshing(true);
|
||||||
|
await refetch();
|
||||||
|
setIsRefreshing(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleStartAuth = () => {
|
||||||
|
const isFirstAccount = (providerAuth?.accounts?.length || 0) === 0;
|
||||||
|
|
||||||
|
startAuthMutation.mutate(
|
||||||
|
{ provider: selectedProvider },
|
||||||
|
{
|
||||||
|
onSuccess: async (data) => {
|
||||||
|
if (isFirstAccount) {
|
||||||
|
const result = await applyDefaultPreset(selectedProvider);
|
||||||
|
if (result.success && result.presetName) {
|
||||||
|
toast.success(`Applied "${result.presetName}" preset`);
|
||||||
|
} else if (!result.success) {
|
||||||
|
toast.warning('Account added, but failed to apply default preset');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.account) {
|
||||||
|
setSelectedAccount(data.account as OAuthAccount);
|
||||||
|
setStep('variant');
|
||||||
|
}
|
||||||
|
refetch();
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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 {
|
||||||
|
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`;
|
||||||
|
const currentProgress = getStepProgress(step);
|
||||||
|
|
||||||
|
// Prevent accidental close when user has made progress
|
||||||
|
const handleOpenChange = (isOpen: boolean) => {
|
||||||
|
if (!isOpen) {
|
||||||
|
if (step === 'success' || step === 'provider') {
|
||||||
|
onClose();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||||
|
<DialogContent
|
||||||
|
className="sm:max-w-lg"
|
||||||
|
onPointerDownOutside={(e) => {
|
||||||
|
if (step !== 'success' && step !== 'provider') {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onEscapeKeyDown={(e) => {
|
||||||
|
if (startAuthMutation.isPending || createMutation.isPending) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<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' && (
|
||||||
|
<ProviderStep providers={PROVIDERS} onSelect={handleProviderSelect} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{step === 'auth' && (
|
||||||
|
<AuthStep
|
||||||
|
selectedProvider={selectedProvider}
|
||||||
|
providers={PROVIDERS}
|
||||||
|
authCommand={authCommand}
|
||||||
|
isRefreshing={isRefreshing}
|
||||||
|
isPending={startAuthMutation.isPending}
|
||||||
|
onBack={() => setStep('provider')}
|
||||||
|
onStartAuth={handleStartAuth}
|
||||||
|
onRefresh={handleRefresh}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{step === 'account' && (
|
||||||
|
<AccountStep
|
||||||
|
accounts={accounts}
|
||||||
|
privacyMode={privacyMode}
|
||||||
|
onSelect={handleAccountSelect}
|
||||||
|
onAddNew={() => {
|
||||||
|
setIsAddingNewAccount(true);
|
||||||
|
setStep('auth');
|
||||||
|
}}
|
||||||
|
onBack={() => setStep('provider')}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{step === 'variant' && (
|
||||||
|
<VariantStep
|
||||||
|
selectedProvider={selectedProvider}
|
||||||
|
selectedAccount={selectedAccount}
|
||||||
|
variantName={variantName}
|
||||||
|
modelName={modelName}
|
||||||
|
isPending={createMutation.isPending}
|
||||||
|
privacyMode={privacyMode}
|
||||||
|
onVariantNameChange={setVariantName}
|
||||||
|
onModelChange={setModelName}
|
||||||
|
onBack={() => (accounts.length > 0 ? setStep('account') : setStep('provider'))}
|
||||||
|
onSkip={onClose}
|
||||||
|
onCreate={handleCreateVariant}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{step === 'success' && <SuccessStep variantName={variantName} onClose={onClose} />}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ProgressIndicator currentProgress={currentProgress} allSteps={ALL_STEPS} />
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-exports
|
||||||
|
export { ProgressIndicator } from './progress-indicator';
|
||||||
|
export { ProviderStep } from './steps/provider-step';
|
||||||
|
export { AuthStep } from './steps/auth-step';
|
||||||
|
export { AccountStep } from './steps/account-step';
|
||||||
|
export { VariantStep } from './steps/variant-step';
|
||||||
|
export { SuccessStep } from './steps/success-step';
|
||||||
|
export { PROVIDERS, ALL_STEPS, getStepProgress } from './constants';
|
||||||
|
export type {
|
||||||
|
WizardStep,
|
||||||
|
QuickSetupWizardProps,
|
||||||
|
ProviderOption,
|
||||||
|
ProviderStepProps,
|
||||||
|
AuthStepProps,
|
||||||
|
AccountStepProps,
|
||||||
|
VariantStepProps,
|
||||||
|
SuccessStepProps,
|
||||||
|
ProgressIndicatorProps,
|
||||||
|
} from './types';
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
/**
|
||||||
|
* Progress Indicator Component
|
||||||
|
* Shows step dots for wizard progress
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { ProgressIndicatorProps } from './types';
|
||||||
|
|
||||||
|
export function ProgressIndicator({ currentProgress, allSteps }: ProgressIndicatorProps) {
|
||||||
|
return (
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
/**
|
||||||
|
* Account Selection Step
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { ChevronRight, ArrowLeft, User, ExternalLink } from 'lucide-react';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context';
|
||||||
|
import type { AccountStepProps } from '../types';
|
||||||
|
|
||||||
|
export function AccountStep({
|
||||||
|
accounts,
|
||||||
|
privacyMode,
|
||||||
|
onSelect,
|
||||||
|
onAddNew,
|
||||||
|
onBack,
|
||||||
|
}: AccountStepProps) {
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Existing accounts header */}
|
||||||
|
<div className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
|
||||||
|
Select an account ({accounts.length})
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-2">
|
||||||
|
{accounts.map((acc) => (
|
||||||
|
<button
|
||||||
|
key={acc.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => onSelect(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">
|
||||||
|
<div className="w-8 h-8 rounded-full bg-muted flex items-center justify-center">
|
||||||
|
<User className="w-4 h-4 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className={cn('font-medium', privacyMode && PRIVACY_BLUR_CLASS)}>
|
||||||
|
{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>
|
||||||
|
|
||||||
|
{/* Divider */}
|
||||||
|
<div className="relative">
|
||||||
|
<div className="absolute inset-0 flex items-center">
|
||||||
|
<span className="w-full border-t" />
|
||||||
|
</div>
|
||||||
|
<div className="relative flex justify-center text-xs uppercase">
|
||||||
|
<span className="bg-background px-2 text-muted-foreground">Or</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Add new account button - more prominent */}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="w-full flex items-center gap-3 p-3 border-2 border-dashed border-primary/50 rounded-lg hover:border-primary hover:bg-primary/5 transition-colors text-left"
|
||||||
|
onClick={onAddNew}
|
||||||
|
>
|
||||||
|
<div className="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center shrink-0">
|
||||||
|
<ExternalLink className="w-4 h-4 text-primary" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="font-medium text-primary">Add new account</div>
|
||||||
|
<div className="text-xs text-muted-foreground">Authenticate with a different account</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between pt-2">
|
||||||
|
<Button variant="ghost" onClick={onBack}>
|
||||||
|
<ArrowLeft className="w-4 h-4 mr-2" />
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
/**
|
||||||
|
* Authentication Step
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
|
import { Copy, Check, RefreshCw, ArrowLeft, Terminal, ExternalLink, Loader2 } from 'lucide-react';
|
||||||
|
import type { AuthStepProps } from '../types';
|
||||||
|
|
||||||
|
export function AuthStep({
|
||||||
|
selectedProvider,
|
||||||
|
providers,
|
||||||
|
authCommand,
|
||||||
|
isRefreshing,
|
||||||
|
isPending,
|
||||||
|
onBack,
|
||||||
|
onStartAuth,
|
||||||
|
onRefresh,
|
||||||
|
}: AuthStepProps) {
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
|
||||||
|
const copyCommand = async (cmd: string) => {
|
||||||
|
await navigator.clipboard.writeText(cmd);
|
||||||
|
setCopied(true);
|
||||||
|
setTimeout(() => setCopied(false), 2000);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Primary: OAuth Button */}
|
||||||
|
<div className="text-center space-y-3">
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Authenticate with {providers.find((p) => p.id === selectedProvider)?.name} to add an
|
||||||
|
account
|
||||||
|
</p>
|
||||||
|
<Button onClick={onStartAuth} disabled={isPending} className="w-full gap-2" size="lg">
|
||||||
|
{isPending ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="w-4 h-4 animate-spin" />
|
||||||
|
Authenticating...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<ExternalLink className="w-4 h-4" />
|
||||||
|
Authenticate in Browser
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
{isPending && (
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Complete the OAuth flow in your browser...
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Divider */}
|
||||||
|
<div className="relative">
|
||||||
|
<div className="absolute inset-0 flex items-center">
|
||||||
|
<span className="w-full border-t" />
|
||||||
|
</div>
|
||||||
|
<div className="relative flex justify-center text-xs uppercase">
|
||||||
|
<span className="bg-background px-2 text-muted-foreground">Or use terminal</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Secondary: CLI Command */}
|
||||||
|
<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>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Button variant="ghost" onClick={onBack} disabled={isPending}>
|
||||||
|
<ArrowLeft className="w-4 h-4 mr-2" />
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" onClick={onRefresh} disabled={isRefreshing || isPending}>
|
||||||
|
<RefreshCw className={`w-4 h-4 mr-2 ${isRefreshing ? 'animate-spin' : ''}`} />
|
||||||
|
{isRefreshing ? 'Checking...' : 'Refresh Status'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
/**
|
||||||
|
* Provider Selection Step
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ChevronRight } from 'lucide-react';
|
||||||
|
import type { ProviderStepProps } from '../types';
|
||||||
|
|
||||||
|
export function ProviderStep({ providers, onSelect }: ProviderStepProps) {
|
||||||
|
return (
|
||||||
|
<div className="grid gap-2">
|
||||||
|
{providers.map((p) => (
|
||||||
|
<button
|
||||||
|
key={p.id}
|
||||||
|
onClick={() => onSelect(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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
/**
|
||||||
|
* Success Step
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
|
import { Check } from 'lucide-react';
|
||||||
|
import type { SuccessStepProps } from '../types';
|
||||||
|
|
||||||
|
export function SuccessStep({ variantName, onClose }: SuccessStepProps) {
|
||||||
|
return (
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
/**
|
||||||
|
* Variant Creation Step
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/components/ui/select';
|
||||||
|
import { ArrowLeft, User } from 'lucide-react';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context';
|
||||||
|
import { MODEL_CATALOGS } from '@/lib/model-catalogs';
|
||||||
|
import type { VariantStepProps } from '../types';
|
||||||
|
|
||||||
|
export function VariantStep({
|
||||||
|
selectedProvider,
|
||||||
|
selectedAccount,
|
||||||
|
variantName,
|
||||||
|
modelName,
|
||||||
|
isPending,
|
||||||
|
privacyMode,
|
||||||
|
onVariantNameChange,
|
||||||
|
onModelChange,
|
||||||
|
onBack,
|
||||||
|
onSkip,
|
||||||
|
onCreate,
|
||||||
|
}: VariantStepProps) {
|
||||||
|
return (
|
||||||
|
<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:{' '}
|
||||||
|
<span className={cn(privacyMode && PRIVACY_BLUR_CLASS)}>
|
||||||
|
{selectedAccount.email || selectedAccount.id}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="variant-name">Variant Name *</Label>
|
||||||
|
<Input
|
||||||
|
id="variant-name"
|
||||||
|
value={variantName}
|
||||||
|
onChange={(e) => onVariantNameChange(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>Model</Label>
|
||||||
|
<Select value={modelName} onValueChange={onModelChange}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select a model" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{MODEL_CATALOGS[selectedProvider]?.models.map((m) => (
|
||||||
|
<SelectItem key={m.id} value={m.id}>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span>{m.name}</span>
|
||||||
|
{m.description && (
|
||||||
|
<span className="text-xs text-muted-foreground">- {m.description}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<div className="text-xs text-muted-foreground">
|
||||||
|
Default: {MODEL_CATALOGS[selectedProvider]?.defaultModel || 'provider default'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between pt-2">
|
||||||
|
<Button variant="ghost" onClick={onBack}>
|
||||||
|
<ArrowLeft className="w-4 h-4 mr-2" />
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button variant="ghost" onClick={onSkip}>
|
||||||
|
Skip
|
||||||
|
</Button>
|
||||||
|
<Button onClick={onCreate} disabled={!variantName || isPending}>
|
||||||
|
{isPending ? 'Creating...' : 'Create Variant'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-center text-muted-foreground">
|
||||||
|
Skip if you just wanted to add an account without creating a variant
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
/**
|
||||||
|
* Types for Quick Setup Wizard
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { OAuthAccount } from '@/lib/api-client';
|
||||||
|
|
||||||
|
export type WizardStep = 'provider' | 'auth' | 'account' | 'variant' | 'success';
|
||||||
|
|
||||||
|
export interface QuickSetupWizardProps {
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProviderOption {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProviderStepProps {
|
||||||
|
providers: ProviderOption[];
|
||||||
|
onSelect: (providerId: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuthStepProps {
|
||||||
|
selectedProvider: string;
|
||||||
|
providers: ProviderOption[];
|
||||||
|
authCommand: string;
|
||||||
|
isRefreshing: boolean;
|
||||||
|
isPending: boolean;
|
||||||
|
onBack: () => void;
|
||||||
|
onStartAuth: () => void;
|
||||||
|
onRefresh: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AccountStepProps {
|
||||||
|
accounts: OAuthAccount[];
|
||||||
|
privacyMode: boolean;
|
||||||
|
onSelect: (account: OAuthAccount) => void;
|
||||||
|
onAddNew: () => void;
|
||||||
|
onBack: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VariantStepProps {
|
||||||
|
selectedProvider: string;
|
||||||
|
selectedAccount: OAuthAccount | null;
|
||||||
|
variantName: string;
|
||||||
|
modelName: string;
|
||||||
|
isPending: boolean;
|
||||||
|
privacyMode: boolean;
|
||||||
|
onVariantNameChange: (value: string) => void;
|
||||||
|
onModelChange: (value: string) => void;
|
||||||
|
onBack: () => void;
|
||||||
|
onSkip: () => void;
|
||||||
|
onCreate: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SuccessStepProps {
|
||||||
|
variantName: string;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProgressIndicatorProps {
|
||||||
|
currentProgress: number;
|
||||||
|
allSteps: string[];
|
||||||
|
}
|
||||||
Reference in new issue
Block a user