@moontra/moonui-pro 2.20.2 → 2.20.4

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.
Files changed (153) hide show
  1. package/package.json +8 -3
  2. package/plugin/index.d.ts +86 -0
  3. package/plugin/index.js +308 -0
  4. package/scripts/postinstall.js +191 -23
  5. package/src/components/advanced-chart/index.tsx +0 -1246
  6. package/src/components/advanced-forms/index.tsx +0 -585
  7. package/src/components/animated-button/index.tsx +0 -385
  8. package/src/components/calendar/event-dialog.tsx +0 -377
  9. package/src/components/calendar/index.tsx +0 -1220
  10. package/src/components/calendar-pro/index.tsx +0 -1697
  11. package/src/components/color-picker/index.tsx +0 -432
  12. package/src/components/credit-card-input/index.tsx +0 -406
  13. package/src/components/dashboard/dashboard-grid.tsx +0 -480
  14. package/src/components/dashboard/demo.tsx +0 -425
  15. package/src/components/dashboard/index.tsx +0 -1046
  16. package/src/components/dashboard/time-range-picker.tsx +0 -336
  17. package/src/components/dashboard/types.ts +0 -225
  18. package/src/components/dashboard/widgets/activity-feed.tsx +0 -349
  19. package/src/components/dashboard/widgets/chart-widget.tsx +0 -418
  20. package/src/components/dashboard/widgets/comparison-widget.tsx +0 -177
  21. package/src/components/dashboard/widgets/index.ts +0 -5
  22. package/src/components/dashboard/widgets/metric-card.tsx +0 -363
  23. package/src/components/dashboard/widgets/progress-widget.tsx +0 -113
  24. package/src/components/data-table/data-table-bulk-actions.tsx +0 -204
  25. package/src/components/data-table/data-table-column-toggle.tsx +0 -169
  26. package/src/components/data-table/data-table-export.ts +0 -156
  27. package/src/components/data-table/data-table-filter-drawer.tsx +0 -448
  28. package/src/components/data-table/index.tsx +0 -845
  29. package/src/components/draggable-list/index.tsx +0 -100
  30. package/src/components/error-boundary/index.tsx +0 -232
  31. package/src/components/file-upload/index.tsx +0 -1660
  32. package/src/components/floating-action-button/index.tsx +0 -206
  33. package/src/components/form-wizard/form-wizard-context.tsx +0 -335
  34. package/src/components/form-wizard/form-wizard-navigation.tsx +0 -118
  35. package/src/components/form-wizard/form-wizard-progress.tsx +0 -329
  36. package/src/components/form-wizard/form-wizard-step.tsx +0 -111
  37. package/src/components/form-wizard/index.tsx +0 -102
  38. package/src/components/form-wizard/types.ts +0 -77
  39. package/src/components/gesture-drawer/index.tsx +0 -551
  40. package/src/components/github-stars/github-api.ts +0 -426
  41. package/src/components/github-stars/hooks.ts +0 -517
  42. package/src/components/github-stars/index.tsx +0 -375
  43. package/src/components/github-stars/types.ts +0 -148
  44. package/src/components/github-stars/variants.tsx +0 -515
  45. package/src/components/health-check/index.tsx +0 -439
  46. package/src/components/hover-card-3d/index.tsx +0 -529
  47. package/src/components/index.ts +0 -130
  48. package/src/components/internal/index.ts +0 -78
  49. package/src/components/kanban/add-card-modal.tsx +0 -502
  50. package/src/components/kanban/card-detail-modal.tsx +0 -761
  51. package/src/components/kanban/index.ts +0 -13
  52. package/src/components/kanban/kanban.tsx +0 -1689
  53. package/src/components/kanban/types.ts +0 -168
  54. package/src/components/lazy-component/index.tsx +0 -823
  55. package/src/components/license-error/index.tsx +0 -31
  56. package/src/components/magnetic-button/index.tsx +0 -216
  57. package/src/components/memory-efficient-data/index.tsx +0 -1018
  58. package/src/components/moonui-quiz-form/index.tsx +0 -817
  59. package/src/components/navbar/index.tsx +0 -781
  60. package/src/components/optimized-image/index.tsx +0 -425
  61. package/src/components/performance-debugger/index.tsx +0 -613
  62. package/src/components/performance-monitor/index.tsx +0 -808
  63. package/src/components/phone-number-input/index.tsx +0 -343
  64. package/src/components/phone-number-input/phone-number-input-simple.tsx +0 -167
  65. package/src/components/pinch-zoom/index.tsx +0 -566
  66. package/src/components/quiz-form/index.tsx +0 -479
  67. package/src/components/rich-text-editor/index.tsx +0 -2322
  68. package/src/components/rich-text-editor/slash-commands-extension.ts +0 -230
  69. package/src/components/rich-text-editor/slash-commands.css +0 -35
  70. package/src/components/rich-text-editor/table-styles.css +0 -65
  71. package/src/components/sidebar/index.tsx +0 -884
  72. package/src/components/spotlight-card/index.tsx +0 -191
  73. package/src/components/swipeable-card/index.tsx +0 -100
  74. package/src/components/timeline/index.tsx +0 -1183
  75. package/src/components/ui/accordion.tsx +0 -581
  76. package/src/components/ui/alert-dialog.tsx +0 -141
  77. package/src/components/ui/alert.tsx +0 -141
  78. package/src/components/ui/aspect-ratio.tsx +0 -245
  79. package/src/components/ui/avatar.tsx +0 -155
  80. package/src/components/ui/badge.tsx +0 -230
  81. package/src/components/ui/breadcrumb.tsx +0 -216
  82. package/src/components/ui/button.tsx +0 -228
  83. package/src/components/ui/calendar.tsx +0 -387
  84. package/src/components/ui/card.tsx +0 -216
  85. package/src/components/ui/checkbox.tsx +0 -259
  86. package/src/components/ui/collapsible.tsx +0 -631
  87. package/src/components/ui/color-picker.tsx +0 -97
  88. package/src/components/ui/command.tsx +0 -948
  89. package/src/components/ui/dialog.tsx +0 -752
  90. package/src/components/ui/dropdown-menu.tsx +0 -706
  91. package/src/components/ui/gesture-drawer.tsx +0 -11
  92. package/src/components/ui/hover-card.tsx +0 -29
  93. package/src/components/ui/index.ts +0 -222
  94. package/src/components/ui/input.tsx +0 -224
  95. package/src/components/ui/label.tsx +0 -29
  96. package/src/components/ui/lightbox.tsx +0 -606
  97. package/src/components/ui/magnetic-button.tsx +0 -129
  98. package/src/components/ui/media-gallery.tsx +0 -611
  99. package/src/components/ui/navigation-menu.tsx +0 -130
  100. package/src/components/ui/pagination.tsx +0 -125
  101. package/src/components/ui/popover.tsx +0 -185
  102. package/src/components/ui/progress.tsx +0 -30
  103. package/src/components/ui/radio-group.tsx +0 -257
  104. package/src/components/ui/scroll-area.tsx +0 -47
  105. package/src/components/ui/select.tsx +0 -378
  106. package/src/components/ui/separator.tsx +0 -145
  107. package/src/components/ui/sheet.tsx +0 -139
  108. package/src/components/ui/skeleton.tsx +0 -20
  109. package/src/components/ui/slider.tsx +0 -354
  110. package/src/components/ui/spotlight-card.tsx +0 -119
  111. package/src/components/ui/switch.tsx +0 -86
  112. package/src/components/ui/table.tsx +0 -331
  113. package/src/components/ui/tabs-pro.tsx +0 -542
  114. package/src/components/ui/tabs.tsx +0 -54
  115. package/src/components/ui/textarea.tsx +0 -28
  116. package/src/components/ui/toast.tsx +0 -317
  117. package/src/components/ui/toggle.tsx +0 -119
  118. package/src/components/ui/tooltip.tsx +0 -151
  119. package/src/components/virtual-list/index.tsx +0 -668
  120. package/src/hooks/use-chart.ts +0 -205
  121. package/src/hooks/use-data-table.ts +0 -182
  122. package/src/hooks/use-docs-pro-access.ts +0 -13
  123. package/src/hooks/use-license-check.ts +0 -65
  124. package/src/hooks/use-subscription.ts +0 -19
  125. package/src/hooks/use-toast.ts +0 -15
  126. package/src/index.ts +0 -22
  127. package/src/lib/ai-providers.ts +0 -377
  128. package/src/lib/component-metadata.ts +0 -18
  129. package/src/lib/micro-interactions.ts +0 -255
  130. package/src/lib/paddle.ts +0 -17
  131. package/src/lib/utils.ts +0 -6
  132. package/src/patterns/login-form/index.tsx +0 -276
  133. package/src/patterns/login-form/types.ts +0 -67
  134. package/src/setupTests.ts +0 -41
  135. package/src/styles/advanced-chart.css +0 -239
  136. package/src/styles/calendar.css +0 -35
  137. package/src/styles/design-system.css +0 -363
  138. package/src/styles/index.css +0 -681
  139. package/src/styles/tailwind.css +0 -7
  140. package/src/styles/tokens.css +0 -455
  141. package/src/types/next-auth.d.ts +0 -21
  142. package/src/use-intersection-observer.tsx +0 -154
  143. package/src/use-local-storage.tsx +0 -71
  144. package/src/use-paddle.ts +0 -138
  145. package/src/use-performance-optimizer.ts +0 -389
  146. package/src/use-pro-access.ts +0 -141
  147. package/src/use-scroll-animation.ts +0 -219
  148. package/src/use-subscription.ts +0 -37
  149. package/src/use-toast.ts +0 -32
  150. package/src/utils/chart-helpers.ts +0 -357
  151. package/src/utils/cn.ts +0 -6
  152. package/src/utils/data-processing.ts +0 -151
  153. package/src/utils/license-validator.tsx +0 -183
@@ -1,2322 +0,0 @@
1
- "use client";
2
-
3
- import React, { useState, useRef, useEffect } from 'react';
4
- import { useEditor, EditorContent } from '@tiptap/react';
5
- import StarterKit from '@tiptap/starter-kit';
6
- import Placeholder from '@tiptap/extension-placeholder';
7
- import TextAlign from '@tiptap/extension-text-align';
8
- import Highlight from '@tiptap/extension-highlight';
9
- import Link from '@tiptap/extension-link';
10
- import Image from '@tiptap/extension-image';
11
- import { Table } from '@tiptap/extension-table';
12
- import TableRow from '@tiptap/extension-table-row';
13
- import TableCell from '@tiptap/extension-table-cell';
14
- import TableHeader from '@tiptap/extension-table-header';
15
- import Color from '@tiptap/extension-color';
16
- import { TextStyle } from '@tiptap/extension-text-style';
17
- import UnderlineExtension from '@tiptap/extension-underline';
18
- import Gapcursor from '@tiptap/extension-gapcursor';
19
- import TaskList from '@tiptap/extension-task-list';
20
- import TaskItem from '@tiptap/extension-task-item';
21
- import Typography from '@tiptap/extension-typography';
22
- import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight';
23
- import { common, createLowlight } from 'lowlight';
24
- import { SlashCommandsExtension, type SlashCommand } from './slash-commands-extension';
25
- import { motion } from 'framer-motion';
26
-
27
- // Import pro access hooks
28
- // Note: DocsProAccess should be handled by consuming application
29
- import { useSubscription } from '../../hooks/use-subscription';
30
- import { Card, CardContent } from '../ui/card';
31
-
32
- const lowlight = createLowlight(common);
33
- import {
34
- Bold,
35
- Italic,
36
- Underline,
37
- Strikethrough,
38
- Heading1,
39
- Heading2,
40
- Heading3,
41
- List,
42
- ListOrdered,
43
- Quote,
44
- Code,
45
- Link2,
46
- Image as ImageIcon,
47
- Table as TableIcon,
48
- AlignLeft,
49
- AlignCenter,
50
- AlignRight,
51
- AlignJustify,
52
- Highlighter,
53
- Type,
54
- Wand2,
55
- Settings,
56
- Sparkles,
57
- ChevronDown,
58
- Plus,
59
- RefreshCw,
60
- Maximize,
61
- FileText,
62
- Languages,
63
- Check,
64
- CheckSquare,
65
- Undo,
66
- Redo,
67
- Palette,
68
- Eye,
69
- Edit,
70
- Lock,
71
- Briefcase,
72
- MessageSquare,
73
- Heart,
74
- GraduationCap,
75
- Zap,
76
- Lightbulb,
77
- X,
78
- Copy
79
- } from 'lucide-react';
80
- import { cn } from '../../lib/utils';
81
- import { Button } from '../ui/button';
82
- import {
83
- DropdownMenu,
84
- DropdownMenuContent,
85
- DropdownMenuItem,
86
- DropdownMenuSeparator,
87
- DropdownMenuTrigger,
88
- DropdownMenuSub,
89
- DropdownMenuSubContent,
90
- DropdownMenuSubTrigger,
91
- } from '../ui/dropdown-menu';
92
- import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/tooltip';
93
- import {
94
- Dialog,
95
- DialogContent,
96
- DialogDescription,
97
- DialogHeader,
98
- DialogTitle,
99
- DialogTrigger,
100
- } from '../ui/dialog';
101
- import { Input } from '../ui/input';
102
- import { Label } from '../ui/label';
103
- import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select';
104
- import { Separator } from '../ui/separator';
105
- import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover';
106
- import { MoonUIColorPickerPro as ColorPicker } from '../ui/color-picker';
107
- import { Slider } from '../ui/slider';
108
- import { toast } from '../ui/toast';
109
- import { Checkbox } from '../ui/checkbox';
110
- import { createAIProvider, type AIProvider as AIProviderInterface } from '../../lib/ai-providers';
111
-
112
- // Type definitions for AI functionality
113
- type AIProvider = 'openai' | 'anthropic' | 'gemini' | 'claude' | 'cohere'
114
-
115
- interface AISettingsType {
116
- provider: 'openai' | 'claude' | 'gemini' | 'cohere'
117
- apiKey: string
118
- model: string
119
- temperature: number
120
- maxTokens: number
121
- }
122
-
123
- // Supported languages for translation
124
- const SUPPORTED_LANGUAGES = [
125
- { code: 'en', name: 'English', nativeName: 'English' },
126
- { code: 'tr', name: 'Turkish', nativeName: 'Türkçe' },
127
- { code: 'es', name: 'Spanish', nativeName: 'Español' },
128
- { code: 'fr', name: 'French', nativeName: 'Français' },
129
- { code: 'de', name: 'German', nativeName: 'Deutsch' },
130
- { code: 'it', name: 'Italian', nativeName: 'Italiano' },
131
- { code: 'pt', name: 'Portuguese', nativeName: 'Português' },
132
- { code: 'ru', name: 'Russian', nativeName: 'Русский' },
133
- { code: 'zh', name: 'Chinese', nativeName: '中文' },
134
- { code: 'ja', name: 'Japanese', nativeName: '日本語' },
135
- { code: 'ko', name: 'Korean', nativeName: '한국어' },
136
- { code: 'ar', name: 'Arabic', nativeName: 'العربية' },
137
- { code: 'hi', name: 'Hindi', nativeName: 'हिन्दी' },
138
- { code: 'nl', name: 'Dutch', nativeName: 'Nederlands' },
139
- ];
140
-
141
- // Use SlashCommand from slash-commands-extension.ts
142
-
143
- // Get AI provider instance
144
- const getAIProvider = (settings: AISettingsType): AIProviderInterface | null => {
145
- if (!settings.apiKey) return null;
146
-
147
- try {
148
- // Map provider names to supported ones
149
- const providerMap: Record<string, 'openai' | 'gemini' | 'claude'> = {
150
- 'openai': 'openai',
151
- 'gemini': 'gemini',
152
- 'claude': 'claude',
153
- 'anthropic': 'claude', // Map anthropic to claude
154
- 'cohere': 'openai' // Use OpenAI as fallback for unsupported providers
155
- };
156
-
157
- const mappedProvider = providerMap[settings.provider] || 'openai';
158
-
159
- return createAIProvider(mappedProvider, {
160
- apiKey: settings.apiKey,
161
- model: settings.model,
162
- temperature: settings.temperature,
163
- maxTokens: settings.maxTokens
164
- });
165
- } catch (error) {
166
- console.error('Failed to create AI provider:', error);
167
- return null;
168
- }
169
- }
170
- import './slash-commands.css';
171
- import './table-styles.css';
172
-
173
- export interface EditorAIConfig {
174
- provider?: 'openai' | 'claude' | 'gemini' | 'cohere';
175
- apiKey?: string;
176
- model?: string;
177
- temperature?: number;
178
- maxTokens?: number;
179
- endpoint?: string; // For server-side proxy
180
- }
181
-
182
- interface RichTextEditorProps {
183
- value?: string;
184
- onChange?: (value: string) => void;
185
- placeholder?: string;
186
- className?: string;
187
- height?: number | string;
188
- features?: {
189
- bold?: boolean;
190
- italic?: boolean;
191
- underline?: boolean;
192
- strike?: boolean;
193
- heading?: boolean;
194
- lists?: boolean;
195
- blockquote?: boolean;
196
- code?: boolean;
197
- link?: boolean;
198
- image?: boolean;
199
- table?: boolean;
200
- align?: boolean;
201
- color?: boolean;
202
- ai?: boolean;
203
- };
204
- aiConfig?: EditorAIConfig;
205
- persistAISettings?: boolean;
206
- }
207
-
208
-
209
- const EditorColorPicker = ({
210
- onColorSelect,
211
- currentColor = "#000000"
212
- }: {
213
- onColorSelect: (color: string) => void;
214
- currentColor?: string;
215
- }) => {
216
- const [showAdvanced, setShowAdvanced] = React.useState(false);
217
-
218
- // Quick colors - 3x3 grid of 8 colors + 1 advanced button
219
- const quickColors = [
220
- '#000000', // Black
221
- '#ef4444', // Red
222
- '#f59e0b', // Orange
223
- '#10b981', // Green
224
- '#3b82f6', // Blue
225
- '#8b5cf6', // Purple
226
- '#6b7280', // Gray
227
- '#ec4899', // Pink
228
- ];
229
-
230
- if (showAdvanced) {
231
- return (
232
- <div className="p-4 w-64">
233
- <div className="flex items-center justify-between mb-3">
234
- <span className="text-sm font-medium">Choose Color</span>
235
- <Button
236
- size="sm"
237
- variant="ghost"
238
- onClick={() => setShowAdvanced(false)}
239
- >
240
- Back
241
- </Button>
242
- </div>
243
- <ColorPicker
244
- value={currentColor}
245
- onChange={(color) => {
246
- onColorSelect(color);
247
- setShowAdvanced(false);
248
- }}
249
- showInput={true}
250
- showPresets={false}
251
- size="sm"
252
- />
253
- </div>
254
- );
255
- }
256
-
257
- return (
258
- <div className="p-3 w-36">
259
- <div className="grid grid-cols-3 gap-2">
260
- {quickColors.map((color) => (
261
- <button
262
- key={color}
263
- onClick={() => onColorSelect(color)}
264
- className={cn(
265
- "w-10 h-10 rounded-md border-2 hover:scale-110 transition-transform",
266
- currentColor === color ? "border-primary" : "border-transparent"
267
- )}
268
- style={{ backgroundColor: color }}
269
- title={color}
270
- />
271
- ))}
272
- <button
273
- onClick={() => setShowAdvanced(true)}
274
- className="w-10 h-10 rounded-md border-2 border-dashed border-muted-foreground/50 hover:border-primary transition-colors flex items-center justify-center"
275
- title="More colors"
276
- >
277
- <Palette className="w-5 h-5 text-muted-foreground" />
278
- </button>
279
- </div>
280
- </div>
281
- );
282
- };
283
-
284
- const ToolbarButton = ({
285
- active,
286
- disabled,
287
- onClick,
288
- children,
289
- tooltip
290
- }: {
291
- active?: boolean;
292
- disabled?: boolean;
293
- onClick: () => void;
294
- children: React.ReactNode;
295
- tooltip?: string;
296
- }) => {
297
- const button = (
298
- <button
299
- type="button"
300
- onClick={onClick}
301
- disabled={disabled}
302
- className={cn(
303
- "p-2 rounded hover:bg-accent transition-colors",
304
- active && "bg-accent",
305
- disabled && "opacity-50 cursor-not-allowed"
306
- )}
307
- >
308
- {children}
309
- </button>
310
- );
311
-
312
- if (!tooltip) return button;
313
-
314
- return (
315
- <Tooltip>
316
- <TooltipTrigger asChild>
317
- {button}
318
- </TooltipTrigger>
319
- <TooltipContent>
320
- <p>{tooltip}</p>
321
- </TooltipContent>
322
- </Tooltip>
323
- );
324
- };
325
-
326
- export function RichTextEditor({
327
- placeholder = 'Start writing...',
328
- className,
329
- height = 300,
330
- features = {
331
- bold: true,
332
- italic: true,
333
- underline: true,
334
- strike: true,
335
- heading: true,
336
- lists: true,
337
- blockquote: true,
338
- code: true,
339
- link: true,
340
- image: true,
341
- table: true,
342
- align: true,
343
- color: true,
344
- ai: true,
345
- },
346
- aiConfig = {},
347
- persistAISettings = true,
348
- }: RichTextEditorProps) {
349
- // Pro access kontrolü
350
- const { hasProAccess, isLoading } = useSubscription();
351
-
352
- // In docs mode, always show the component
353
-
354
- // If not in docs mode and no pro access, show upgrade prompt
355
- if (!isLoading && !hasProAccess) {
356
- return (
357
- <Card className={cn("w-full", className)}>
358
- <CardContent className="py-12 text-center">
359
- <div className="max-w-md mx-auto space-y-4">
360
- <div className="rounded-full bg-purple-100 dark:bg-purple-900/30 p-3 w-fit mx-auto">
361
- <Lock className="h-6 w-6 text-purple-600 dark:text-purple-400" />
362
- </div>
363
- <div>
364
- <h3 className="font-semibold text-lg mb-2">Pro Feature</h3>
365
- <p className="text-muted-foreground text-sm mb-4">
366
- Advanced Rich Text Editor is available exclusively to MoonUI Pro subscribers.
367
- </p>
368
- <div className="flex gap-3 justify-center">
369
- <a href="/pricing">
370
- <Button size="sm">
371
- <Sparkles className="mr-2 h-4 w-4" />
372
- Upgrade to Pro
373
- </Button>
374
- </a>
375
- </div>
376
- </div>
377
- </div>
378
- </CardContent>
379
- </Card>
380
- );
381
- }
382
-
383
- const [aiSettings, setAiSettings] = useState<AISettingsType>(() => {
384
- // Öncelik sırası: Props > LocalStorage > Varsayılan
385
- if (persistAISettings && typeof window !== 'undefined') {
386
- try {
387
- const stored = localStorage.getItem('moonui-ai-settings');
388
- if (stored) {
389
- const parsed = JSON.parse(stored);
390
- // Props'tan gelen değerler her zaman öncelikli
391
- const settings = {
392
- provider: (aiConfig.provider !== undefined ? aiConfig.provider : parsed.provider || 'openai') as 'openai' | 'claude' | 'gemini' | 'cohere',
393
- apiKey: aiConfig.apiKey !== undefined ? aiConfig.apiKey : parsed.apiKey || '',
394
- model: aiConfig.model !== undefined ? aiConfig.model : parsed.model || 'gpt-3.5-turbo',
395
- temperature: aiConfig.temperature !== undefined ? aiConfig.temperature : parsed.temperature ?? 0.7,
396
- maxTokens: aiConfig.maxTokens !== undefined ? aiConfig.maxTokens : parsed.maxTokens ?? 1000,
397
- };
398
- return settings;
399
- }
400
- } catch (e) {
401
- console.error('Failed to load AI settings from localStorage:', e);
402
- }
403
- }
404
-
405
- // LocalStorage yoksa veya persist kapalıysa props/varsayılan değerleri kullan
406
- const defaultSettings = {
407
- provider: (aiConfig.provider || 'openai') as 'openai' | 'claude' | 'gemini' | 'cohere',
408
- apiKey: aiConfig.apiKey || '',
409
- model: aiConfig.model || 'gpt-3.5-turbo',
410
- temperature: aiConfig.temperature ?? 0.7,
411
- maxTokens: aiConfig.maxTokens ?? 1000,
412
- };
413
- return defaultSettings;
414
- });
415
- const [isAiSettingsOpen, setIsAiSettingsOpen] = useState(false);
416
- const [isProcessing, setIsProcessing] = useState(false);
417
- const [typingText, setTypingText] = useState('');
418
- const [isTyping, setIsTyping] = useState(false);
419
- const typingIntervalRef = useRef<NodeJS.Timeout | null>(null);
420
- const [currentAction, setCurrentAction] = useState('');
421
- const [remainingText, setRemainingText] = useState(''); // To store remaining text when stopped
422
- const isTypingRef = useRef(false); // Ref to track typing state in closure
423
- const [isSourceView, setIsSourceView] = useState(false);
424
- const [sourceContent, setSourceContent] = useState('');
425
- const [currentTextColor, setCurrentTextColor] = useState('#000000');
426
- const [currentBgColor, setCurrentBgColor] = useState('#ffffff');
427
- const [lastTranslateLanguage, setLastTranslateLanguage] = useState<string>(() => {
428
- // Son kullanılan dili localStorage'dan al
429
- if (typeof window !== 'undefined') {
430
- return localStorage.getItem('moonui-last-translate-language') || 'en';
431
- }
432
- return 'en';
433
- });
434
-
435
- // AI Preview Modal states
436
- const [isAiPreviewOpen, setIsAiPreviewOpen] = useState(false);
437
- const [previewContent, setPreviewContent] = useState('');
438
- const [previewAction, setPreviewAction] = useState('');
439
- const [previewOriginalText, setPreviewOriginalText] = useState('');
440
-
441
- // Statistics states
442
- const [wordCount, setWordCount] = useState(0);
443
- const [characterCount, setCharacterCount] = useState(0);
444
- const [tokensUsed, setTokensUsed] = useState(0);
445
- const [lastAIResponse, setLastAIResponse] = useState<{
446
- action: string;
447
- tokens: number;
448
- model: string;
449
- timestamp: Date;
450
- } | null>(null);
451
-
452
- // Clean up typewriter effect on unmount
453
- useEffect(() => {
454
- return () => {
455
- if (typingIntervalRef.current) {
456
- clearTimeout(typingIntervalRef.current);
457
- }
458
- };
459
- }, []);
460
-
461
- // Stop typing function
462
- const stopTyping = () => {
463
- if (typingIntervalRef.current) {
464
- clearTimeout(typingIntervalRef.current);
465
- typingIntervalRef.current = null;
466
- }
467
- setIsTyping(false);
468
- isTypingRef.current = false;
469
- setTypingText('');
470
- setRemainingText('');
471
- toast({
472
- title: "AI typing stopped",
473
- description: "The AI response was interrupted.",
474
- });
475
- };
476
-
477
- // Check if action should use modal preview
478
- const shouldUseModal = (action: string): boolean => {
479
- const modalActions = ['expand', 'summarize', 'ideas', 'continue'];
480
- return modalActions.includes(action);
481
- };
482
-
483
- // Utility functions for statistics
484
- const countWords = (text: string): number => {
485
- return text.trim().split(/\s+/).filter(word => word.length > 0).length;
486
- };
487
-
488
- const countCharacters = (text: string): number => {
489
- return text.length;
490
- };
491
-
492
- // Approximate token count (rough estimation: 1 token ≈ 4 characters)
493
- const estimateTokens = (text: string): number => {
494
- return Math.ceil(text.length / 4);
495
- };
496
-
497
- // Optimize text for token efficiency
498
- const optimizeTextForAI = (text: string, action: string): string => {
499
- const maxTokens = 2000; // Safe limit for most providers
500
- const estimatedTokens = estimateTokens(text);
501
-
502
- if (estimatedTokens <= maxTokens) {
503
- return text;
504
- }
505
-
506
- // For summarize, we can use more text since output will be shorter
507
- if (action === 'summarize') {
508
- const maxChars = maxTokens * 4;
509
- return text.substring(0, maxChars) + '...';
510
- }
511
-
512
- // For other actions, limit to smaller chunks
513
- const maxChars = (maxTokens * 4) / 2;
514
- return text.substring(0, maxChars) + '...';
515
- };
516
-
517
-
518
- // Apply AI content to editor with typewriter effect
519
- const applyAIContentToEditor = (content: string, replaceSelection: boolean = true) => {
520
- if (!editor) return;
521
-
522
- // Start typewriter effect
523
- setIsTyping(true);
524
- isTypingRef.current = true;
525
- setTypingText('');
526
-
527
- if (replaceSelection) {
528
- const selection = editor.state.selection;
529
- const selectedText = editor.state.doc.textBetween(selection.from, selection.to, ' ');
530
- if (selectedText) {
531
- editor.chain().focus().deleteSelection().run();
532
- }
533
- }
534
-
535
- let currentIndex = 0;
536
- const typeSpeed = 5; // ms per character - much faster for better UX
537
-
538
- const typeNextChar = () => {
539
- if (currentIndex < content.length && isTypingRef.current) {
540
- const nextChar = content[currentIndex];
541
- setTypingText(prev => prev + nextChar);
542
- editor.chain().focus().insertContent(nextChar).run();
543
- currentIndex++;
544
- setRemainingText(content.substring(currentIndex));
545
- typingIntervalRef.current = setTimeout(typeNextChar, typeSpeed);
546
- } else {
547
- // Typing complete or stopped
548
- setIsTyping(false);
549
- isTypingRef.current = false;
550
- setTypingText('');
551
- setRemainingText('');
552
-
553
- if (currentIndex >= content.length) {
554
- // Success toast only if completed
555
- toast({
556
- title: "AI action completed",
557
- description: "Your text has been updated successfully.",
558
- });
559
- }
560
- }
561
- };
562
-
563
- // Start typing
564
- typeNextChar();
565
- };
566
-
567
- // Slash commands tanımları
568
- const slashCommands: SlashCommand[] = [
569
- {
570
- id: 'rewrite',
571
- command: 'rewrite',
572
- description: 'Rewrite selected text',
573
- icon: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21.5 2v6h-6M2.5 22v-6h6M2 11.5a10 10 0 0 1 18.8-4.3M22 12.5a10 10 0 0 1-18.8 4.3"/></svg>',
574
- action: async (text: string) => {
575
- const result = await callAI('rewrite', text);
576
- return { text: result || '', error: result ? undefined : 'Failed to rewrite' };
577
- }
578
- },
579
- {
580
- id: 'expand',
581
- command: 'expand',
582
- description: 'Expand text with more details',
583
- icon: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m21 21-6-6m6 6v-4.8m0 4.8h-4.8"/><path d="M3 16.2V21m0 0h4.8M3 21l6-6"/><path d="M21 7.8V3m0 0h-4.8M21 3l-6 6"/><path d="M3 7.8V3m0 0h4.8M3 3l6 6"/></svg>',
584
- action: async (text: string) => {
585
- const result = await callAI('expand', text);
586
- return { text: result || '', error: result ? undefined : 'Failed to expand' };
587
- }
588
- },
589
- {
590
- id: 'summarize',
591
- command: 'summarize',
592
- description: 'Summarize text',
593
- icon: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 11H3"/><path d="M9 7H3"/><path d="M9 15H3"/><path d="M16 10V3"/><path d="M16 10l3-3"/><path d="M13 10l3 3"/><path d="M9 19h12"/></svg>',
594
- action: async (text: string) => {
595
- const result = await callAI('summarize', text);
596
- return { text: result || '', error: result ? undefined : 'Failed to summarize' };
597
- }
598
- },
599
- {
600
- id: 'continue',
601
- command: 'continue',
602
- description: 'Continue writing from here',
603
- icon: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12h14"/><path d="m12 5 7 7-7 7"/></svg>',
604
- action: async (text: string) => {
605
- const result = await callAI('continue', text);
606
- return { text: result || '', error: result ? undefined : 'Failed to continue' };
607
- }
608
- },
609
- {
610
- id: 'improve',
611
- command: 'improve',
612
- description: 'Improve writing quality',
613
- icon: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m12 3-1.912 5.813a2 2 0 0 1-1.275 1.275L3 12l5.813 1.912a2 2 0 0 1 1.275 1.275L12 21l1.912-5.813a2 2 0 0 1 1.275-1.275L21 12l-5.813-1.912a2 2 0 0 1-1.275-1.275L12 3Z"/></svg>',
614
- action: async (text: string) => {
615
- const result = await callAI('improve', text);
616
- return { text: result || '', error: result ? undefined : 'Failed to improve' };
617
- }
618
- },
619
- {
620
- id: 'fix',
621
- command: 'fix',
622
- description: 'Fix grammar and spelling',
623
- icon: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m6 9 6 6 6-6"/></svg>',
624
- action: async (text: string) => {
625
- const result = await callAI('fix', text);
626
- return { text: result || '', error: result ? undefined : 'Failed to fix' };
627
- }
628
- },
629
- {
630
- id: 'translate',
631
- command: 'translate',
632
- description: 'Translate to Turkish',
633
- icon: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m5 8 6 6"/><path d="m4 14 6-6 2-3"/><path d="M2 5h12"/><path d="M7 2v3"/><path d="m22 22-5-10-5 10"/><path d="m14 18 6 0"/></svg>',
634
- action: async (text: string) => {
635
- const result = await callAI('translate', text);
636
- return { text: result || '', error: result ? undefined : 'Failed to translate' };
637
- }
638
- },
639
- {
640
- id: 'ideas',
641
- command: 'ideas',
642
- description: 'Generate writing ideas',
643
- icon: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2v20"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg>',
644
- action: async (text: string) => {
645
- const result = await callAI('ideas', text);
646
- return { text: result || '', error: result ? undefined : 'Failed to generate ideas' };
647
- }
648
- }
649
- ];
650
-
651
- const editor = useEditor({
652
- extensions: [
653
- StarterKit.configure({
654
- heading: {
655
- levels: [1, 2, 3],
656
- },
657
- codeBlock: false, // We'll use CodeBlockLowlight instead
658
- }),
659
- Placeholder.configure({
660
- placeholder,
661
- }),
662
- TextAlign.configure({
663
- types: ['heading', 'paragraph'],
664
- }),
665
- Highlight.configure({
666
- multicolor: true,
667
- }),
668
- Link.configure({
669
- openOnClick: false,
670
- HTMLAttributes: {
671
- target: '_blank',
672
- rel: 'noopener noreferrer',
673
- },
674
- }),
675
- Image.configure({
676
- inline: true,
677
- allowBase64: true,
678
- }),
679
- Table.configure({
680
- resizable: true,
681
- }),
682
- TableRow,
683
- TableHeader,
684
- TableCell,
685
- TextStyle,
686
- Color,
687
- UnderlineExtension,
688
- Gapcursor,
689
- TaskList,
690
- TaskItem.configure({
691
- nested: true,
692
- }),
693
- Typography,
694
- CodeBlockLowlight.configure({
695
- lowlight,
696
- }),
697
- SlashCommandsExtension.configure({
698
- commands: slashCommands,
699
- onSelectCommand: async (command) => {
700
- if (!editor) return;
701
-
702
- const { from, to } = editor.state.selection;
703
- const selectedText = editor.state.doc.textBetween(from, to, ' ');
704
-
705
- setIsProcessing(true);
706
- setCurrentAction(command.id || '');
707
-
708
- try {
709
- const response = command.action ? await command.action(selectedText || editor.getText()) : { text: '', error: 'No action defined' };
710
- if (response.text) {
711
- // Check if this command should use modal preview
712
- if (shouldUseModal(command.id || '')) {
713
- // Open preview modal
714
- setPreviewContent(response.text);
715
- setPreviewAction(command.id || '');
716
- setPreviewOriginalText(selectedText || editor.getText());
717
- setIsAiPreviewOpen(true);
718
- } else {
719
- // Apply directly with typewriter effect
720
- applyAIContentToEditor(response.text, !!selectedText);
721
- }
722
- } else if (response.error) {
723
- toast({
724
- title: "AI Error",
725
- description: response.error,
726
- variant: "destructive",
727
- });
728
- }
729
- } catch {
730
- toast({
731
- title: "Error",
732
- description: "Failed to execute command",
733
- variant: "destructive",
734
- });
735
- } finally {
736
- setIsProcessing(false);
737
- }
738
- },
739
- }),
740
- ],
741
- content: '',
742
- editorProps: {
743
- attributes: {
744
- class: cn(
745
- 'prose prose-sm sm:prose lg:prose-lg xl:prose-xl focus:outline-none min-h-[200px] p-4 max-w-none',
746
- // Ensure headings show different sizes
747
- '[&_h1]:text-3xl [&_h1]:font-bold [&_h1]:mb-4',
748
- '[&_h2]:text-2xl [&_h2]:font-bold [&_h2]:mb-3',
749
- '[&_h3]:text-xl [&_h3]:font-bold [&_h3]:mb-2',
750
- '[&_h4]:text-lg [&_h4]:font-bold [&_h4]:mb-2',
751
- '[&_h5]:text-base [&_h5]:font-bold [&_h5]:mb-1',
752
- '[&_h6]:text-sm [&_h6]:font-bold [&_h6]:mb-1',
753
- // Ensure lists show proper styling
754
- '[&_ul]:list-disc [&_ul]:pl-6 [&_ul]:mb-4',
755
- '[&_ol]:list-decimal [&_ol]:pl-6 [&_ol]:mb-4',
756
- '[&_li]:mb-1',
757
- // Table styles
758
- '[&_table]:border-collapse-collapse [&_table]:w-full [&_table]:mb-4',
759
- '[&_table_td]:border [&_table_td]:border-border [&_table_td]:p-2',
760
- '[&_table_th]:border [&_table_th]:border-border [&_table_th]:p-2 [&_table_th]:bg-muted [&_table_th]:font-semibold',
761
- // Other styles
762
- '[&_blockquote]:border-l-4 [&_blockquote]:border-muted-foreground/30 [&_blockquote]:pl-4 [&_blockquote]:italic',
763
- '[&_pre]:bg-muted [&_pre]:p-4 [&_pre]:rounded [&_pre]:overflow-x-auto'
764
- ),
765
- },
766
- },
767
- immediatelyRender: false,
768
- });
769
-
770
- // Update statistics when editor content changes
771
- useEffect(() => {
772
- if (editor) {
773
- const text = editor.getText();
774
- setWordCount(countWords(text));
775
- setCharacterCount(countCharacters(text));
776
- }
777
- }, [editor?.state.doc.content]);
778
-
779
- // Listen to editor updates
780
- useEffect(() => {
781
- if (editor) {
782
- const updateStats = () => {
783
- const text = editor.getText();
784
- setWordCount(countWords(text));
785
- setCharacterCount(countCharacters(text));
786
- };
787
-
788
- editor.on('update', updateStats);
789
- return () => {
790
- editor.off('update', updateStats);
791
- };
792
- }
793
- }, [editor]);
794
-
795
- // AI işlevleri
796
- const callAI = async (action: string, text: string, targetLanguage?: string) => {
797
- if (!aiSettings.apiKey) {
798
- toast({
799
- title: "API Key Required",
800
- description: "Please configure your AI settings first.",
801
- variant: "destructive",
802
- });
803
- setIsAiSettingsOpen(true);
804
- return null;
805
- }
806
-
807
- setIsProcessing(true);
808
- const startTime = Date.now();
809
-
810
- try {
811
- const provider = getAIProvider(aiSettings);
812
- if (!provider) {
813
- throw new Error('Failed to initialize AI provider');
814
- }
815
-
816
- // Optimize text for better token efficiency
817
- const optimizedText = optimizeTextForAI(text, action);
818
- const inputTokens = estimateTokens(optimizedText);
819
-
820
- // Show warning if text was truncated
821
- if (optimizedText !== text) {
822
- toast({
823
- title: "Text optimized",
824
- description: `Long text was shortened to ${inputTokens} tokens for better efficiency.`,
825
- });
826
- }
827
-
828
- let response: string;
829
-
830
- switch (action) {
831
- case 'rewrite':
832
- response = await provider.rewrite(optimizedText);
833
- break;
834
- case 'expand':
835
- response = await provider.expand(optimizedText);
836
- break;
837
- case 'summarize':
838
- response = await provider.summarize(optimizedText);
839
- break;
840
- case 'fix':
841
- response = await provider.fixGrammar(optimizedText);
842
- break;
843
- case 'translate':
844
- response = await provider.translate(optimizedText, targetLanguage || 'English');
845
- break;
846
- case 'tone_professional':
847
- response = await provider.changeTone(optimizedText, 'professional');
848
- break;
849
- case 'tone_casual':
850
- response = await provider.changeTone(optimizedText, 'casual');
851
- break;
852
- case 'tone_friendly':
853
- response = await provider.changeTone(optimizedText, 'friendly');
854
- break;
855
- case 'tone_formal':
856
- response = await provider.changeTone(optimizedText, 'formal');
857
- break;
858
- case 'continue':
859
- response = await provider.continueWriting(optimizedText);
860
- break;
861
- case 'improve':
862
- response = await provider.improveWriting(optimizedText);
863
- break;
864
- case 'ideas':
865
- response = await provider.generateIdeas(optimizedText);
866
- break;
867
- default:
868
- response = await provider.complete(optimizedText);
869
- }
870
-
871
- // Estimate output tokens and total usage
872
- const outputTokens = estimateTokens(response);
873
- const totalTokens = inputTokens + outputTokens;
874
-
875
- // Update token usage statistics
876
- setTokensUsed(prev => prev + totalTokens);
877
- setLastAIResponse({
878
- action,
879
- tokens: totalTokens,
880
- model: aiSettings.model,
881
- timestamp: new Date()
882
- });
883
-
884
- return response;
885
- } catch (error) {
886
- toast({
887
- title: "AI Error",
888
- description: error instanceof Error ? error.message : "Failed to process with AI",
889
- variant: "destructive",
890
- });
891
- return null;
892
- } finally {
893
- setIsProcessing(false);
894
- }
895
- };
896
-
897
- const handleAIAction = async (action: string, targetLanguage?: string) => {
898
- if (!editor) return;
899
-
900
- const selection = editor.state.selection;
901
- const selectedText = editor.state.doc.textBetween(selection.from, selection.to, ' ');
902
- const fullText = editor.getText();
903
-
904
- // If no text is selected, use the full editor content as prompt
905
- const textToProcess = selectedText || fullText;
906
-
907
- if (!textToProcess && action !== 'complete') {
908
- toast({
909
- title: "No content available",
910
- description: "Please write some text first.",
911
- variant: "destructive",
912
- });
913
- return;
914
- }
915
-
916
- // Set current action for UI
917
- setCurrentAction(action);
918
-
919
- // Show processing toast
920
- const processingToast = toast({
921
- title: "Processing with AI...",
922
- description: getActionDescription(action, targetLanguage),
923
- duration: 60000, // Long duration
924
- });
925
-
926
- const result = await callAI(action, textToProcess, targetLanguage);
927
-
928
- // Dismiss processing toast
929
- processingToast.dismiss();
930
-
931
- if (result) {
932
- // Check if this action should use modal preview
933
- if (shouldUseModal(action)) {
934
- // Open preview modal instead of directly applying
935
- setPreviewContent(result);
936
- setPreviewAction(action);
937
- setPreviewOriginalText(textToProcess);
938
- setIsAiPreviewOpen(true);
939
- } else {
940
- // Apply directly with typewriter effect (for rewrite, fix grammar, tone changes, etc.)
941
- applyAIContentToEditor(result, !!selectedText);
942
- }
943
- }
944
- };
945
-
946
- const getActionDescription = (action: string, targetLanguage?: string): string => {
947
- const descriptions: Record<string, string> = {
948
- rewrite: "Rewriting your text...",
949
- improve: "Improving your writing...",
950
- expand: "Expanding your text...",
951
- summarize: "Creating a summary...",
952
- fix: "Fixing grammar and spelling...",
953
- translate: targetLanguage ? `Translating to ${SUPPORTED_LANGUAGES.find(l => l.name === targetLanguage)?.nativeName || targetLanguage}...` : "Translating...",
954
- tone_professional: "Making text professional...",
955
- tone_casual: "Making text casual...",
956
- tone_friendly: "Making text friendly...",
957
- tone_formal: "Making text formal...",
958
- continue: "Continuing your writing...",
959
- ideas: "Generating ideas...",
960
- complete: "Completing your text..."
961
- };
962
- return descriptions[action] || "Processing...";
963
- };
964
-
965
- const [linkUrl, setLinkUrl] = useState('');
966
- const [imageUrl, setImageUrl] = useState('');
967
- const [isLinkDialogOpen, setIsLinkDialogOpen] = useState(false);
968
- const [isImageDialogOpen, setIsImageDialogOpen] = useState(false);
969
- const [isTableDialogOpen, setIsTableDialogOpen] = useState(false);
970
- const [tableRows, setTableRows] = useState(3);
971
- const [tableCols, setTableCols] = useState(3);
972
- const [isTableBorderDialogOpen, setIsTableBorderDialogOpen] = useState(false);
973
- const [tableBorderWidth, setTableBorderWidth] = useState(1);
974
- const [tableBorderColor, setTableBorderColor] = useState('#000000');
975
- const [tableBorderStyle, setTableBorderStyle] = useState('solid');
976
-
977
- const addImage = () => {
978
- if (imageUrl && editor) {
979
- editor.chain().focus().setImage({ src: imageUrl }).run();
980
- setImageUrl('');
981
- setIsImageDialogOpen(false);
982
- }
983
- };
984
-
985
- const addLink = () => {
986
- if (linkUrl && editor) {
987
- if (editor.state.selection.empty) {
988
- // Eğer seçim yoksa, link'i text olarak ekle
989
- editor.chain().focus().insertContent(`<a href="${linkUrl}">${linkUrl}</a>`).run();
990
- } else {
991
- // Seçili metne link ekle
992
- editor.chain().focus().extendMarkRange('link').setLink({ href: linkUrl }).run();
993
- }
994
- setLinkUrl('');
995
- setIsLinkDialogOpen(false);
996
- }
997
- };
998
-
999
- const removeLink = () => {
1000
- editor?.chain().focus().unsetLink().run();
1001
- };
1002
-
1003
- const createTable = () => {
1004
- if (editor && tableRows > 0 && tableCols > 0) {
1005
- editor.chain().focus().insertTable({
1006
- rows: tableRows,
1007
- cols: tableCols,
1008
- withHeaderRow: true
1009
- }).run();
1010
- // Apply default border styles to the newly created table
1011
- setTimeout(() => {
1012
- applyTableBorders(tableBorderWidth, tableBorderColor, tableBorderStyle);
1013
- }, 100);
1014
- setIsTableDialogOpen(false);
1015
- }
1016
- };
1017
-
1018
- const applyTableBorders = (width: number, color: string, style: string) => {
1019
- if (!editor) return;
1020
-
1021
- const borderStyle = `${width}px ${style} ${color}`;
1022
-
1023
- // Apply borders to the selected table
1024
- editor.view.dom.querySelectorAll('.ProseMirror table').forEach((table) => {
1025
- if (table instanceof HTMLElement) {
1026
- table.style.borderCollapse = 'collapse';
1027
- table.style.border = borderStyle;
1028
-
1029
- // Apply borders to all cells
1030
- table.querySelectorAll('td, th').forEach((cell) => {
1031
- if (cell instanceof HTMLElement) {
1032
- cell.style.border = borderStyle;
1033
- cell.style.padding = '8px';
1034
- }
1035
- });
1036
- }
1037
- });
1038
- };
1039
-
1040
- if (!editor) {
1041
- return null;
1042
- }
1043
-
1044
- return (
1045
- <div className={cn("border rounded-lg overflow-hidden bg-background", className)}>
1046
- {/* Toolbar */}
1047
- <TooltipProvider>
1048
- <div className="border-b bg-muted/50 p-3">
1049
- <div className="flex items-center flex-wrap gap-1">
1050
- {/* Format Group */}
1051
- {(features.bold || features.italic || features.underline || features.strike || features.code) && (
1052
- <div className="flex items-center gap-1 shrink-0">
1053
- {features.bold && (
1054
- <ToolbarButton
1055
- active={editor.isActive('bold')}
1056
- onClick={() => editor.chain().focus().toggleBold().run()}
1057
- tooltip="Bold (Cmd+B)"
1058
- disabled={isSourceView}
1059
- >
1060
- <Bold className="w-4 h-4" />
1061
- </ToolbarButton>
1062
- )}
1063
- {features.italic && (
1064
- <ToolbarButton
1065
- active={editor.isActive('italic')}
1066
- onClick={() => editor.chain().focus().toggleItalic().run()}
1067
- tooltip="Italic (Cmd+I)"
1068
- disabled={isSourceView}
1069
- >
1070
- <Italic className="w-4 h-4" />
1071
- </ToolbarButton>
1072
- )}
1073
- {features.underline && (
1074
- <ToolbarButton
1075
- active={editor.isActive('underline')}
1076
- onClick={() => editor.chain().focus().toggleUnderline().run()}
1077
- tooltip="Underline (Cmd+U)"
1078
- disabled={isSourceView}
1079
- >
1080
- <Underline className="w-4 h-4" />
1081
- </ToolbarButton>
1082
- )}
1083
- {features.strike && (
1084
- <ToolbarButton
1085
- active={editor.isActive('strike')}
1086
- onClick={() => editor.chain().focus().toggleStrike().run()}
1087
- tooltip="Strikethrough"
1088
- disabled={isSourceView}
1089
- >
1090
- <Strikethrough className="w-4 h-4" />
1091
- </ToolbarButton>
1092
- )}
1093
- {features.code && (
1094
- <ToolbarButton
1095
- active={editor.isActive('code')}
1096
- onClick={() => editor.chain().focus().toggleCode().run()}
1097
- tooltip="Inline code"
1098
- disabled={isSourceView}
1099
- >
1100
- <Code className="w-4 h-4" />
1101
- </ToolbarButton>
1102
- )}
1103
- </div>
1104
- )}
1105
-
1106
- {/* Separator */}
1107
- {(features.bold || features.italic || features.underline || features.strike || features.code) &&
1108
- (features.heading || features.align) && (
1109
- <div className="w-px h-6 bg-border mx-1 shrink-0"></div>
1110
- )}
1111
-
1112
- {/* Typography Group */}
1113
- {(features.heading || features.align) && (
1114
- <div className="flex items-center gap-1 shrink-0">
1115
- {features.heading && (
1116
- <DropdownMenu>
1117
- <DropdownMenuTrigger asChild>
1118
- <Button variant="ghost" size="sm" className="h-8 px-2" disabled={isSourceView}>
1119
- <Type className="w-4 h-4 mr-1" />
1120
- <ChevronDown className="w-3 h-3" />
1121
- </Button>
1122
- </DropdownMenuTrigger>
1123
- <DropdownMenuContent>
1124
- <DropdownMenuItem onClick={() => editor.chain().focus().setParagraph().run()}>
1125
- Paragraph
1126
- </DropdownMenuItem>
1127
- <DropdownMenuItem onClick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()}>
1128
- <Heading1 className="w-4 h-4 mr-2" /> Heading 1
1129
- </DropdownMenuItem>
1130
- <DropdownMenuItem onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}>
1131
- <Heading2 className="w-4 h-4 mr-2" /> Heading 2
1132
- </DropdownMenuItem>
1133
- <DropdownMenuItem onClick={() => editor.chain().focus().toggleHeading({ level: 3 }).run()}>
1134
- <Heading3 className="w-4 h-4 mr-2" /> Heading 3
1135
- </DropdownMenuItem>
1136
- </DropdownMenuContent>
1137
- </DropdownMenu>
1138
- )}
1139
-
1140
- {features.align && (
1141
- <>
1142
- <ToolbarButton
1143
- active={editor.isActive({ textAlign: 'left' })}
1144
- onClick={() => editor.chain().focus().setTextAlign('left').run()}
1145
- tooltip="Align left"
1146
- disabled={isSourceView}
1147
- >
1148
- <AlignLeft className="w-4 h-4" />
1149
- </ToolbarButton>
1150
- <ToolbarButton
1151
- active={editor.isActive({ textAlign: 'center' })}
1152
- onClick={() => editor.chain().focus().setTextAlign('center').run()}
1153
- tooltip="Align center"
1154
- disabled={isSourceView}
1155
- >
1156
- <AlignCenter className="w-4 h-4" />
1157
- </ToolbarButton>
1158
- <ToolbarButton
1159
- active={editor.isActive({ textAlign: 'right' })}
1160
- onClick={() => editor.chain().focus().setTextAlign('right').run()}
1161
- tooltip="Align right"
1162
- disabled={isSourceView}
1163
- >
1164
- <AlignRight className="w-4 h-4" />
1165
- </ToolbarButton>
1166
- <ToolbarButton
1167
- active={editor.isActive({ textAlign: 'justify' })}
1168
- onClick={() => editor.chain().focus().setTextAlign('justify').run()}
1169
- tooltip="Justify"
1170
- disabled={isSourceView}
1171
- >
1172
- <AlignJustify className="w-4 h-4" />
1173
- </ToolbarButton>
1174
- </>
1175
- )}
1176
- </div>
1177
- )}
1178
-
1179
- {/* Separator */}
1180
- {(features.heading || features.align) &&
1181
- (features.lists || features.blockquote) && (
1182
- <div className="w-px h-6 bg-border mx-1 shrink-0"></div>
1183
- )}
1184
-
1185
- {/* Lists & Structure Group */}
1186
- {(features.lists || features.blockquote) && (
1187
- <div className="flex items-center gap-1 shrink-0">
1188
- {features.lists && (
1189
- <>
1190
- <ToolbarButton
1191
- active={editor.isActive('bulletList')}
1192
- onClick={() => editor.chain().focus().toggleBulletList().run()}
1193
- tooltip="Bullet list"
1194
- disabled={isSourceView}
1195
- >
1196
- <List className="w-4 h-4" />
1197
- </ToolbarButton>
1198
- <ToolbarButton
1199
- active={editor.isActive('orderedList')}
1200
- onClick={() => editor.chain().focus().toggleOrderedList().run()}
1201
- tooltip="Numbered list"
1202
- disabled={isSourceView}
1203
- >
1204
- <ListOrdered className="w-4 h-4" />
1205
- </ToolbarButton>
1206
- <ToolbarButton
1207
- active={editor.isActive('taskList')}
1208
- onClick={() => editor.chain().focus().toggleTaskList().run()}
1209
- tooltip="Task list"
1210
- disabled={isSourceView}
1211
- >
1212
- <CheckSquare className="w-4 h-4" />
1213
- </ToolbarButton>
1214
- </>
1215
- )}
1216
-
1217
- {features.blockquote && (
1218
- <ToolbarButton
1219
- active={editor.isActive('blockquote')}
1220
- onClick={() => editor.chain().focus().toggleBlockquote().run()}
1221
- tooltip="Quote"
1222
- disabled={isSourceView}
1223
- >
1224
- <Quote className="w-4 h-4" />
1225
- </ToolbarButton>
1226
- )}
1227
- </div>
1228
- )}
1229
-
1230
- {/* Separator */}
1231
- {(features.lists || features.blockquote) &&
1232
- (features.color) && (
1233
- <div className="w-px h-6 bg-border mx-1 shrink-0"></div>
1234
- )}
1235
-
1236
- {/* Colors Group */}
1237
- {features.color && (
1238
- <div className="flex items-center gap-1 shrink-0">
1239
- <Popover>
1240
- <PopoverTrigger asChild>
1241
- <Button
1242
- variant="ghost"
1243
- size="sm"
1244
- className="h-8 px-2"
1245
- disabled={isSourceView}
1246
- title="Text Color"
1247
- >
1248
- <Palette className="w-4 h-4" />
1249
- </Button>
1250
- </PopoverTrigger>
1251
- <PopoverContent className="w-auto p-0">
1252
- <EditorColorPicker
1253
- currentColor={currentTextColor}
1254
- onColorSelect={(color) => {
1255
- setCurrentTextColor(color);
1256
- editor.chain().focus().setColor(color).run();
1257
- }}
1258
- />
1259
- </PopoverContent>
1260
- </Popover>
1261
-
1262
- <Popover>
1263
- <PopoverTrigger asChild>
1264
- <Button
1265
- variant="ghost"
1266
- size="sm"
1267
- className="h-8 px-2"
1268
- disabled={isSourceView}
1269
- title="Background Color"
1270
- >
1271
- <Highlighter className="w-4 h-4" />
1272
- </Button>
1273
- </PopoverTrigger>
1274
- <PopoverContent className="w-auto p-0">
1275
- <EditorColorPicker
1276
- currentColor={currentBgColor}
1277
- onColorSelect={(color) => {
1278
- setCurrentBgColor(color);
1279
- editor.chain().focus().toggleHighlight({ color }).run();
1280
- }}
1281
- />
1282
- </PopoverContent>
1283
- </Popover>
1284
- </div>
1285
- )}
1286
-
1287
- {/* Separator */}
1288
- {(features.color) &&
1289
- (features.link || features.image || features.table) && (
1290
- <div className="w-px h-6 bg-border mx-1 shrink-0"></div>
1291
- )}
1292
-
1293
- {/* Media & Links Group */}
1294
- {(features.link || features.image || features.table) && (
1295
- <div className="flex items-center gap-1 shrink-0">
1296
- {features.link && (
1297
- <Dialog open={isLinkDialogOpen} onOpenChange={setIsLinkDialogOpen}>
1298
- <DialogTrigger asChild>
1299
- <ToolbarButton
1300
- active={editor.isActive('link')}
1301
- onClick={() => {
1302
- const previousUrl = editor.getAttributes('link').href
1303
- const url = window.prompt('URL', previousUrl)
1304
- if (url === null) return
1305
- if (url === '') {
1306
- editor.chain().focus().extendMarkRange('link').unsetLink().run()
1307
- } else {
1308
- editor.chain().focus().extendMarkRange('link').setLink({ href: url }).run()
1309
- }
1310
- }}
1311
- tooltip="Add link"
1312
- disabled={isSourceView}
1313
- >
1314
- <Link2 className="w-4 h-4" />
1315
- </ToolbarButton>
1316
- </DialogTrigger>
1317
- <DialogContent className="sm:max-w-[425px]">
1318
- <DialogHeader>
1319
- <DialogTitle>Add Link</DialogTitle>
1320
- <DialogDescription>
1321
- Enter the URL for the link.
1322
- </DialogDescription>
1323
- </DialogHeader>
1324
- <div className="grid gap-4 py-4">
1325
- <div className="grid gap-2">
1326
- <Label htmlFor="url">URL</Label>
1327
- <Input
1328
- id="url"
1329
- value={linkUrl}
1330
- onChange={(e) => setLinkUrl(e.target.value)}
1331
- placeholder="https://example.com"
1332
- onKeyDown={(e) => {
1333
- if (e.key === 'Enter') {
1334
- e.preventDefault();
1335
- addLink();
1336
- }
1337
- }}
1338
- />
1339
- </div>
1340
- </div>
1341
- <div className="flex justify-between">
1342
- {editor.isActive('link') && (
1343
- <Button
1344
- variant="destructive"
1345
- onClick={() => {
1346
- removeLink();
1347
- setIsLinkDialogOpen(false);
1348
- }}
1349
- >
1350
- Remove Link
1351
- </Button>
1352
- )}
1353
- <div className="ml-auto flex gap-2">
1354
- <Button variant="outline" onClick={() => setIsLinkDialogOpen(false)}>
1355
- Cancel
1356
- </Button>
1357
- <Button onClick={addLink}>Add Link</Button>
1358
- </div>
1359
- </div>
1360
- </DialogContent>
1361
- </Dialog>
1362
- )}
1363
- {features.image && (
1364
- <Dialog open={isImageDialogOpen} onOpenChange={setIsImageDialogOpen}>
1365
- <DialogTrigger asChild>
1366
- <ToolbarButton
1367
- onClick={() => {
1368
- const url = window.prompt('Image URL')
1369
- if (url) {
1370
- editor.chain().focus().setImage({ src: url }).run()
1371
- }
1372
- }}
1373
- tooltip="Add image"
1374
- disabled={isSourceView}
1375
- >
1376
- <ImageIcon className="w-4 h-4" />
1377
- </ToolbarButton>
1378
- </DialogTrigger>
1379
- <DialogContent className="sm:max-w-[425px]">
1380
- <DialogHeader>
1381
- <DialogTitle>Add Image</DialogTitle>
1382
- <DialogDescription>
1383
- Enter the URL for the image.
1384
- </DialogDescription>
1385
- </DialogHeader>
1386
- <div className="grid gap-4 py-4">
1387
- <div className="grid gap-2">
1388
- <Label htmlFor="image-url">Image URL</Label>
1389
- <Input
1390
- id="image-url"
1391
- value={imageUrl}
1392
- onChange={(e) => setImageUrl(e.target.value)}
1393
- placeholder="https://example.com/image.jpg"
1394
- onKeyDown={(e) => {
1395
- if (e.key === 'Enter') {
1396
- e.preventDefault();
1397
- addImage();
1398
- }
1399
- }}
1400
- />
1401
- </div>
1402
- </div>
1403
- <div className="flex justify-end gap-2">
1404
- <Button variant="outline" onClick={() => setIsImageDialogOpen(false)}>
1405
- Cancel
1406
- </Button>
1407
- <Button onClick={addImage}>Add Image</Button>
1408
- </div>
1409
- </DialogContent>
1410
- </Dialog>
1411
- )}
1412
- {features.table && (
1413
- <>
1414
- <Dialog open={isTableDialogOpen} onOpenChange={setIsTableDialogOpen}>
1415
- <DialogTrigger asChild>
1416
- <ToolbarButton
1417
- onClick={() => editor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run()}
1418
- tooltip="Create table"
1419
- disabled={isSourceView}
1420
- >
1421
- <TableIcon className="w-4 h-4" />
1422
- </ToolbarButton>
1423
- </DialogTrigger>
1424
- <DialogContent className="sm:max-w-[425px]">
1425
- <DialogHeader>
1426
- <DialogTitle>Create Table</DialogTitle>
1427
- <DialogDescription>
1428
- Choose the size for your new table.
1429
- </DialogDescription>
1430
- </DialogHeader>
1431
- <div className="grid gap-4 py-4">
1432
- <div className="grid grid-cols-2 gap-4">
1433
- <div className="grid gap-2">
1434
- <Label htmlFor="rows">Rows</Label>
1435
- <Input
1436
- id="rows"
1437
- type="number"
1438
- min="1"
1439
- max="20"
1440
- value={tableRows}
1441
- onChange={(e) => setTableRows(parseInt(e.target.value) || 3)}
1442
- />
1443
- </div>
1444
- <div className="grid gap-2">
1445
- <Label htmlFor="cols">Columns</Label>
1446
- <Input
1447
- id="cols"
1448
- type="number"
1449
- min="1"
1450
- max="10"
1451
- value={tableCols}
1452
- onChange={(e) => setTableCols(parseInt(e.target.value) || 3)}
1453
- />
1454
- </div>
1455
- </div>
1456
- <div className="flex items-center space-x-2">
1457
- <input
1458
- type="checkbox"
1459
- id="headerRow"
1460
- defaultChecked
1461
- className="rounded border-input"
1462
- />
1463
- <Label htmlFor="headerRow" className="text-sm font-normal">
1464
- Include header row
1465
- </Label>
1466
- </div>
1467
- </div>
1468
- <div className="flex justify-end gap-2">
1469
- <Button variant="outline" onClick={() => setIsTableDialogOpen(false)}>
1470
- Cancel
1471
- </Button>
1472
- <Button onClick={createTable}>Create Table</Button>
1473
- </div>
1474
- </DialogContent>
1475
- </Dialog>
1476
- <DropdownMenu>
1477
- <DropdownMenuTrigger asChild>
1478
- <Button variant="ghost" size="sm" className="h-8 px-1">
1479
- <ChevronDown className="w-3 h-3" />
1480
- </Button>
1481
- </DropdownMenuTrigger>
1482
- <DropdownMenuContent>
1483
- <DropdownMenuItem onClick={() => editor.chain().focus().addColumnBefore().run()}>
1484
- Add column before
1485
- </DropdownMenuItem>
1486
- <DropdownMenuItem onClick={() => editor.chain().focus().addColumnAfter().run()}>
1487
- Add column after
1488
- </DropdownMenuItem>
1489
- <DropdownMenuItem onClick={() => editor.chain().focus().deleteColumn().run()}>
1490
- Delete column
1491
- </DropdownMenuItem>
1492
- <DropdownMenuSeparator />
1493
- <DropdownMenuItem onClick={() => editor.chain().focus().addRowBefore().run()}>
1494
- Add row before
1495
- </DropdownMenuItem>
1496
- <DropdownMenuItem onClick={() => editor.chain().focus().addRowAfter().run()}>
1497
- Add row after
1498
- </DropdownMenuItem>
1499
- <DropdownMenuItem onClick={() => editor.chain().focus().deleteRow().run()}>
1500
- Delete row
1501
- </DropdownMenuItem>
1502
- <DropdownMenuSeparator />
1503
- <DropdownMenuItem onClick={() => setIsTableBorderDialogOpen(true)}>
1504
- <Settings className="w-4 h-4 mr-2" />
1505
- Table Borders
1506
- </DropdownMenuItem>
1507
- <DropdownMenuSeparator />
1508
- <DropdownMenuItem onClick={() => editor.chain().focus().deleteTable().run()}>
1509
- Delete table
1510
- </DropdownMenuItem>
1511
- </DropdownMenuContent>
1512
- </DropdownMenu>
1513
- </>
1514
- )}
1515
- </div>
1516
- )}
1517
-
1518
- {/* Separator */}
1519
- {(features.link || features.image || features.table) &&
1520
- (true) && (
1521
- <div className="w-px h-6 bg-border mx-1 shrink-0"></div>
1522
- )}
1523
-
1524
- {/* Tools Group */}
1525
- <div className="flex items-center gap-1 shrink-0">
1526
- <ToolbarButton
1527
- onClick={() => editor.chain().focus().undo().run()}
1528
- disabled={!editor.can().undo() || isSourceView}
1529
- tooltip="Undo (Cmd+Z)"
1530
- >
1531
- <Undo className="w-4 h-4" />
1532
- </ToolbarButton>
1533
- <ToolbarButton
1534
- onClick={() => editor.chain().focus().redo().run()}
1535
- disabled={!editor.can().redo() || isSourceView}
1536
- tooltip="Redo (Cmd+Shift+Z)"
1537
- >
1538
- <Redo className="w-4 h-4" />
1539
- </ToolbarButton>
1540
-
1541
- <ToolbarButton
1542
- active={isSourceView}
1543
- onClick={() => {
1544
- if (isSourceView) {
1545
- // Apply source changes when switching back to visual mode
1546
- editor.commands.setContent(sourceContent);
1547
- } else {
1548
- // Update source content when switching to source view
1549
- setSourceContent(editor.getHTML());
1550
- }
1551
- setIsSourceView(!isSourceView);
1552
- }}
1553
- tooltip={isSourceView ? "Visual Mode" : "Source Code"}
1554
- >
1555
- {isSourceView ? <Edit className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
1556
- </ToolbarButton>
1557
-
1558
- {/* AI Tools */}
1559
- {features.ai && (
1560
- <>
1561
- <DropdownMenu>
1562
- <DropdownMenuTrigger asChild>
1563
- <Button
1564
- variant="ghost"
1565
- size="sm"
1566
- className="h-8 px-3 bg-purple-100 hover:bg-purple-200 dark:bg-purple-900 dark:hover:bg-purple-800 transition-colors"
1567
- disabled={isProcessing || isTyping}
1568
- >
1569
- {isProcessing || isTyping ? (
1570
- <RefreshCw className="w-4 h-4 mr-1 animate-spin" />
1571
- ) : (
1572
- <Wand2 className="w-4 h-4 mr-1" />
1573
- )}
1574
- {isTyping ? 'Typing...' : 'AI Tools'}
1575
- </Button>
1576
- </DropdownMenuTrigger>
1577
- {isTyping && (
1578
- <Button
1579
- variant="destructive"
1580
- size="sm"
1581
- className="h-8 px-2"
1582
- onClick={stopTyping}
1583
- title="Stop AI typing"
1584
- >
1585
- <X className="w-4 h-4" />
1586
- </Button>
1587
- )}
1588
- <DropdownMenuContent className="w-64">
1589
- <div className="px-2 py-1.5 text-xs font-semibold text-muted-foreground flex items-center gap-2">
1590
- <Wand2 className="w-3 h-3" />
1591
- Writing Improvements
1592
- </div>
1593
- <DropdownMenuItem
1594
- onClick={() => handleAIAction('rewrite')}
1595
- disabled={isProcessing || isTyping}
1596
- >
1597
- <RefreshCw className="w-4 h-4 mr-2" />
1598
- Rewrite Selection
1599
- <span className="ml-auto text-xs text-muted-foreground">Alt+R</span>
1600
- </DropdownMenuItem>
1601
- <DropdownMenuItem
1602
- onClick={() => handleAIAction('improve')}
1603
- disabled={isProcessing || isTyping}
1604
- >
1605
- <Sparkles className="w-4 h-4 mr-2" />
1606
- Improve Writing
1607
- </DropdownMenuItem>
1608
- <DropdownMenuItem
1609
- onClick={() => handleAIAction('expand')}
1610
- disabled={isProcessing || isTyping}
1611
- >
1612
- <Maximize className="w-4 h-4 mr-2" />
1613
- Expand Text
1614
- </DropdownMenuItem>
1615
- <DropdownMenuItem
1616
- onClick={() => handleAIAction('summarize')}
1617
- disabled={isProcessing || isTyping}
1618
- >
1619
- <FileText className="w-4 h-4 mr-2" />
1620
- Summarize
1621
- </DropdownMenuItem>
1622
- <DropdownMenuItem
1623
- onClick={() => handleAIAction('continue')}
1624
- disabled={isProcessing || isTyping}
1625
- >
1626
- <Plus className="w-4 h-4 mr-2" />
1627
- Continue Writing
1628
- </DropdownMenuItem>
1629
-
1630
- <DropdownMenuSeparator />
1631
- <div className="px-2 py-1.5 text-xs font-semibold text-muted-foreground flex items-center gap-2">
1632
- <Palette className="w-3 h-3" />
1633
- Tone Adjustments
1634
- </div>
1635
- <DropdownMenuItem
1636
- onClick={() => handleAIAction('tone_professional')}
1637
- disabled={isProcessing || isTyping}
1638
- >
1639
- <Briefcase className="w-4 h-4 mr-2" />
1640
- Make Professional
1641
- </DropdownMenuItem>
1642
- <DropdownMenuItem
1643
- onClick={() => handleAIAction('tone_casual')}
1644
- disabled={isProcessing || isTyping}
1645
- >
1646
- <MessageSquare className="w-4 h-4 mr-2" />
1647
- Make Casual
1648
- </DropdownMenuItem>
1649
- <DropdownMenuItem
1650
- onClick={() => handleAIAction('tone_friendly')}
1651
- disabled={isProcessing || isTyping}
1652
- >
1653
- <Heart className="w-4 h-4 mr-2" />
1654
- Make Friendly
1655
- </DropdownMenuItem>
1656
- <DropdownMenuItem
1657
- onClick={() => handleAIAction('tone_formal')}
1658
- disabled={isProcessing || isTyping}
1659
- >
1660
- <GraduationCap className="w-4 h-4 mr-2" />
1661
- Make Formal
1662
- </DropdownMenuItem>
1663
-
1664
- <DropdownMenuSeparator />
1665
- <div className="px-2 py-1.5 text-xs font-semibold text-muted-foreground flex items-center gap-2">
1666
- <Zap className="w-3 h-3" />
1667
- Other Actions
1668
- </div>
1669
- <DropdownMenuItem
1670
- onClick={() => handleAIAction('fix')}
1671
- disabled={isProcessing || isTyping}
1672
- >
1673
- <Check className="w-4 h-4 mr-2" />
1674
- Fix Grammar & Spelling
1675
- <span className="ml-auto text-xs text-muted-foreground">F7</span>
1676
- </DropdownMenuItem>
1677
- <DropdownMenuSub>
1678
- <DropdownMenuSubTrigger disabled={isProcessing || isTyping}>
1679
- <Languages className="w-4 h-4 mr-2" />
1680
- Translate
1681
- {lastTranslateLanguage && (
1682
- <span className="ml-auto text-xs text-muted-foreground">
1683
- {SUPPORTED_LANGUAGES.find(l => l.code === lastTranslateLanguage)?.nativeName || 'English'}
1684
- </span>
1685
- )}
1686
- </DropdownMenuSubTrigger>
1687
- <DropdownMenuSubContent className="w-56">
1688
- {SUPPORTED_LANGUAGES.map((language) => (
1689
- <DropdownMenuItem
1690
- key={language.code}
1691
- onClick={() => {
1692
- // Dili kaydet
1693
- setLastTranslateLanguage(language.code);
1694
- localStorage.setItem('moonui-last-translate-language', language.code);
1695
- // Çeviriyi yap
1696
- handleAIAction('translate', language.name);
1697
- }}
1698
- disabled={isProcessing || isTyping}
1699
- >
1700
- <span className="text-sm">{language.nativeName}</span>
1701
- <span className="ml-auto text-xs text-muted-foreground">{language.name}</span>
1702
- {lastTranslateLanguage === language.code && (
1703
- <Check className="w-4 h-4 ml-2 text-primary" />
1704
- )}
1705
- </DropdownMenuItem>
1706
- ))}
1707
- </DropdownMenuSubContent>
1708
- </DropdownMenuSub>
1709
- <DropdownMenuItem
1710
- onClick={() => handleAIAction('ideas')}
1711
- disabled={isProcessing || isTyping}
1712
- >
1713
- <Lightbulb className="w-4 h-4 mr-2" />
1714
- Generate Ideas
1715
- </DropdownMenuItem>
1716
-
1717
- {!aiSettings.apiKey && (
1718
- <>
1719
- <DropdownMenuSeparator />
1720
- <div className="px-2 py-2">
1721
- <motion.div
1722
- initial={{ opacity: 0, y: -10 }}
1723
- animate={{ opacity: 1, y: 0 }}
1724
- 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"
1725
- >
1726
- <div className="flex items-start gap-2">
1727
- <Settings className="w-3 h-3 mt-0.5 text-yellow-600 dark:text-yellow-400" />
1728
- <div>
1729
- <div className="font-medium text-yellow-900 dark:text-yellow-200 mb-1">
1730
- API Key Required
1731
- </div>
1732
- <div className="text-yellow-800 dark:text-yellow-300">
1733
- Click the settings icon to configure your AI provider and API key.
1734
- </div>
1735
- </div>
1736
- </div>
1737
- </motion.div>
1738
- </div>
1739
- </>
1740
- )}
1741
- </DropdownMenuContent>
1742
- </DropdownMenu>
1743
-
1744
- <Dialog open={isAiSettingsOpen} onOpenChange={setIsAiSettingsOpen}>
1745
- <DialogTrigger asChild>
1746
- <ToolbarButton
1747
- onClick={() => setIsAiSettingsOpen(true)}
1748
- tooltip="AI Settings - Configure your API keys here"
1749
- disabled={isSourceView}
1750
- >
1751
- <Settings className="w-4 h-4" />
1752
- </ToolbarButton>
1753
- </DialogTrigger>
1754
- <DialogContent className="sm:max-w-[425px] overflow-visible" style={{ zIndex: 9998 }}>
1755
- <DialogHeader>
1756
- <DialogTitle>AI Settings</DialogTitle>
1757
- <DialogDescription>
1758
- Configure your AI provider and API settings.
1759
- </DialogDescription>
1760
- </DialogHeader>
1761
- <div className="grid gap-4 py-4">
1762
- <div className="grid gap-2">
1763
- <Label htmlFor="provider">Provider</Label>
1764
- <Select
1765
- value={aiSettings.provider}
1766
- onValueChange={(value: 'openai' | 'claude' | 'gemini' | 'cohere') => {
1767
- console.log('Provider changed to:', value);
1768
- // Update model when provider changes
1769
- const defaultModels = {
1770
- openai: 'gpt-3.5-turbo',
1771
- claude: 'claude-3-sonnet-20240229',
1772
- gemini: 'gemini-2.0-flash-exp',
1773
- cohere: 'command'
1774
- };
1775
- const newSettings = {
1776
- ...aiSettings,
1777
- provider: value,
1778
- model: defaultModels[value] || 'gpt-3.5-turbo'
1779
- };
1780
- setAiSettings(newSettings);
1781
- // LocalStorage'a hemen kaydet
1782
- if (persistAISettings) {
1783
- localStorage.setItem('moonui-ai-settings', JSON.stringify(newSettings));
1784
- }
1785
- }}
1786
- >
1787
- <SelectTrigger>
1788
- <SelectValue />
1789
- </SelectTrigger>
1790
- <SelectContent className="z-[9999]" sideOffset={5}>
1791
- <SelectItem value="openai">OpenAI</SelectItem>
1792
- <SelectItem value="claude">Claude (Anthropic)</SelectItem>
1793
- <SelectItem value="gemini">Gemini (Google)</SelectItem>
1794
- <SelectItem value="cohere">Cohere</SelectItem>
1795
- </SelectContent>
1796
- </Select>
1797
- </div>
1798
- <div className="grid gap-2">
1799
- <Label htmlFor="apiKey">API Key</Label>
1800
- <Input
1801
- id="apiKey"
1802
- type="password"
1803
- value={aiSettings.apiKey}
1804
- onChange={(e) => {
1805
- const newSettings = { ...aiSettings, apiKey: e.target.value };
1806
- setAiSettings(newSettings);
1807
- // Save to localStorage immediately
1808
- if (persistAISettings) {
1809
- localStorage.setItem('moonui-ai-settings', JSON.stringify(newSettings));
1810
- }
1811
- }}
1812
- placeholder="sk-..."
1813
- />
1814
- </div>
1815
- <div className="grid gap-2">
1816
- <Label htmlFor="model">Model</Label>
1817
- <Select
1818
- value={aiSettings.model}
1819
- onValueChange={(value) => {
1820
- const newSettings = { ...aiSettings, model: value };
1821
- setAiSettings(newSettings);
1822
- // Save to localStorage immediately
1823
- if (persistAISettings) {
1824
- localStorage.setItem('moonui-ai-settings', JSON.stringify(newSettings));
1825
- }
1826
- }}
1827
- >
1828
- <SelectTrigger>
1829
- <SelectValue />
1830
- </SelectTrigger>
1831
- <SelectContent className="z-[9999]" sideOffset={5}>
1832
- {aiSettings.provider === 'openai' && (
1833
- <>
1834
- <SelectItem value="gpt-4-turbo-preview">GPT-4 Turbo</SelectItem>
1835
- <SelectItem value="gpt-4">GPT-4</SelectItem>
1836
- <SelectItem value="gpt-3.5-turbo">GPT-3.5 Turbo</SelectItem>
1837
- <SelectItem value="gpt-3.5-turbo-16k">GPT-3.5 Turbo 16K</SelectItem>
1838
- </>
1839
- )}
1840
- {aiSettings.provider === 'claude' && (
1841
- <>
1842
- <SelectItem value="claude-3-opus-20240229">Claude 3 Opus</SelectItem>
1843
- <SelectItem value="claude-3-sonnet-20240229">Claude 3 Sonnet</SelectItem>
1844
- <SelectItem value="claude-3-haiku-20240307">Claude 3 Haiku</SelectItem>
1845
- <SelectItem value="claude-2.1">Claude 2.1</SelectItem>
1846
- <SelectItem value="claude-2.0">Claude 2.0</SelectItem>
1847
- </>
1848
- )}
1849
- {aiSettings.provider === 'gemini' && (
1850
- <>
1851
- <SelectItem value="gemini-2.0-flash-exp">Gemini 2.0 Flash (Experimental)</SelectItem>
1852
- <SelectItem value="gemini-2.0-flash-thinking-exp">Gemini 2.0 Flash Thinking (Experimental)</SelectItem>
1853
- <SelectItem value="gemini-1.5-pro">Gemini 1.5 Pro</SelectItem>
1854
- <SelectItem value="gemini-1.5-flash">Gemini 1.5 Flash</SelectItem>
1855
- <SelectItem value="gemini-1.5-flash-8b">Gemini 1.5 Flash 8B</SelectItem>
1856
- <SelectItem value="gemini-pro">Gemini Pro</SelectItem>
1857
- <SelectItem value="gemini-pro-vision">Gemini Pro Vision</SelectItem>
1858
- </>
1859
- )}
1860
- {aiSettings.provider === 'cohere' && (
1861
- <>
1862
- <SelectItem value="command">Command</SelectItem>
1863
- <SelectItem value="command-light">Command Light</SelectItem>
1864
- </>
1865
- )}
1866
- </SelectContent>
1867
- </Select>
1868
- </div>
1869
- <div className="grid grid-cols-2 gap-4">
1870
- <div className="grid gap-2">
1871
- <Label htmlFor="temperature">Temperature</Label>
1872
- <Input
1873
- id="temperature"
1874
- type="number"
1875
- min="0"
1876
- max="2"
1877
- step="0.1"
1878
- value={aiSettings.temperature}
1879
- onChange={(e) => {
1880
- const newSettings = { ...aiSettings, temperature: parseFloat(e.target.value) };
1881
- setAiSettings(newSettings);
1882
- // Save to localStorage immediately
1883
- if (persistAISettings) {
1884
- localStorage.setItem('moonui-ai-settings', JSON.stringify(newSettings));
1885
- }
1886
- }}
1887
- />
1888
- </div>
1889
- <div className="grid gap-2">
1890
- <Label htmlFor="maxTokens">Max Tokens</Label>
1891
- <Input
1892
- id="maxTokens"
1893
- type="number"
1894
- min="1"
1895
- max="4000"
1896
- value={aiSettings.maxTokens}
1897
- onChange={(e) => {
1898
- const newSettings = { ...aiSettings, maxTokens: parseInt(e.target.value) };
1899
- setAiSettings(newSettings);
1900
- // Save to localStorage immediately
1901
- if (persistAISettings) {
1902
- localStorage.setItem('moonui-ai-settings', JSON.stringify(newSettings));
1903
- }
1904
- }}
1905
- />
1906
- </div>
1907
- </div>
1908
- </div>
1909
- {persistAISettings && (
1910
- <div className="space-y-4">
1911
- <div className="flex items-start space-x-2">
1912
- <Checkbox
1913
- id="rememberSettings"
1914
- defaultChecked
1915
- onCheckedChange={(checked) => {
1916
- if (!checked) {
1917
- localStorage.removeItem('moonui-ai-settings');
1918
- }
1919
- }}
1920
- />
1921
- <div className="grid gap-1.5 leading-none">
1922
- <Label
1923
- htmlFor="rememberSettings"
1924
- className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
1925
- >
1926
- Remember my settings
1927
- </Label>
1928
- <p className="text-xs text-muted-foreground">
1929
- Save settings locally for future sessions
1930
- </p>
1931
- </div>
1932
- </div>
1933
-
1934
- {aiSettings.apiKey && (
1935
- <div className="rounded-md bg-yellow-50 dark:bg-yellow-900/10 p-3">
1936
- <div className="flex">
1937
- <div className="flex-shrink-0">
1938
- <Settings className="h-4 w-4 text-yellow-400" />
1939
- </div>
1940
- <div className="ml-3">
1941
- <p className="text-xs text-yellow-800 dark:text-yellow-200">
1942
- <strong>Security Notice:</strong> API keys will be stored in your browser's local storage.
1943
- For production use, consider using a server-side proxy.
1944
- </p>
1945
- </div>
1946
- </div>
1947
- </div>
1948
- )}
1949
- </div>
1950
- )}
1951
-
1952
- <div className="flex justify-end gap-2">
1953
- <Button
1954
- variant="outline"
1955
- onClick={() => setIsAiSettingsOpen(false)}
1956
- >
1957
- Cancel
1958
- </Button>
1959
- <Button onClick={() => {
1960
- // LocalStorage'a kaydet
1961
- if (persistAISettings) {
1962
- const toStore = { ...aiSettings };
1963
- // Güvenlik için API key'i opsiyonel olarak kaydet
1964
- localStorage.setItem('moonui-ai-settings', JSON.stringify(toStore));
1965
- }
1966
-
1967
- setIsAiSettingsOpen(false);
1968
- toast({
1969
- title: "Settings saved",
1970
- description: "Your AI settings have been updated.",
1971
- });
1972
- }}>
1973
- Save Settings
1974
- </Button>
1975
- </div>
1976
- </DialogContent>
1977
- </Dialog>
1978
- </>
1979
- )}
1980
- </div>
1981
- </div>
1982
- </div>
1983
- </TooltipProvider>
1984
-
1985
- {/* Table Border Settings Dialog */}
1986
- <Dialog open={isTableBorderDialogOpen} onOpenChange={setIsTableBorderDialogOpen}>
1987
- <DialogContent className="sm:max-w-[425px]">
1988
- <DialogHeader>
1989
- <DialogTitle>Table Border Settings</DialogTitle>
1990
- <DialogDescription>
1991
- Customize the appearance of table borders.
1992
- </DialogDescription>
1993
- </DialogHeader>
1994
- <div className="grid gap-6 py-4">
1995
- <div className="grid gap-3">
1996
- <Label htmlFor="border-width">Border Width</Label>
1997
- <div className="flex items-center gap-4">
1998
- <Slider
1999
- id="border-width"
2000
- min={0}
2001
- max={5}
2002
- step={1}
2003
- value={[tableBorderWidth]}
2004
- onValueChange={(value) => setTableBorderWidth(value[0])}
2005
- className="flex-1"
2006
- />
2007
- <span className="text-sm font-medium w-8">{tableBorderWidth}px</span>
2008
- </div>
2009
- </div>
2010
-
2011
- <div className="grid gap-3">
2012
- <Label htmlFor="border-color">Border Color</Label>
2013
- <div className="flex items-center gap-2">
2014
- <ColorPicker
2015
- value={tableBorderColor}
2016
- onChange={setTableBorderColor}
2017
- showInput={true}
2018
- showPresets={false}
2019
- size="sm"
2020
- />
2021
- <span className="text-sm text-muted-foreground">{tableBorderColor}</span>
2022
- </div>
2023
- </div>
2024
-
2025
- <div className="grid gap-3">
2026
- <Label htmlFor="border-style">Border Style</Label>
2027
- <Select value={tableBorderStyle} onValueChange={setTableBorderStyle}>
2028
- <SelectTrigger>
2029
- <SelectValue />
2030
- </SelectTrigger>
2031
- <SelectContent>
2032
- <SelectItem value="solid">Solid</SelectItem>
2033
- <SelectItem value="dashed">Dashed</SelectItem>
2034
- <SelectItem value="dotted">Dotted</SelectItem>
2035
- <SelectItem value="double">Double</SelectItem>
2036
- <SelectItem value="groove">Groove</SelectItem>
2037
- <SelectItem value="ridge">Ridge</SelectItem>
2038
- <SelectItem value="inset">Inset</SelectItem>
2039
- <SelectItem value="outset">Outset</SelectItem>
2040
- </SelectContent>
2041
- </Select>
2042
- </div>
2043
-
2044
- {/* Preview */}
2045
- <div className="grid gap-3">
2046
- <Label>Preview</Label>
2047
- <div className="p-4 bg-muted rounded-lg">
2048
- <table
2049
- className="w-full text-sm"
2050
- style={{
2051
- borderCollapse: 'collapse',
2052
- border: `${tableBorderWidth}px ${tableBorderStyle} ${tableBorderColor}`
2053
- }}
2054
- >
2055
- <thead>
2056
- <tr>
2057
- <th
2058
- style={{
2059
- border: `${tableBorderWidth}px ${tableBorderStyle} ${tableBorderColor}`,
2060
- padding: '8px',
2061
- textAlign: 'left'
2062
- }}
2063
- >
2064
- Header 1
2065
- </th>
2066
- <th
2067
- style={{
2068
- border: `${tableBorderWidth}px ${tableBorderStyle} ${tableBorderColor}`,
2069
- padding: '8px',
2070
- textAlign: 'left'
2071
- }}
2072
- >
2073
- Header 2
2074
- </th>
2075
- </tr>
2076
- </thead>
2077
- <tbody>
2078
- <tr>
2079
- <td
2080
- style={{
2081
- border: `${tableBorderWidth}px ${tableBorderStyle} ${tableBorderColor}`,
2082
- padding: '8px'
2083
- }}
2084
- >
2085
- Cell 1
2086
- </td>
2087
- <td
2088
- style={{
2089
- border: `${tableBorderWidth}px ${tableBorderStyle} ${tableBorderColor}`,
2090
- padding: '8px'
2091
- }}
2092
- >
2093
- Cell 2
2094
- </td>
2095
- </tr>
2096
- </tbody>
2097
- </table>
2098
- </div>
2099
- </div>
2100
- </div>
2101
- <div className="flex justify-end gap-2">
2102
- <Button variant="outline" onClick={() => setIsTableBorderDialogOpen(false)}>
2103
- Cancel
2104
- </Button>
2105
- <Button onClick={() => {
2106
- applyTableBorders(tableBorderWidth, tableBorderColor, tableBorderStyle);
2107
- setIsTableBorderDialogOpen(false);
2108
- toast({
2109
- title: "Borders applied",
2110
- description: "Table borders have been updated.",
2111
- });
2112
- }}>
2113
- Apply Borders
2114
- </Button>
2115
- </div>
2116
- </DialogContent>
2117
- </Dialog>
2118
-
2119
- {/* AI Preview Modal */}
2120
- <Dialog open={isAiPreviewOpen} onOpenChange={setIsAiPreviewOpen}>
2121
- <DialogContent className="max-w-4xl max-h-[80vh] overflow-hidden flex flex-col">
2122
- <DialogHeader>
2123
- <DialogTitle className="flex items-center gap-2">
2124
- <Wand2 className="w-5 h-5 text-purple-600 dark:text-purple-400" />
2125
- AI Preview - {getActionDescription(previewAction).replace('...', '')}
2126
- </DialogTitle>
2127
- <DialogDescription>
2128
- Review the AI-generated content before applying it to your editor.
2129
- </DialogDescription>
2130
- </DialogHeader>
2131
-
2132
- <div className="flex-1 overflow-hidden flex flex-col gap-4">
2133
- {/* Original Text Section */}
2134
- <div className="flex-shrink-0">
2135
- <div className="text-sm font-medium text-muted-foreground mb-2">Original Text:</div>
2136
- <div className="p-3 bg-muted/50 rounded-md border max-h-32 overflow-auto">
2137
- <p className="text-sm whitespace-pre-wrap">{previewOriginalText}</p>
2138
- </div>
2139
- </div>
2140
-
2141
- {/* AI Generated Content */}
2142
- <div className="flex-1 overflow-hidden flex flex-col">
2143
- <div className="text-sm font-medium text-muted-foreground mb-2 flex items-center gap-2">
2144
- <Sparkles className="w-4 h-4" />
2145
- AI Generated Content:
2146
- </div>
2147
- <div className="flex-1 p-4 bg-gradient-to-br from-purple-50 to-blue-50 dark:from-purple-950/50 dark:to-blue-950/50 rounded-md border-2 border-purple-200 dark:border-purple-800 overflow-auto">
2148
- <div className="prose prose-sm max-w-none dark:prose-invert">
2149
- <div className="whitespace-pre-wrap text-sm leading-relaxed">
2150
- {previewContent}
2151
- </div>
2152
- </div>
2153
- </div>
2154
- </div>
2155
- </div>
2156
-
2157
- {/* Action Buttons */}
2158
- <div className="flex-shrink-0 flex justify-between items-center pt-4 border-t">
2159
- <div className="flex gap-2">
2160
- <Button
2161
- variant="outline"
2162
- size="sm"
2163
- onClick={() => {
2164
- navigator.clipboard.writeText(previewContent);
2165
- toast({
2166
- title: "Copied to clipboard",
2167
- description: "The AI-generated content has been copied.",
2168
- });
2169
- }}
2170
- >
2171
- <Copy className="w-4 h-4 mr-2" />
2172
- Copy
2173
- </Button>
2174
- <Button
2175
- variant="outline"
2176
- size="sm"
2177
- onClick={() => {
2178
- // Regenerate content
2179
- setIsAiPreviewOpen(false);
2180
- handleAIAction(previewAction);
2181
- }}
2182
- >
2183
- <RefreshCw className="w-4 h-4 mr-2" />
2184
- Regenerate
2185
- </Button>
2186
- </div>
2187
-
2188
- <div className="flex gap-2">
2189
- <Button
2190
- variant="outline"
2191
- onClick={() => setIsAiPreviewOpen(false)}
2192
- >
2193
- Cancel
2194
- </Button>
2195
- <Button
2196
- onClick={() => {
2197
- setIsAiPreviewOpen(false);
2198
- // Apply with typewriter effect
2199
- const replaceSelection = previewOriginalText !== editor?.getText();
2200
- applyAIContentToEditor(previewContent, replaceSelection);
2201
- }}
2202
- className="bg-gradient-to-r from-purple-600 to-blue-600 hover:from-purple-700 hover:to-blue-700"
2203
- >
2204
- <Check className="w-4 h-4 mr-2" />
2205
- Apply to Editor
2206
- </Button>
2207
- </div>
2208
- </div>
2209
- </DialogContent>
2210
- </Dialog>
2211
-
2212
- {/* Editor */}
2213
- <div
2214
- className="overflow-auto relative"
2215
- style={{ height: typeof height === 'number' ? `${height}px` : height }}
2216
- >
2217
- {isSourceView ? (
2218
- <textarea
2219
- value={sourceContent}
2220
- onChange={(e) => setSourceContent(e.target.value)}
2221
- className="w-full h-full p-4 font-mono text-sm resize-none focus:outline-none bg-muted"
2222
- placeholder="HTML source code..."
2223
- />
2224
- ) : (
2225
- <>
2226
- <EditorContent editor={editor} />
2227
- {/* AI Processing Overlay */}
2228
- {isProcessing && (
2229
- <motion.div
2230
- initial={{ opacity: 0 }}
2231
- animate={{ opacity: 1 }}
2232
- exit={{ opacity: 0 }}
2233
- className="absolute inset-0 bg-background/80 backdrop-blur-sm flex items-center justify-center z-50"
2234
- >
2235
- <div className="bg-card border rounded-lg p-6 shadow-lg max-w-sm w-full mx-4">
2236
- <div className="flex flex-col items-center space-y-4">
2237
- <div className="relative">
2238
- <div className="w-16 h-16 border-4 border-purple-200 dark:border-purple-800 rounded-full animate-pulse"></div>
2239
- <div className="absolute inset-0 flex items-center justify-center">
2240
- <Wand2 className="w-8 h-8 text-purple-600 dark:text-purple-400 animate-bounce" />
2241
- </div>
2242
- </div>
2243
- <div className="text-center space-y-2">
2244
- <h3 className="font-semibold text-lg">AI is thinking...</h3>
2245
- <p className="text-sm text-muted-foreground">
2246
- {getActionDescription(
2247
- currentAction,
2248
- lastTranslateLanguage
2249
- )}
2250
- </p>
2251
- </div>
2252
- <div className="flex space-x-1">
2253
- <motion.div
2254
- animate={{ scale: [1, 1.5, 1] }}
2255
- transition={{ duration: 0.6, repeat: Infinity, delay: 0 }}
2256
- className="w-2 h-2 bg-purple-600 dark:bg-purple-400 rounded-full"
2257
- />
2258
- <motion.div
2259
- animate={{ scale: [1, 1.5, 1] }}
2260
- transition={{ duration: 0.6, repeat: Infinity, delay: 0.2 }}
2261
- className="w-2 h-2 bg-purple-600 dark:bg-purple-400 rounded-full"
2262
- />
2263
- <motion.div
2264
- animate={{ scale: [1, 1.5, 1] }}
2265
- transition={{ duration: 0.6, repeat: Infinity, delay: 0.4 }}
2266
- className="w-2 h-2 bg-purple-600 dark:bg-purple-400 rounded-full"
2267
- />
2268
- </div>
2269
- </div>
2270
- </div>
2271
- </motion.div>
2272
- )}
2273
- </>
2274
- )}
2275
- </div>
2276
-
2277
- {/* Statistics Bar */}
2278
- <div className="border-t bg-muted/50 px-4 py-2">
2279
- <div className="flex items-center justify-between text-xs text-muted-foreground">
2280
- <div className="flex items-center gap-6">
2281
- <div className="flex items-center gap-1">
2282
- <FileText className="w-3 h-3" />
2283
- <span>{wordCount} words, {characterCount} characters</span>
2284
- </div>
2285
-
2286
- {lastAIResponse && (
2287
- <div className="flex items-center gap-1">
2288
- <Wand2 className="w-3 h-3 text-purple-600 dark:text-purple-400" />
2289
- <span>
2290
- Last: {lastAIResponse.action} | {lastAIResponse.tokens} tokens | {lastAIResponse.model}
2291
- </span>
2292
- </div>
2293
- )}
2294
- </div>
2295
-
2296
- <div className="flex items-center gap-4">
2297
- {tokensUsed > 0 && (
2298
- <div className="flex items-center gap-1">
2299
- <Zap className="w-3 h-3 text-orange-500" />
2300
- <span className="font-medium">
2301
- {tokensUsed.toLocaleString()} tokens used
2302
- </span>
2303
- </div>
2304
- )}
2305
-
2306
- <div className="flex items-center gap-1">
2307
- <div className={cn(
2308
- "w-2 h-2 rounded-full",
2309
- aiSettings.apiKey ? "bg-green-500" : "bg-red-500"
2310
- )} />
2311
- <span className="capitalize">
2312
- {aiSettings.provider} {aiSettings.apiKey ? "connected" : "disconnected"}
2313
- </span>
2314
- </div>
2315
- </div>
2316
- </div>
2317
- </div>
2318
- </div>
2319
- );
2320
- }
2321
-
2322
- export default RichTextEditor;