add-provider-dialog.tsx 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. 'use client';
  2. import { useState } from 'react';
  3. import { Dialog, DialogContent, DialogTitle, DialogDescription } from '@/components/ui/dialog';
  4. import { Button } from '@/components/ui/button';
  5. import { Input } from '@/components/ui/input';
  6. import { Label } from '@/components/ui/label';
  7. import { Checkbox } from '@/components/ui/checkbox';
  8. import { Plus } from 'lucide-react';
  9. import { useI18n } from '@/lib/hooks/use-i18n';
  10. import { cn } from '@/lib/utils';
  11. export interface NewProviderData {
  12. name: string;
  13. type: 'openai' | 'anthropic' | 'google';
  14. baseUrl: string;
  15. icon: string;
  16. requiresApiKey: boolean;
  17. }
  18. interface AddProviderDialogProps {
  19. open: boolean;
  20. onOpenChange: (open: boolean) => void;
  21. onAdd: (provider: NewProviderData) => void;
  22. }
  23. export function AddProviderDialog({ open, onOpenChange, onAdd }: AddProviderDialogProps) {
  24. const { t } = useI18n();
  25. // Internal state
  26. const [name, setName] = useState('');
  27. const [type, setType] = useState<'openai' | 'anthropic' | 'google'>('openai');
  28. const [baseUrl, setBaseUrl] = useState('');
  29. const [icon, setIcon] = useState('');
  30. const [requiresApiKey, setRequiresApiKey] = useState(true);
  31. // Reset form when dialog closes (derived state pattern)
  32. const [prevOpen, setPrevOpen] = useState(open);
  33. if (open !== prevOpen) {
  34. setPrevOpen(open);
  35. if (!open) {
  36. setName('');
  37. setType('openai');
  38. setBaseUrl('');
  39. setIcon('');
  40. setRequiresApiKey(true);
  41. }
  42. }
  43. const handleClose = () => {
  44. onOpenChange(false);
  45. };
  46. const handleAdd = () => {
  47. onAdd({
  48. name,
  49. type,
  50. baseUrl,
  51. icon,
  52. requiresApiKey,
  53. });
  54. };
  55. return (
  56. <Dialog open={open} onOpenChange={onOpenChange}>
  57. <DialogContent className="sm:max-w-[450px]">
  58. <DialogTitle className="sr-only">{t('settings.addProviderDialog')}</DialogTitle>
  59. <DialogDescription className="sr-only">
  60. {t('settings.addProviderDescription')}
  61. </DialogDescription>
  62. <div className="space-y-4">
  63. <div className="pb-3 border-b">
  64. <h2 className="text-lg font-semibold">{t('settings.addProviderDialog')}</h2>
  65. </div>
  66. {/* Provider Name */}
  67. <div className="space-y-2">
  68. <Label>{t('settings.providerName')}</Label>
  69. <Input
  70. placeholder={t('settings.providerNamePlaceholder')}
  71. value={name}
  72. onChange={(e) => setName(e.target.value)}
  73. />
  74. </div>
  75. {/* API Mode */}
  76. <div className="space-y-2">
  77. <Label>{t('settings.providerApiMode')}</Label>
  78. <div className="grid grid-cols-3 gap-2">
  79. <button
  80. onClick={() => setType('openai')}
  81. className={cn(
  82. 'p-2 rounded-lg border text-left text-sm transition-colors',
  83. type === 'openai'
  84. ? 'bg-primary/5 border-primary/50'
  85. : 'hover:bg-muted/50 border-transparent',
  86. )}
  87. >
  88. {t('settings.apiModeOpenAI')}
  89. </button>
  90. <button
  91. onClick={() => setType('anthropic')}
  92. className={cn(
  93. 'p-2 rounded-lg border text-left text-sm transition-colors',
  94. type === 'anthropic'
  95. ? 'bg-primary/5 border-primary/50'
  96. : 'hover:bg-muted/50 border-transparent',
  97. )}
  98. >
  99. {t('settings.apiModeAnthropic')}
  100. </button>
  101. <button
  102. onClick={() => setType('google')}
  103. className={cn(
  104. 'p-2 rounded-lg border text-left text-sm transition-colors',
  105. type === 'google'
  106. ? 'bg-primary/5 border-primary/50'
  107. : 'hover:bg-muted/50 border-transparent',
  108. )}
  109. >
  110. {t('settings.apiModeGoogle')}
  111. </button>
  112. </div>
  113. </div>
  114. {/* Default Base URL */}
  115. <div className="space-y-2">
  116. <Label>{t('settings.defaultBaseUrl')}</Label>
  117. <Input
  118. type="url"
  119. placeholder="https://api.example.com/v1"
  120. value={baseUrl}
  121. onChange={(e) => setBaseUrl(e.target.value)}
  122. />
  123. </div>
  124. {/* Icon URL */}
  125. <div className="space-y-2">
  126. <Label>{t('settings.providerIcon')}</Label>
  127. <Input
  128. type="url"
  129. placeholder="https://example.com/icon.svg"
  130. value={icon}
  131. onChange={(e) => setIcon(e.target.value)}
  132. />
  133. </div>
  134. {/* Requires API Key */}
  135. <div className="flex items-center space-x-2">
  136. <Checkbox
  137. id="requires-api-key"
  138. checked={requiresApiKey}
  139. onCheckedChange={(checked) => setRequiresApiKey(checked as boolean)}
  140. />
  141. <label htmlFor="requires-api-key" className="text-sm cursor-pointer">
  142. {t('settings.requiresApiKey')}
  143. </label>
  144. </div>
  145. {/* Footer */}
  146. <div className="flex items-center justify-end gap-2 pt-3 border-t">
  147. <Button variant="outline" size="sm" onClick={handleClose}>
  148. {t('settings.cancelEdit')}
  149. </Button>
  150. <Button size="sm" onClick={handleAdd} className="gap-1.5">
  151. <Plus className="h-3.5 w-3.5" />
  152. {t('settings.addProviderButton')}
  153. </Button>
  154. </div>
  155. </div>
  156. </DialogContent>
  157. </Dialog>
  158. );
  159. }