@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.
- package/package.json +8 -3
- package/plugin/index.d.ts +86 -0
- package/plugin/index.js +308 -0
- package/scripts/postinstall.js +191 -23
- package/src/components/advanced-chart/index.tsx +0 -1246
- package/src/components/advanced-forms/index.tsx +0 -585
- package/src/components/animated-button/index.tsx +0 -385
- package/src/components/calendar/event-dialog.tsx +0 -377
- package/src/components/calendar/index.tsx +0 -1220
- package/src/components/calendar-pro/index.tsx +0 -1697
- package/src/components/color-picker/index.tsx +0 -432
- package/src/components/credit-card-input/index.tsx +0 -406
- package/src/components/dashboard/dashboard-grid.tsx +0 -480
- package/src/components/dashboard/demo.tsx +0 -425
- package/src/components/dashboard/index.tsx +0 -1046
- package/src/components/dashboard/time-range-picker.tsx +0 -336
- package/src/components/dashboard/types.ts +0 -225
- package/src/components/dashboard/widgets/activity-feed.tsx +0 -349
- package/src/components/dashboard/widgets/chart-widget.tsx +0 -418
- package/src/components/dashboard/widgets/comparison-widget.tsx +0 -177
- package/src/components/dashboard/widgets/index.ts +0 -5
- package/src/components/dashboard/widgets/metric-card.tsx +0 -363
- package/src/components/dashboard/widgets/progress-widget.tsx +0 -113
- package/src/components/data-table/data-table-bulk-actions.tsx +0 -204
- package/src/components/data-table/data-table-column-toggle.tsx +0 -169
- package/src/components/data-table/data-table-export.ts +0 -156
- package/src/components/data-table/data-table-filter-drawer.tsx +0 -448
- package/src/components/data-table/index.tsx +0 -845
- package/src/components/draggable-list/index.tsx +0 -100
- package/src/components/error-boundary/index.tsx +0 -232
- package/src/components/file-upload/index.tsx +0 -1660
- package/src/components/floating-action-button/index.tsx +0 -206
- package/src/components/form-wizard/form-wizard-context.tsx +0 -335
- package/src/components/form-wizard/form-wizard-navigation.tsx +0 -118
- package/src/components/form-wizard/form-wizard-progress.tsx +0 -329
- package/src/components/form-wizard/form-wizard-step.tsx +0 -111
- package/src/components/form-wizard/index.tsx +0 -102
- package/src/components/form-wizard/types.ts +0 -77
- package/src/components/gesture-drawer/index.tsx +0 -551
- package/src/components/github-stars/github-api.ts +0 -426
- package/src/components/github-stars/hooks.ts +0 -517
- package/src/components/github-stars/index.tsx +0 -375
- package/src/components/github-stars/types.ts +0 -148
- package/src/components/github-stars/variants.tsx +0 -515
- package/src/components/health-check/index.tsx +0 -439
- package/src/components/hover-card-3d/index.tsx +0 -529
- package/src/components/index.ts +0 -130
- package/src/components/internal/index.ts +0 -78
- package/src/components/kanban/add-card-modal.tsx +0 -502
- package/src/components/kanban/card-detail-modal.tsx +0 -761
- package/src/components/kanban/index.ts +0 -13
- package/src/components/kanban/kanban.tsx +0 -1689
- package/src/components/kanban/types.ts +0 -168
- package/src/components/lazy-component/index.tsx +0 -823
- package/src/components/license-error/index.tsx +0 -31
- package/src/components/magnetic-button/index.tsx +0 -216
- package/src/components/memory-efficient-data/index.tsx +0 -1018
- package/src/components/moonui-quiz-form/index.tsx +0 -817
- package/src/components/navbar/index.tsx +0 -781
- package/src/components/optimized-image/index.tsx +0 -425
- package/src/components/performance-debugger/index.tsx +0 -613
- package/src/components/performance-monitor/index.tsx +0 -808
- package/src/components/phone-number-input/index.tsx +0 -343
- package/src/components/phone-number-input/phone-number-input-simple.tsx +0 -167
- package/src/components/pinch-zoom/index.tsx +0 -566
- package/src/components/quiz-form/index.tsx +0 -479
- package/src/components/rich-text-editor/index.tsx +0 -2322
- package/src/components/rich-text-editor/slash-commands-extension.ts +0 -230
- package/src/components/rich-text-editor/slash-commands.css +0 -35
- package/src/components/rich-text-editor/table-styles.css +0 -65
- package/src/components/sidebar/index.tsx +0 -884
- package/src/components/spotlight-card/index.tsx +0 -191
- package/src/components/swipeable-card/index.tsx +0 -100
- package/src/components/timeline/index.tsx +0 -1183
- package/src/components/ui/accordion.tsx +0 -581
- package/src/components/ui/alert-dialog.tsx +0 -141
- package/src/components/ui/alert.tsx +0 -141
- package/src/components/ui/aspect-ratio.tsx +0 -245
- package/src/components/ui/avatar.tsx +0 -155
- package/src/components/ui/badge.tsx +0 -230
- package/src/components/ui/breadcrumb.tsx +0 -216
- package/src/components/ui/button.tsx +0 -228
- package/src/components/ui/calendar.tsx +0 -387
- package/src/components/ui/card.tsx +0 -216
- package/src/components/ui/checkbox.tsx +0 -259
- package/src/components/ui/collapsible.tsx +0 -631
- package/src/components/ui/color-picker.tsx +0 -97
- package/src/components/ui/command.tsx +0 -948
- package/src/components/ui/dialog.tsx +0 -752
- package/src/components/ui/dropdown-menu.tsx +0 -706
- package/src/components/ui/gesture-drawer.tsx +0 -11
- package/src/components/ui/hover-card.tsx +0 -29
- package/src/components/ui/index.ts +0 -222
- package/src/components/ui/input.tsx +0 -224
- package/src/components/ui/label.tsx +0 -29
- package/src/components/ui/lightbox.tsx +0 -606
- package/src/components/ui/magnetic-button.tsx +0 -129
- package/src/components/ui/media-gallery.tsx +0 -611
- package/src/components/ui/navigation-menu.tsx +0 -130
- package/src/components/ui/pagination.tsx +0 -125
- package/src/components/ui/popover.tsx +0 -185
- package/src/components/ui/progress.tsx +0 -30
- package/src/components/ui/radio-group.tsx +0 -257
- package/src/components/ui/scroll-area.tsx +0 -47
- package/src/components/ui/select.tsx +0 -378
- package/src/components/ui/separator.tsx +0 -145
- package/src/components/ui/sheet.tsx +0 -139
- package/src/components/ui/skeleton.tsx +0 -20
- package/src/components/ui/slider.tsx +0 -354
- package/src/components/ui/spotlight-card.tsx +0 -119
- package/src/components/ui/switch.tsx +0 -86
- package/src/components/ui/table.tsx +0 -331
- package/src/components/ui/tabs-pro.tsx +0 -542
- package/src/components/ui/tabs.tsx +0 -54
- package/src/components/ui/textarea.tsx +0 -28
- package/src/components/ui/toast.tsx +0 -317
- package/src/components/ui/toggle.tsx +0 -119
- package/src/components/ui/tooltip.tsx +0 -151
- package/src/components/virtual-list/index.tsx +0 -668
- package/src/hooks/use-chart.ts +0 -205
- package/src/hooks/use-data-table.ts +0 -182
- package/src/hooks/use-docs-pro-access.ts +0 -13
- package/src/hooks/use-license-check.ts +0 -65
- package/src/hooks/use-subscription.ts +0 -19
- package/src/hooks/use-toast.ts +0 -15
- package/src/index.ts +0 -22
- package/src/lib/ai-providers.ts +0 -377
- package/src/lib/component-metadata.ts +0 -18
- package/src/lib/micro-interactions.ts +0 -255
- package/src/lib/paddle.ts +0 -17
- package/src/lib/utils.ts +0 -6
- package/src/patterns/login-form/index.tsx +0 -276
- package/src/patterns/login-form/types.ts +0 -67
- package/src/setupTests.ts +0 -41
- package/src/styles/advanced-chart.css +0 -239
- package/src/styles/calendar.css +0 -35
- package/src/styles/design-system.css +0 -363
- package/src/styles/index.css +0 -681
- package/src/styles/tailwind.css +0 -7
- package/src/styles/tokens.css +0 -455
- package/src/types/next-auth.d.ts +0 -21
- package/src/use-intersection-observer.tsx +0 -154
- package/src/use-local-storage.tsx +0 -71
- package/src/use-paddle.ts +0 -138
- package/src/use-performance-optimizer.ts +0 -389
- package/src/use-pro-access.ts +0 -141
- package/src/use-scroll-animation.ts +0 -219
- package/src/use-subscription.ts +0 -37
- package/src/use-toast.ts +0 -32
- package/src/utils/chart-helpers.ts +0 -357
- package/src/utils/cn.ts +0 -6
- package/src/utils/data-processing.ts +0 -151
- 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;
|