@moontra/moonui-pro 2.8.4 → 2.8.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moontra/moonui-pro",
3
- "version": "2.8.4",
3
+ "version": "2.8.6",
4
4
  "description": "Premium React components for MoonUI - Advanced UI library with 50+ pro components including performance, interactive, and gesture components",
5
5
  "type": "module",
6
6
  "main": "dist/index.mjs",
@@ -250,7 +250,7 @@ export const MoonUIPhoneNumberInputPro = React.forwardRef<HTMLDivElement, PhoneN
250
250
  onPaste={handlePaste}
251
251
  onFocus={() => setIsFocused(true)}
252
252
  onBlur={() => setIsFocused(false)}
253
- placeholder={selectedCountry.format.replace(/x/g, '•')}
253
+ placeholder={selectedCountry?.format?.replace(/x/g, '•') || 'Enter phone number'}
254
254
  className={cn(
255
255
  "pr-10",
256
256
  error && "border-destructive",
@@ -22,6 +22,7 @@ import Typography from '@tiptap/extension-typography';
22
22
  import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight';
23
23
  import { common, createLowlight } from 'lowlight';
24
24
  import { SlashCommandsExtension } from './slash-commands-extension';
25
+ import { motion } from 'framer-motion';
25
26
 
26
27
  // Import pro access hooks
27
28
  // Note: DocsProAccess should be handled by consuming application
@@ -66,7 +67,13 @@ import {
66
67
  Palette,
67
68
  Eye,
68
69
  Edit,
69
- Lock
70
+ Lock,
71
+ Briefcase,
72
+ MessageSquare,
73
+ Heart,
74
+ GraduationCap,
75
+ Zap,
76
+ Lightbulb
70
77
  } from 'lucide-react';
71
78
  import { cn } from '../../lib/utils';
72
79
  import { Button } from '../ui/button';
@@ -94,7 +101,8 @@ import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover';
94
101
  import { MoonUIColorPickerPro as ColorPicker } from '../ui/color-picker';
95
102
  import { Slider } from '../ui/slider';
96
103
  import { toast } from '../ui/toast';
97
- // Note: AI providers should be handled by consuming application
104
+ import { Checkbox } from '../ui/checkbox';
105
+ import { createAIProvider, type AIProvider as AIProviderInterface } from '../../lib/ai-providers';
98
106
 
99
107
  // Type definitions for AI functionality
100
108
  type AIProvider = 'openai' | 'anthropic' | 'gemini' | 'claude' | 'cohere'
@@ -116,64 +124,45 @@ interface SlashCommand {
116
124
  action: (text: string) => Promise<{ text: string; error?: string }> | void
117
125
  }
118
126
 
119
- // Mock AI provider function
120
- const getAIProvider = (settings: AISettingsType) => {
121
- const generateWithPrompt = async (systemPrompt: string, userText: string) => {
122
- // Mock implementation - consuming app should provide real AI integration
123
- const result = `[${systemPrompt}] ${userText}`
124
- return { text: result, error: undefined }
125
- }
126
-
127
- return {
128
- generateText: async (prompt: string) => {
129
- const result = await generateWithPrompt('Generate text', prompt)
130
- return result.text
131
- },
132
- rewrite: async (text: string) => {
133
- const result = await generateWithPrompt('Rewrite this text', text)
134
- return result
135
- },
136
- expand: async (text: string) => {
137
- const result = await generateWithPrompt('Expand this text', text)
138
- return result
139
- },
140
- summarize: async (text: string) => {
141
- const result = await generateWithPrompt('Summarize this text', text)
142
- return result
143
- },
144
- fixGrammar: async (text: string) => {
145
- const result = await generateWithPrompt('Fix grammar and spelling', text)
146
- return result
147
- },
148
- translate: async (text: string, targetLang: string) => {
149
- const result = await generateWithPrompt(`Translate to ${targetLang}`, text)
150
- return result
151
- },
152
- changeTone: async (text: string, tone: string) => {
153
- const result = await generateWithPrompt(`Change tone to ${tone}`, text)
154
- return result
155
- },
156
- continueWriting: async (text: string) => {
157
- const result = await generateWithPrompt('Continue writing', text)
158
- return result
159
- },
160
- improveWriting: async (text: string) => {
161
- const result = await generateWithPrompt('Improve writing quality', text)
162
- return result
163
- },
164
- generateIdeas: async (text: string) => {
165
- const result = await generateWithPrompt('Generate writing ideas', text)
166
- return result
167
- },
168
- complete: async (text: string) => {
169
- const result = await generateWithPrompt('Complete this text', text)
170
- return result
171
- }
127
+ // Get AI provider instance
128
+ const getAIProvider = (settings: AISettingsType): AIProviderInterface | null => {
129
+ if (!settings.apiKey) return null;
130
+
131
+ try {
132
+ // Map provider names to supported ones
133
+ const providerMap: Record<string, 'openai' | 'gemini' | 'claude'> = {
134
+ 'openai': 'openai',
135
+ 'gemini': 'gemini',
136
+ 'claude': 'claude',
137
+ 'anthropic': 'claude', // Map anthropic to claude
138
+ 'cohere': 'openai' // Use OpenAI as fallback for unsupported providers
139
+ };
140
+
141
+ const mappedProvider = providerMap[settings.provider] || 'openai';
142
+
143
+ return createAIProvider(mappedProvider, {
144
+ apiKey: settings.apiKey,
145
+ model: settings.model,
146
+ temperature: settings.temperature,
147
+ maxTokens: settings.maxTokens
148
+ });
149
+ } catch (error) {
150
+ console.error('Failed to create AI provider:', error);
151
+ return null;
172
152
  }
173
153
  }
174
154
  import './slash-commands.css';
175
155
  import './table-styles.css';
176
156
 
157
+ export interface AIConfig {
158
+ provider?: 'openai' | 'claude' | 'gemini' | 'cohere';
159
+ apiKey?: string;
160
+ model?: string;
161
+ temperature?: number;
162
+ maxTokens?: number;
163
+ endpoint?: string; // For server-side proxy
164
+ }
165
+
177
166
  interface RichTextEditorProps {
178
167
  value?: string;
179
168
  onChange?: (value: string) => void;
@@ -196,13 +185,8 @@ interface RichTextEditorProps {
196
185
  color?: boolean;
197
186
  ai?: boolean;
198
187
  };
199
- aiConfig?: {
200
- provider?: 'openai' | 'claude' | 'gemini' | 'cohere';
201
- apiKey?: string;
202
- model?: string;
203
- temperature?: number;
204
- maxTokens?: number;
205
- };
188
+ aiConfig?: AIConfig;
189
+ persistAISettings?: boolean;
206
190
  }
207
191
 
208
192
 
@@ -298,10 +282,11 @@ export function RichTextEditor({
298
282
  aiConfig = {
299
283
  provider: 'openai',
300
284
  apiKey: '',
301
- model: 'gpt-4',
285
+ model: 'gpt-3.5-turbo',
302
286
  temperature: 0.7,
303
287
  maxTokens: 1000,
304
288
  },
289
+ persistAISettings = true,
305
290
  }: RichTextEditorProps) {
306
291
  // Pro access kontrolü
307
292
  const { hasProAccess, isLoading } = useSubscription();
@@ -337,12 +322,35 @@ export function RichTextEditor({
337
322
  );
338
323
  }
339
324
 
340
- const [aiSettings, setAiSettings] = useState<AISettingsType>({
341
- provider: aiConfig.provider || 'openai',
342
- apiKey: aiConfig.apiKey || '',
343
- model: aiConfig.model || 'gpt-4',
344
- temperature: aiConfig.temperature || 0.7,
345
- maxTokens: aiConfig.maxTokens || 1000,
325
+ const [aiSettings, setAiSettings] = useState<AISettingsType>(() => {
326
+ // Öncelik sırası: Props > LocalStorage > Varsayılan
327
+ if (persistAISettings) {
328
+ try {
329
+ const stored = localStorage.getItem('moonui-ai-settings');
330
+ if (stored) {
331
+ const parsed = JSON.parse(stored);
332
+ // Props'tan gelen değerler her zaman öncelikli
333
+ return {
334
+ provider: aiConfig.provider || parsed.provider || 'openai',
335
+ apiKey: aiConfig.apiKey || parsed.apiKey || '',
336
+ model: aiConfig.model || parsed.model || 'gpt-3.5-turbo',
337
+ temperature: aiConfig.temperature ?? parsed.temperature ?? 0.7,
338
+ maxTokens: aiConfig.maxTokens ?? parsed.maxTokens ?? 1000,
339
+ };
340
+ }
341
+ } catch (e) {
342
+ console.error('Failed to load AI settings from localStorage:', e);
343
+ }
344
+ }
345
+
346
+ // LocalStorage yoksa veya persist kapalıysa props/varsayılan değerleri kullan
347
+ return {
348
+ provider: aiConfig.provider || 'openai',
349
+ apiKey: aiConfig.apiKey || '',
350
+ model: aiConfig.model || 'gpt-3.5-turbo',
351
+ temperature: aiConfig.temperature ?? 0.7,
352
+ maxTokens: aiConfig.maxTokens ?? 1000,
353
+ };
346
354
  });
347
355
  const [isAiSettingsOpen, setIsAiSettingsOpen] = useState(false);
348
356
  const [isProcessing, setIsProcessing] = useState(false);
@@ -557,7 +565,11 @@ export function RichTextEditor({
557
565
  setIsProcessing(true);
558
566
  try {
559
567
  const provider = getAIProvider(aiSettings);
560
- let response;
568
+ if (!provider) {
569
+ throw new Error('Failed to initialize AI provider');
570
+ }
571
+
572
+ let response: string;
561
573
 
562
574
  switch (action) {
563
575
  case 'rewrite':
@@ -573,7 +585,6 @@ export function RichTextEditor({
573
585
  response = await provider.fixGrammar(text);
574
586
  break;
575
587
  case 'translate':
576
- // TODO: Kullanıcıdan hedef dil seçmesini iste
577
588
  response = await provider.translate(text, 'Turkish');
578
589
  break;
579
590
  case 'tone_professional':
@@ -601,20 +612,11 @@ export function RichTextEditor({
601
612
  response = await provider.complete(text);
602
613
  }
603
614
 
604
- if (response.error) {
605
- toast({
606
- title: "AI Error",
607
- description: response.error,
608
- variant: "destructive",
609
- });
610
- return null;
611
- }
612
-
613
- return response.text;
614
- } catch {
615
+ return response;
616
+ } catch (error) {
615
617
  toast({
616
618
  title: "AI Error",
617
- description: "Failed to process with AI. Please check your settings.",
619
+ description: error instanceof Error ? error.message : "Failed to process with AI",
618
620
  variant: "destructive",
619
621
  });
620
622
  return null;
@@ -638,17 +640,52 @@ export function RichTextEditor({
638
640
  return;
639
641
  }
640
642
 
643
+ // Show processing toast
644
+ const processingToast = toast({
645
+ title: "Processing with AI...",
646
+ description: getActionDescription(action),
647
+ duration: 60000, // Long duration
648
+ });
649
+
641
650
  const result = await callAI(action, selectedText || editor.getText());
642
651
 
652
+ // Dismiss processing toast
653
+ processingToast.dismiss();
654
+
643
655
  if (result) {
644
656
  if (selectedText) {
645
657
  editor.chain().focus().deleteSelection().insertContent(result).run();
646
658
  } else {
647
659
  editor.chain().focus().insertContent(result).run();
648
660
  }
661
+
662
+ // Success toast
663
+ toast({
664
+ title: "AI action completed",
665
+ description: "Your text has been updated successfully.",
666
+ });
649
667
  }
650
668
  };
651
669
 
670
+ const getActionDescription = (action: string): string => {
671
+ const descriptions: Record<string, string> = {
672
+ rewrite: "Rewriting your text...",
673
+ improve: "Improving your writing...",
674
+ expand: "Expanding your text...",
675
+ summarize: "Creating a summary...",
676
+ fix: "Fixing grammar and spelling...",
677
+ translate: "Translating to Turkish...",
678
+ tone_professional: "Making text professional...",
679
+ tone_casual: "Making text casual...",
680
+ tone_friendly: "Making text friendly...",
681
+ tone_formal: "Making text formal...",
682
+ continue: "Continuing your writing...",
683
+ ideas: "Generating ideas...",
684
+ complete: "Completing your text..."
685
+ };
686
+ return descriptions[action] || "Processing...";
687
+ };
688
+
652
689
  const [linkUrl, setLinkUrl] = useState('');
653
690
  const [imageUrl, setImageUrl] = useState('');
654
691
  const [isLinkDialogOpen, setIsLinkDialogOpen] = useState(false);
@@ -1250,7 +1287,7 @@ export function RichTextEditor({
1250
1287
  <Button
1251
1288
  variant="ghost"
1252
1289
  size="sm"
1253
- className="h-8 px-3 bg-purple-100 hover:bg-purple-200 dark:bg-purple-900 dark:hover:bg-purple-800"
1290
+ className="h-8 px-3 bg-purple-100 hover:bg-purple-200 dark:bg-purple-900 dark:hover:bg-purple-800 transition-colors"
1254
1291
  disabled={isProcessing}
1255
1292
  >
1256
1293
  {isProcessing ? (
@@ -1261,58 +1298,134 @@ export function RichTextEditor({
1261
1298
  AI Tools
1262
1299
  </Button>
1263
1300
  </DropdownMenuTrigger>
1264
- <DropdownMenuContent className="w-56">
1265
- <DropdownMenuItem onClick={() => handleAIAction('rewrite')}>
1301
+ <DropdownMenuContent className="w-64">
1302
+ <div className="px-2 py-1.5 text-xs font-semibold text-muted-foreground flex items-center gap-2">
1303
+ <Wand2 className="w-3 h-3" />
1304
+ Writing Improvements
1305
+ </div>
1306
+ <DropdownMenuItem
1307
+ onClick={() => handleAIAction('rewrite')}
1308
+ disabled={isProcessing}
1309
+ >
1266
1310
  <RefreshCw className="w-4 h-4 mr-2" />
1267
1311
  Rewrite Selection
1312
+ <span className="ml-auto text-xs text-muted-foreground">Alt+R</span>
1268
1313
  </DropdownMenuItem>
1269
- <DropdownMenuItem onClick={() => handleAIAction('improve')}>
1270
- <Wand2 className="w-4 h-4 mr-2" />
1314
+ <DropdownMenuItem
1315
+ onClick={() => handleAIAction('improve')}
1316
+ disabled={isProcessing}
1317
+ >
1318
+ <Sparkles className="w-4 h-4 mr-2" />
1271
1319
  Improve Writing
1272
1320
  </DropdownMenuItem>
1273
- <DropdownMenuItem onClick={() => handleAIAction('expand')}>
1321
+ <DropdownMenuItem
1322
+ onClick={() => handleAIAction('expand')}
1323
+ disabled={isProcessing}
1324
+ >
1274
1325
  <Maximize className="w-4 h-4 mr-2" />
1275
1326
  Expand Text
1276
1327
  </DropdownMenuItem>
1277
- <DropdownMenuItem onClick={() => handleAIAction('summarize')}>
1328
+ <DropdownMenuItem
1329
+ onClick={() => handleAIAction('summarize')}
1330
+ disabled={isProcessing}
1331
+ >
1278
1332
  <FileText className="w-4 h-4 mr-2" />
1279
1333
  Summarize
1280
1334
  </DropdownMenuItem>
1281
- <DropdownMenuItem onClick={() => handleAIAction('continue')}>
1335
+ <DropdownMenuItem
1336
+ onClick={() => handleAIAction('continue')}
1337
+ disabled={isProcessing}
1338
+ >
1282
1339
  <Plus className="w-4 h-4 mr-2" />
1283
1340
  Continue Writing
1284
1341
  </DropdownMenuItem>
1342
+
1285
1343
  <DropdownMenuSeparator />
1286
- <DropdownMenuItem onClick={() => handleAIAction('fix')}>
1287
- <Check className="w-4 h-4 mr-2" />
1288
- Fix Grammar & Spelling
1289
- </DropdownMenuItem>
1290
- <DropdownMenuSeparator />
1291
- <DropdownMenuItem onClick={() => handleAIAction('tone_professional')}>
1292
- <Sparkles className="w-4 h-4 mr-2" />
1344
+ <div className="px-2 py-1.5 text-xs font-semibold text-muted-foreground flex items-center gap-2">
1345
+ <Palette className="w-3 h-3" />
1346
+ Tone Adjustments
1347
+ </div>
1348
+ <DropdownMenuItem
1349
+ onClick={() => handleAIAction('tone_professional')}
1350
+ disabled={isProcessing}
1351
+ >
1352
+ <Briefcase className="w-4 h-4 mr-2" />
1293
1353
  Make Professional
1294
1354
  </DropdownMenuItem>
1295
- <DropdownMenuItem onClick={() => handleAIAction('tone_casual')}>
1296
- <Sparkles className="w-4 h-4 mr-2" />
1355
+ <DropdownMenuItem
1356
+ onClick={() => handleAIAction('tone_casual')}
1357
+ disabled={isProcessing}
1358
+ >
1359
+ <MessageSquare className="w-4 h-4 mr-2" />
1297
1360
  Make Casual
1298
1361
  </DropdownMenuItem>
1299
- <DropdownMenuItem onClick={() => handleAIAction('tone_friendly')}>
1300
- <Sparkles className="w-4 h-4 mr-2" />
1362
+ <DropdownMenuItem
1363
+ onClick={() => handleAIAction('tone_friendly')}
1364
+ disabled={isProcessing}
1365
+ >
1366
+ <Heart className="w-4 h-4 mr-2" />
1301
1367
  Make Friendly
1302
1368
  </DropdownMenuItem>
1303
- <DropdownMenuItem onClick={() => handleAIAction('tone_formal')}>
1304
- <Sparkles className="w-4 h-4 mr-2" />
1369
+ <DropdownMenuItem
1370
+ onClick={() => handleAIAction('tone_formal')}
1371
+ disabled={isProcessing}
1372
+ >
1373
+ <GraduationCap className="w-4 h-4 mr-2" />
1305
1374
  Make Formal
1306
1375
  </DropdownMenuItem>
1376
+
1307
1377
  <DropdownMenuSeparator />
1308
- <DropdownMenuItem onClick={() => handleAIAction('translate')}>
1378
+ <div className="px-2 py-1.5 text-xs font-semibold text-muted-foreground flex items-center gap-2">
1379
+ <Zap className="w-3 h-3" />
1380
+ Other Actions
1381
+ </div>
1382
+ <DropdownMenuItem
1383
+ onClick={() => handleAIAction('fix')}
1384
+ disabled={isProcessing}
1385
+ >
1386
+ <Check className="w-4 h-4 mr-2" />
1387
+ Fix Grammar & Spelling
1388
+ <span className="ml-auto text-xs text-muted-foreground">F7</span>
1389
+ </DropdownMenuItem>
1390
+ <DropdownMenuItem
1391
+ onClick={() => handleAIAction('translate')}
1392
+ disabled={isProcessing}
1393
+ >
1309
1394
  <Languages className="w-4 h-4 mr-2" />
1310
1395
  Translate to Turkish
1311
1396
  </DropdownMenuItem>
1312
- <DropdownMenuItem onClick={() => handleAIAction('ideas')}>
1313
- <Sparkles className="w-4 h-4 mr-2" />
1397
+ <DropdownMenuItem
1398
+ onClick={() => handleAIAction('ideas')}
1399
+ disabled={isProcessing}
1400
+ >
1401
+ <Lightbulb className="w-4 h-4 mr-2" />
1314
1402
  Generate Ideas
1315
1403
  </DropdownMenuItem>
1404
+
1405
+ {!aiSettings.apiKey && (
1406
+ <>
1407
+ <DropdownMenuSeparator />
1408
+ <div className="px-2 py-2">
1409
+ <motion.div
1410
+ initial={{ opacity: 0, y: -10 }}
1411
+ animate={{ opacity: 1, y: 0 }}
1412
+ className="text-xs text-muted-foreground bg-yellow-100 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-md p-3"
1413
+ >
1414
+ <div className="flex items-start gap-2">
1415
+ <Settings className="w-3 h-3 mt-0.5 text-yellow-600 dark:text-yellow-400" />
1416
+ <div>
1417
+ <div className="font-medium text-yellow-900 dark:text-yellow-200 mb-1">
1418
+ API Key Required
1419
+ </div>
1420
+ <div className="text-yellow-800 dark:text-yellow-300">
1421
+ Click the settings icon to configure your AI provider and API key.
1422
+ </div>
1423
+ </div>
1424
+ </div>
1425
+ </motion.div>
1426
+ </div>
1427
+ </>
1428
+ )}
1316
1429
  </DropdownMenuContent>
1317
1430
  </DropdownMenu>
1318
1431
 
@@ -1338,7 +1451,20 @@ export function RichTextEditor({
1338
1451
  <Label htmlFor="provider">Provider</Label>
1339
1452
  <Select
1340
1453
  value={aiSettings.provider}
1341
- onValueChange={(value: 'openai' | 'claude' | 'gemini' | 'cohere') => setAiSettings({ ...aiSettings, provider: value })}
1454
+ onValueChange={(value: 'openai' | 'claude' | 'gemini' | 'cohere') => {
1455
+ // Update model when provider changes
1456
+ const defaultModels = {
1457
+ openai: 'gpt-3.5-turbo',
1458
+ claude: 'claude-3-sonnet-20240229',
1459
+ gemini: 'gemini-2.0-flash',
1460
+ cohere: 'command'
1461
+ };
1462
+ setAiSettings({
1463
+ ...aiSettings,
1464
+ provider: value,
1465
+ model: defaultModels[value] || 'gpt-3.5-turbo'
1466
+ });
1467
+ }}
1342
1468
  >
1343
1469
  <SelectTrigger>
1344
1470
  <SelectValue />
@@ -1386,8 +1512,9 @@ export function RichTextEditor({
1386
1512
  )}
1387
1513
  {aiSettings.provider === 'gemini' && (
1388
1514
  <>
1389
- <SelectItem value="gemini-pro">Gemini Pro</SelectItem>
1390
- <SelectItem value="gemini-pro-vision">Gemini Pro Vision</SelectItem>
1515
+ <SelectItem value="gemini-2.0-flash">Gemini 2.0 Flash</SelectItem>
1516
+ <SelectItem value="gemini-1.5-flash">Gemini 1.5 Flash</SelectItem>
1517
+ <SelectItem value="gemini-1.5-pro">Gemini 1.5 Pro</SelectItem>
1391
1518
  </>
1392
1519
  )}
1393
1520
  {aiSettings.provider === 'cohere' && (
@@ -1425,8 +1552,64 @@ export function RichTextEditor({
1425
1552
  </div>
1426
1553
  </div>
1427
1554
  </div>
1428
- <div className="flex justify-end">
1555
+ {persistAISettings && (
1556
+ <div className="space-y-4">
1557
+ <div className="flex items-start space-x-2">
1558
+ <Checkbox
1559
+ id="rememberSettings"
1560
+ defaultChecked
1561
+ onCheckedChange={(checked) => {
1562
+ if (!checked) {
1563
+ localStorage.removeItem('moonui-ai-settings');
1564
+ }
1565
+ }}
1566
+ />
1567
+ <div className="grid gap-1.5 leading-none">
1568
+ <Label
1569
+ htmlFor="rememberSettings"
1570
+ className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
1571
+ >
1572
+ Remember my settings
1573
+ </Label>
1574
+ <p className="text-xs text-muted-foreground">
1575
+ Save settings locally for future sessions
1576
+ </p>
1577
+ </div>
1578
+ </div>
1579
+
1580
+ {aiSettings.apiKey && (
1581
+ <div className="rounded-md bg-yellow-50 dark:bg-yellow-900/10 p-3">
1582
+ <div className="flex">
1583
+ <div className="flex-shrink-0">
1584
+ <Settings className="h-4 w-4 text-yellow-400" />
1585
+ </div>
1586
+ <div className="ml-3">
1587
+ <p className="text-xs text-yellow-800 dark:text-yellow-200">
1588
+ <strong>Security Notice:</strong> API keys will be stored in your browser's local storage.
1589
+ For production use, consider using a server-side proxy.
1590
+ </p>
1591
+ </div>
1592
+ </div>
1593
+ </div>
1594
+ )}
1595
+ </div>
1596
+ )}
1597
+
1598
+ <div className="flex justify-end gap-2">
1599
+ <Button
1600
+ variant="outline"
1601
+ onClick={() => setIsAiSettingsOpen(false)}
1602
+ >
1603
+ Cancel
1604
+ </Button>
1429
1605
  <Button onClick={() => {
1606
+ // LocalStorage'a kaydet
1607
+ if (persistAISettings) {
1608
+ const toStore = { ...aiSettings };
1609
+ // Güvenlik için API key'i opsiyonel olarak kaydet
1610
+ localStorage.setItem('moonui-ai-settings', JSON.stringify(toStore));
1611
+ }
1612
+
1430
1613
  setIsAiSettingsOpen(false);
1431
1614
  toast({
1432
1615
  title: "Settings saved",