mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 08:17:11 +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,541 +1,30 @@
|
||||
/**
|
||||
* Quick Setup Wizard Component
|
||||
* Phase 03: Multi-account dashboard support
|
||||
*
|
||||
* Step-by-step wizard: Provider -> Auth -> Account -> Variant -> Success
|
||||
* Quick Setup Wizard - Re-export from modular structure
|
||||
* @deprecated Import from '@/components/setup/wizard' directly
|
||||
*/
|
||||
|
||||
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 {
|
||||
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';
|
||||
/* eslint-disable react-refresh/only-export-components */
|
||||
export {
|
||||
QuickSetupWizard,
|
||||
ProgressIndicator,
|
||||
ProviderStep,
|
||||
AuthStep,
|
||||
AccountStep,
|
||||
VariantStep,
|
||||
SuccessStep,
|
||||
PROVIDERS,
|
||||
ALL_STEPS,
|
||||
getStepProgress,
|
||||
} from './setup/wizard';
|
||||
|
||||
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 [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>
|
||||
);
|
||||
}
|
||||
export type {
|
||||
WizardStep,
|
||||
QuickSetupWizardProps,
|
||||
ProviderOption,
|
||||
ProviderStepProps,
|
||||
AuthStepProps,
|
||||
AccountStepProps,
|
||||
VariantStepProps,
|
||||
SuccessStepProps,
|
||||
ProgressIndicatorProps,
|
||||
} from './setup/wizard';
|
||||
|
||||
@@ -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