@moontra/moonui-pro 2.20.1 → 2.20.3

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