@moontra/moonui-pro 2.0.22 → 2.1.0
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/dist/index.mjs +215 -214
- package/package.json +4 -2
- package/src/__tests__/use-intersection-observer.test.tsx +216 -0
- package/src/__tests__/use-local-storage.test.tsx +174 -0
- package/src/__tests__/use-pro-access.test.tsx +183 -0
- package/src/components/advanced-chart/advanced-chart.test.tsx +281 -0
- package/src/components/advanced-chart/index.tsx +412 -0
- package/src/components/advanced-forms/index.tsx +431 -0
- package/src/components/animated-button/index.tsx +202 -0
- package/src/components/calendar/event-dialog.tsx +372 -0
- package/src/components/calendar/index.tsx +557 -0
- package/src/components/color-picker/index.tsx +434 -0
- package/src/components/dashboard/index.tsx +334 -0
- package/src/components/data-table/data-table.test.tsx +187 -0
- package/src/components/data-table/index.tsx +368 -0
- package/src/components/draggable-list/index.tsx +100 -0
- package/src/components/enhanced/button.tsx +360 -0
- package/src/components/enhanced/card.tsx +272 -0
- package/src/components/enhanced/dialog.tsx +248 -0
- package/src/components/enhanced/index.ts +3 -0
- package/src/components/error-boundary/index.tsx +111 -0
- package/src/components/file-upload/file-upload.test.tsx +242 -0
- package/src/components/file-upload/index.tsx +362 -0
- package/src/components/floating-action-button/index.tsx +209 -0
- package/src/components/github-stars/index.tsx +414 -0
- package/src/components/health-check/index.tsx +441 -0
- package/src/components/hover-card-3d/index.tsx +170 -0
- package/src/components/index.ts +76 -0
- package/src/components/kanban/index.tsx +436 -0
- package/src/components/lazy-component/index.tsx +342 -0
- package/src/components/magnetic-button/index.tsx +170 -0
- package/src/components/memory-efficient-data/index.tsx +352 -0
- package/src/components/optimized-image/index.tsx +427 -0
- package/src/components/performance-debugger/index.tsx +591 -0
- package/src/components/performance-monitor/index.tsx +775 -0
- package/src/components/pinch-zoom/index.tsx +172 -0
- package/src/components/rich-text-editor/index-old-backup.tsx +443 -0
- package/src/components/rich-text-editor/index.tsx +1537 -0
- package/src/components/rich-text-editor/slash-commands-extension.ts +220 -0
- package/src/components/rich-text-editor/slash-commands.css +35 -0
- package/src/components/rich-text-editor/table-styles.css +65 -0
- package/src/components/spotlight-card/index.tsx +194 -0
- package/src/components/swipeable-card/index.tsx +100 -0
- package/src/components/timeline/index.tsx +333 -0
- package/src/components/ui/animated-button.tsx +185 -0
- package/src/components/ui/avatar.tsx +135 -0
- package/src/components/ui/badge.tsx +225 -0
- package/src/components/ui/button.tsx +221 -0
- package/src/components/ui/card.tsx +141 -0
- package/src/components/ui/checkbox.tsx +256 -0
- package/src/components/ui/color-picker.tsx +95 -0
- package/src/components/ui/dialog.tsx +332 -0
- package/src/components/ui/dropdown-menu.tsx +200 -0
- package/src/components/ui/hover-card-3d.tsx +103 -0
- package/src/components/ui/index.ts +33 -0
- package/src/components/ui/input.tsx +219 -0
- package/src/components/ui/label.tsx +26 -0
- package/src/components/ui/magnetic-button.tsx +129 -0
- package/src/components/ui/popover.tsx +183 -0
- package/src/components/ui/select.tsx +273 -0
- package/src/components/ui/separator.tsx +140 -0
- package/src/components/ui/slider.tsx +351 -0
- package/src/components/ui/spotlight-card.tsx +119 -0
- package/src/components/ui/switch.tsx +83 -0
- package/src/components/ui/tabs.tsx +195 -0
- package/src/components/ui/textarea.tsx +25 -0
- package/src/components/ui/toast.tsx +313 -0
- package/src/components/ui/tooltip.tsx +152 -0
- package/src/components/virtual-list/index.tsx +369 -0
- package/src/hooks/use-chart.ts +205 -0
- package/src/hooks/use-data-table.ts +182 -0
- package/src/hooks/use-docs-pro-access.ts +13 -0
- package/src/hooks/use-license-check.ts +65 -0
- package/src/hooks/use-subscription.ts +19 -0
- package/src/index.ts +14 -0
- package/src/lib/micro-interactions.ts +255 -0
- package/src/lib/utils.ts +6 -0
- package/src/patterns/login-form/index.tsx +276 -0
- package/src/patterns/login-form/types.ts +67 -0
- package/src/setupTests.ts +41 -0
- package/src/styles/design-system.css +365 -0
- package/src/styles/index.css +4 -0
- package/src/styles/tailwind.css +6 -0
- package/src/styles/tokens.css +453 -0
- package/src/types/moonui.d.ts +22 -0
- package/src/use-intersection-observer.tsx +154 -0
- package/src/use-local-storage.tsx +71 -0
- package/src/use-paddle.ts +138 -0
- package/src/use-performance-optimizer.ts +379 -0
- package/src/use-pro-access.ts +141 -0
- package/src/use-scroll-animation.ts +221 -0
- package/src/use-subscription.ts +37 -0
- package/src/use-toast.ts +32 -0
- package/src/utils/chart-helpers.ts +257 -0
- package/src/utils/cn.ts +69 -0
- package/src/utils/data-processing.ts +151 -0
- package/src/utils/license-guard.tsx +177 -0
- package/src/utils/license-validator.tsx +183 -0
- package/src/utils/package-guard.ts +60 -0
|
@@ -0,0 +1,1537 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useState } from 'react';
|
|
4
|
+
import { useEditor, EditorContent } from '@tiptap/react';
|
|
5
|
+
import StarterKit from '@tiptap/starter-kit';
|
|
6
|
+
import Placeholder from '@tiptap/extension-placeholder';
|
|
7
|
+
import TextAlign from '@tiptap/extension-text-align';
|
|
8
|
+
import Highlight from '@tiptap/extension-highlight';
|
|
9
|
+
import Link from '@tiptap/extension-link';
|
|
10
|
+
import Image from '@tiptap/extension-image';
|
|
11
|
+
import { Table } from '@tiptap/extension-table';
|
|
12
|
+
import TableRow from '@tiptap/extension-table-row';
|
|
13
|
+
import TableCell from '@tiptap/extension-table-cell';
|
|
14
|
+
import TableHeader from '@tiptap/extension-table-header';
|
|
15
|
+
import Color from '@tiptap/extension-color';
|
|
16
|
+
import { TextStyle } from '@tiptap/extension-text-style';
|
|
17
|
+
import UnderlineExtension from '@tiptap/extension-underline';
|
|
18
|
+
import Gapcursor from '@tiptap/extension-gapcursor';
|
|
19
|
+
import TaskList from '@tiptap/extension-task-list';
|
|
20
|
+
import TaskItem from '@tiptap/extension-task-item';
|
|
21
|
+
import Typography from '@tiptap/extension-typography';
|
|
22
|
+
import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight';
|
|
23
|
+
import { common, createLowlight } from 'lowlight';
|
|
24
|
+
import { SlashCommandsExtension } from './slash-commands-extension';
|
|
25
|
+
|
|
26
|
+
// Import pro access hooks
|
|
27
|
+
// Note: DocsProAccess should be handled by consuming application
|
|
28
|
+
import { useSubscription } from '../../hooks/use-subscription';
|
|
29
|
+
import { Card, CardContent } from '../ui/card';
|
|
30
|
+
|
|
31
|
+
const lowlight = createLowlight(common);
|
|
32
|
+
import {
|
|
33
|
+
Bold,
|
|
34
|
+
Italic,
|
|
35
|
+
Underline,
|
|
36
|
+
Strikethrough,
|
|
37
|
+
Heading1,
|
|
38
|
+
Heading2,
|
|
39
|
+
Heading3,
|
|
40
|
+
List,
|
|
41
|
+
ListOrdered,
|
|
42
|
+
Quote,
|
|
43
|
+
Code,
|
|
44
|
+
Link2,
|
|
45
|
+
Image as ImageIcon,
|
|
46
|
+
Table as TableIcon,
|
|
47
|
+
AlignLeft,
|
|
48
|
+
AlignCenter,
|
|
49
|
+
AlignRight,
|
|
50
|
+
AlignJustify,
|
|
51
|
+
Highlighter,
|
|
52
|
+
Type,
|
|
53
|
+
Wand2,
|
|
54
|
+
Settings,
|
|
55
|
+
Sparkles,
|
|
56
|
+
ChevronDown,
|
|
57
|
+
Plus,
|
|
58
|
+
RefreshCw,
|
|
59
|
+
Maximize,
|
|
60
|
+
FileText,
|
|
61
|
+
Languages,
|
|
62
|
+
Check,
|
|
63
|
+
CheckSquare,
|
|
64
|
+
Undo,
|
|
65
|
+
Redo,
|
|
66
|
+
Palette,
|
|
67
|
+
Eye,
|
|
68
|
+
Edit,
|
|
69
|
+
Lock
|
|
70
|
+
} from 'lucide-react';
|
|
71
|
+
import { cn } from '../../lib/utils';
|
|
72
|
+
import { Button } from '../ui/button';
|
|
73
|
+
import {
|
|
74
|
+
DropdownMenu,
|
|
75
|
+
DropdownMenuContent,
|
|
76
|
+
DropdownMenuItem,
|
|
77
|
+
DropdownMenuSeparator,
|
|
78
|
+
DropdownMenuTrigger,
|
|
79
|
+
} from '../ui/dropdown-menu';
|
|
80
|
+
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/tooltip';
|
|
81
|
+
import {
|
|
82
|
+
Dialog,
|
|
83
|
+
DialogContent,
|
|
84
|
+
DialogDescription,
|
|
85
|
+
DialogHeader,
|
|
86
|
+
DialogTitle,
|
|
87
|
+
DialogTrigger,
|
|
88
|
+
} from '../ui/dialog';
|
|
89
|
+
import { Input } from '../ui/input';
|
|
90
|
+
import { Label } from '../ui/label';
|
|
91
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select';
|
|
92
|
+
import { Separator } from '../ui/separator';
|
|
93
|
+
import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover';
|
|
94
|
+
import { ColorPicker } from '../ui/color-picker';
|
|
95
|
+
import { Slider } from '../ui/slider';
|
|
96
|
+
import { toast } from '../ui/toast';
|
|
97
|
+
// Note: AI providers should be handled by consuming application
|
|
98
|
+
|
|
99
|
+
// Type definitions for AI functionality
|
|
100
|
+
type AIProvider = 'openai' | 'anthropic' | 'gemini'
|
|
101
|
+
|
|
102
|
+
interface AISettingsType {
|
|
103
|
+
provider: AIProvider
|
|
104
|
+
apiKey?: string
|
|
105
|
+
model?: string
|
|
106
|
+
maxTokens?: number
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
interface SlashCommand {
|
|
110
|
+
title: string
|
|
111
|
+
description: string
|
|
112
|
+
command: string
|
|
113
|
+
action: () => void
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Mock AI provider function
|
|
117
|
+
const getAIProvider = (settings: AISettingsType) => {
|
|
118
|
+
return {
|
|
119
|
+
generateText: async (prompt: string) => {
|
|
120
|
+
// Mock implementation - consuming app should provide real AI integration
|
|
121
|
+
return 'AI functionality requires proper API configuration'
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
import './slash-commands.css';
|
|
126
|
+
import './table-styles.css';
|
|
127
|
+
|
|
128
|
+
interface RichTextEditorProps {
|
|
129
|
+
value?: string;
|
|
130
|
+
onChange?: (value: string) => void;
|
|
131
|
+
placeholder?: string;
|
|
132
|
+
className?: string;
|
|
133
|
+
height?: number | string;
|
|
134
|
+
features?: {
|
|
135
|
+
bold?: boolean;
|
|
136
|
+
italic?: boolean;
|
|
137
|
+
underline?: boolean;
|
|
138
|
+
strike?: boolean;
|
|
139
|
+
heading?: boolean;
|
|
140
|
+
lists?: boolean;
|
|
141
|
+
blockquote?: boolean;
|
|
142
|
+
code?: boolean;
|
|
143
|
+
link?: boolean;
|
|
144
|
+
image?: boolean;
|
|
145
|
+
table?: boolean;
|
|
146
|
+
align?: boolean;
|
|
147
|
+
color?: boolean;
|
|
148
|
+
ai?: boolean;
|
|
149
|
+
};
|
|
150
|
+
aiConfig?: {
|
|
151
|
+
provider?: 'openai' | 'claude' | 'gemini' | 'cohere';
|
|
152
|
+
apiKey?: string;
|
|
153
|
+
model?: string;
|
|
154
|
+
temperature?: number;
|
|
155
|
+
maxTokens?: number;
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
const EditorColorPicker = ({
|
|
161
|
+
onColorSelect,
|
|
162
|
+
currentColor = "#000000"
|
|
163
|
+
}: {
|
|
164
|
+
onColorSelect: (color: string) => void;
|
|
165
|
+
currentColor?: string;
|
|
166
|
+
}) => {
|
|
167
|
+
return (
|
|
168
|
+
<ColorPicker
|
|
169
|
+
value={currentColor}
|
|
170
|
+
onChange={onColorSelect}
|
|
171
|
+
showInput={true}
|
|
172
|
+
showPresets={true}
|
|
173
|
+
size="sm"
|
|
174
|
+
presets={[
|
|
175
|
+
'#000000', '#ffffff', '#ff0000', '#00ff00', '#0000ff',
|
|
176
|
+
'#ffff00', '#ff00ff', '#00ffff', '#ff8800', '#8800ff',
|
|
177
|
+
'#88ff00', '#0088ff', '#ff0088', '#00ff88', '#888888',
|
|
178
|
+
'#f87171', '#fb923c', '#fbbf24', '#facc15', '#a3e635',
|
|
179
|
+
'#4ade80', '#34d399', '#2dd4bf', '#22d3ee', '#38bdf8',
|
|
180
|
+
'#60a5fa', '#818cf8', '#a78bfa', '#c084fc', '#e879f9',
|
|
181
|
+
'#f472b6', '#fb7185', '#f43f5e', '#ef4444', '#dc2626',
|
|
182
|
+
]}
|
|
183
|
+
/>
|
|
184
|
+
);
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const ToolbarButton = ({
|
|
188
|
+
active,
|
|
189
|
+
disabled,
|
|
190
|
+
onClick,
|
|
191
|
+
children,
|
|
192
|
+
tooltip
|
|
193
|
+
}: {
|
|
194
|
+
active?: boolean;
|
|
195
|
+
disabled?: boolean;
|
|
196
|
+
onClick: () => void;
|
|
197
|
+
children: React.ReactNode;
|
|
198
|
+
tooltip?: string;
|
|
199
|
+
}) => {
|
|
200
|
+
const button = (
|
|
201
|
+
<button
|
|
202
|
+
type="button"
|
|
203
|
+
onClick={onClick}
|
|
204
|
+
disabled={disabled}
|
|
205
|
+
className={cn(
|
|
206
|
+
"p-2 rounded hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors",
|
|
207
|
+
active && "bg-gray-100 dark:bg-gray-800",
|
|
208
|
+
disabled && "opacity-50 cursor-not-allowed"
|
|
209
|
+
)}
|
|
210
|
+
>
|
|
211
|
+
{children}
|
|
212
|
+
</button>
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
if (!tooltip) return button;
|
|
216
|
+
|
|
217
|
+
return (
|
|
218
|
+
<Tooltip>
|
|
219
|
+
<TooltipTrigger asChild>
|
|
220
|
+
{button}
|
|
221
|
+
</TooltipTrigger>
|
|
222
|
+
<TooltipContent>
|
|
223
|
+
<p>{tooltip}</p>
|
|
224
|
+
</TooltipContent>
|
|
225
|
+
</Tooltip>
|
|
226
|
+
);
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
export function RichTextEditor({
|
|
230
|
+
placeholder = 'Start writing...',
|
|
231
|
+
className,
|
|
232
|
+
height = 300,
|
|
233
|
+
features = {
|
|
234
|
+
bold: true,
|
|
235
|
+
italic: true,
|
|
236
|
+
underline: true,
|
|
237
|
+
strike: true,
|
|
238
|
+
heading: true,
|
|
239
|
+
lists: true,
|
|
240
|
+
blockquote: true,
|
|
241
|
+
code: true,
|
|
242
|
+
link: true,
|
|
243
|
+
image: true,
|
|
244
|
+
table: true,
|
|
245
|
+
align: true,
|
|
246
|
+
color: true,
|
|
247
|
+
ai: true,
|
|
248
|
+
},
|
|
249
|
+
aiConfig = {
|
|
250
|
+
provider: 'openai',
|
|
251
|
+
apiKey: '',
|
|
252
|
+
model: 'gpt-4',
|
|
253
|
+
temperature: 0.7,
|
|
254
|
+
maxTokens: 1000,
|
|
255
|
+
},
|
|
256
|
+
}: RichTextEditorProps) {
|
|
257
|
+
// Pro access kontrolü
|
|
258
|
+
const docsProAccess = { hasAccess: true }; // Pro access assumed in package
|
|
259
|
+
const { hasProAccess, isLoading } = useSubscription();
|
|
260
|
+
|
|
261
|
+
// In docs mode, always show the component
|
|
262
|
+
const canShowComponent = docsProAccess.isDocsMode || hasProAccess;
|
|
263
|
+
|
|
264
|
+
// If not in docs mode and no pro access, show upgrade prompt
|
|
265
|
+
if (!docsProAccess.isDocsMode && !isLoading && !hasProAccess) {
|
|
266
|
+
return (
|
|
267
|
+
<Card className={cn("w-full", className)}>
|
|
268
|
+
<CardContent className="py-12 text-center">
|
|
269
|
+
<div className="max-w-md mx-auto space-y-4">
|
|
270
|
+
<div className="rounded-full bg-purple-100 dark:bg-purple-900/30 p-3 w-fit mx-auto">
|
|
271
|
+
<Lock className="h-6 w-6 text-purple-600 dark:text-purple-400" />
|
|
272
|
+
</div>
|
|
273
|
+
<div>
|
|
274
|
+
<h3 className="font-semibold text-lg mb-2">Pro Feature</h3>
|
|
275
|
+
<p className="text-muted-foreground text-sm mb-4">
|
|
276
|
+
Advanced Rich Text Editor is available exclusively to MoonUI Pro subscribers.
|
|
277
|
+
</p>
|
|
278
|
+
<div className="flex gap-3 justify-center">
|
|
279
|
+
<a href="/pricing">
|
|
280
|
+
<Button size="sm">
|
|
281
|
+
<Sparkles className="mr-2 h-4 w-4" />
|
|
282
|
+
Upgrade to Pro
|
|
283
|
+
</Button>
|
|
284
|
+
</a>
|
|
285
|
+
</div>
|
|
286
|
+
</div>
|
|
287
|
+
</div>
|
|
288
|
+
</CardContent>
|
|
289
|
+
</Card>
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const [aiSettings, setAiSettings] = useState<AISettingsType>({
|
|
294
|
+
provider: aiConfig.provider || 'openai',
|
|
295
|
+
apiKey: aiConfig.apiKey || '',
|
|
296
|
+
model: aiConfig.model || 'gpt-4',
|
|
297
|
+
temperature: aiConfig.temperature || 0.7,
|
|
298
|
+
maxTokens: aiConfig.maxTokens || 1000,
|
|
299
|
+
});
|
|
300
|
+
const [isAiSettingsOpen, setIsAiSettingsOpen] = useState(false);
|
|
301
|
+
const [isProcessing, setIsProcessing] = useState(false);
|
|
302
|
+
const [isSourceView, setIsSourceView] = useState(false);
|
|
303
|
+
const [sourceContent, setSourceContent] = useState('');
|
|
304
|
+
const [currentTextColor, setCurrentTextColor] = useState('#000000');
|
|
305
|
+
const [currentBgColor, setCurrentBgColor] = useState('#ffffff');
|
|
306
|
+
|
|
307
|
+
// Slash commands tanımları
|
|
308
|
+
const slashCommands: SlashCommand[] = [
|
|
309
|
+
{
|
|
310
|
+
id: 'rewrite',
|
|
311
|
+
command: 'rewrite',
|
|
312
|
+
description: 'Rewrite selected text',
|
|
313
|
+
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>',
|
|
314
|
+
action: async (text: string) => {
|
|
315
|
+
const result = await callAI('rewrite', text);
|
|
316
|
+
return { text: result || '', error: result ? undefined : 'Failed to rewrite' };
|
|
317
|
+
}
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
id: 'expand',
|
|
321
|
+
command: 'expand',
|
|
322
|
+
description: 'Expand text with more details',
|
|
323
|
+
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>',
|
|
324
|
+
action: async (text: string) => {
|
|
325
|
+
const result = await callAI('expand', text);
|
|
326
|
+
return { text: result || '', error: result ? undefined : 'Failed to expand' };
|
|
327
|
+
}
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
id: 'summarize',
|
|
331
|
+
command: 'summarize',
|
|
332
|
+
description: 'Summarize text',
|
|
333
|
+
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>',
|
|
334
|
+
action: async (text: string) => {
|
|
335
|
+
const result = await callAI('summarize', text);
|
|
336
|
+
return { text: result || '', error: result ? undefined : 'Failed to summarize' };
|
|
337
|
+
}
|
|
338
|
+
},
|
|
339
|
+
{
|
|
340
|
+
id: 'continue',
|
|
341
|
+
command: 'continue',
|
|
342
|
+
description: 'Continue writing from here',
|
|
343
|
+
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>',
|
|
344
|
+
action: async (text: string) => {
|
|
345
|
+
const result = await callAI('continue', text);
|
|
346
|
+
return { text: result || '', error: result ? undefined : 'Failed to continue' };
|
|
347
|
+
}
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
id: 'improve',
|
|
351
|
+
command: 'improve',
|
|
352
|
+
description: 'Improve writing quality',
|
|
353
|
+
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>',
|
|
354
|
+
action: async (text: string) => {
|
|
355
|
+
const result = await callAI('improve', text);
|
|
356
|
+
return { text: result || '', error: result ? undefined : 'Failed to improve' };
|
|
357
|
+
}
|
|
358
|
+
},
|
|
359
|
+
{
|
|
360
|
+
id: 'fix',
|
|
361
|
+
command: 'fix',
|
|
362
|
+
description: 'Fix grammar and spelling',
|
|
363
|
+
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>',
|
|
364
|
+
action: async (text: string) => {
|
|
365
|
+
const result = await callAI('fix', text);
|
|
366
|
+
return { text: result || '', error: result ? undefined : 'Failed to fix' };
|
|
367
|
+
}
|
|
368
|
+
},
|
|
369
|
+
{
|
|
370
|
+
id: 'translate',
|
|
371
|
+
command: 'translate',
|
|
372
|
+
description: 'Translate to Turkish',
|
|
373
|
+
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>',
|
|
374
|
+
action: async (text: string) => {
|
|
375
|
+
const result = await callAI('translate', text);
|
|
376
|
+
return { text: result || '', error: result ? undefined : 'Failed to translate' };
|
|
377
|
+
}
|
|
378
|
+
},
|
|
379
|
+
{
|
|
380
|
+
id: 'ideas',
|
|
381
|
+
command: 'ideas',
|
|
382
|
+
description: 'Generate writing ideas',
|
|
383
|
+
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>',
|
|
384
|
+
action: async (text: string) => {
|
|
385
|
+
const result = await callAI('ideas', text);
|
|
386
|
+
return { text: result || '', error: result ? undefined : 'Failed to generate ideas' };
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
];
|
|
390
|
+
|
|
391
|
+
const editor = useEditor({
|
|
392
|
+
extensions: [
|
|
393
|
+
StarterKit.configure({
|
|
394
|
+
heading: {
|
|
395
|
+
levels: [1, 2, 3],
|
|
396
|
+
},
|
|
397
|
+
codeBlock: false, // We'll use CodeBlockLowlight instead
|
|
398
|
+
}),
|
|
399
|
+
Placeholder.configure({
|
|
400
|
+
placeholder,
|
|
401
|
+
}),
|
|
402
|
+
TextAlign.configure({
|
|
403
|
+
types: ['heading', 'paragraph'],
|
|
404
|
+
}),
|
|
405
|
+
Highlight.configure({
|
|
406
|
+
multicolor: true,
|
|
407
|
+
}),
|
|
408
|
+
Link.configure({
|
|
409
|
+
openOnClick: false,
|
|
410
|
+
HTMLAttributes: {
|
|
411
|
+
target: '_blank',
|
|
412
|
+
rel: 'noopener noreferrer',
|
|
413
|
+
},
|
|
414
|
+
}),
|
|
415
|
+
Image.configure({
|
|
416
|
+
inline: true,
|
|
417
|
+
allowBase64: true,
|
|
418
|
+
}),
|
|
419
|
+
Table.configure({
|
|
420
|
+
resizable: true,
|
|
421
|
+
}),
|
|
422
|
+
TableRow,
|
|
423
|
+
TableHeader,
|
|
424
|
+
TableCell,
|
|
425
|
+
TextStyle,
|
|
426
|
+
Color,
|
|
427
|
+
UnderlineExtension,
|
|
428
|
+
Gapcursor,
|
|
429
|
+
TaskList,
|
|
430
|
+
TaskItem.configure({
|
|
431
|
+
nested: true,
|
|
432
|
+
}),
|
|
433
|
+
Typography,
|
|
434
|
+
CodeBlockLowlight.configure({
|
|
435
|
+
lowlight,
|
|
436
|
+
}),
|
|
437
|
+
SlashCommandsExtension.configure({
|
|
438
|
+
commands: slashCommands,
|
|
439
|
+
onSelectCommand: async (command) => {
|
|
440
|
+
if (!editor) return;
|
|
441
|
+
|
|
442
|
+
const { from, to } = editor.state.selection;
|
|
443
|
+
const selectedText = editor.state.doc.textBetween(from, to, ' ');
|
|
444
|
+
|
|
445
|
+
setIsProcessing(true);
|
|
446
|
+
try {
|
|
447
|
+
const response = await command.action(selectedText || editor.getText());
|
|
448
|
+
if (response.text) {
|
|
449
|
+
editor.chain().focus().insertContent(response.text).run();
|
|
450
|
+
} else if (response.error) {
|
|
451
|
+
toast({
|
|
452
|
+
title: "AI Error",
|
|
453
|
+
description: response.error,
|
|
454
|
+
variant: "destructive",
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
} catch {
|
|
458
|
+
toast({
|
|
459
|
+
title: "Error",
|
|
460
|
+
description: "Failed to execute command",
|
|
461
|
+
variant: "destructive",
|
|
462
|
+
});
|
|
463
|
+
} finally {
|
|
464
|
+
setIsProcessing(false);
|
|
465
|
+
}
|
|
466
|
+
},
|
|
467
|
+
}),
|
|
468
|
+
],
|
|
469
|
+
content: '',
|
|
470
|
+
editorProps: {
|
|
471
|
+
attributes: {
|
|
472
|
+
class: cn(
|
|
473
|
+
'prose prose-sm sm:prose lg:prose-lg xl:prose-xl focus:outline-none min-h-[200px] p-4 max-w-none',
|
|
474
|
+
// Ensure headings show different sizes
|
|
475
|
+
'[&_h1]:text-3xl [&_h1]:font-bold [&_h1]:mb-4',
|
|
476
|
+
'[&_h2]:text-2xl [&_h2]:font-bold [&_h2]:mb-3',
|
|
477
|
+
'[&_h3]:text-xl [&_h3]:font-bold [&_h3]:mb-2',
|
|
478
|
+
'[&_h4]:text-lg [&_h4]:font-bold [&_h4]:mb-2',
|
|
479
|
+
'[&_h5]:text-base [&_h5]:font-bold [&_h5]:mb-1',
|
|
480
|
+
'[&_h6]:text-sm [&_h6]:font-bold [&_h6]:mb-1',
|
|
481
|
+
// Ensure lists show proper styling
|
|
482
|
+
'[&_ul]:list-disc [&_ul]:pl-6 [&_ul]:mb-4',
|
|
483
|
+
'[&_ol]:list-decimal [&_ol]:pl-6 [&_ol]:mb-4',
|
|
484
|
+
'[&_li]:mb-1',
|
|
485
|
+
// Table styles
|
|
486
|
+
'[&_table]:border-collapse-collapse [&_table]:w-full [&_table]:mb-4',
|
|
487
|
+
'[&_table_td]:border [&_table_td]:border-gray-300 [&_table_td]:p-2',
|
|
488
|
+
'[&_table_th]:border [&_table_th]:border-gray-300 [&_table_th]:p-2 [&_table_th]:bg-gray-50 [&_table_th]:font-semibold',
|
|
489
|
+
// Other styles
|
|
490
|
+
'[&_blockquote]:border-l-4 [&_blockquote]:border-gray-300 [&_blockquote]:pl-4 [&_blockquote]:italic',
|
|
491
|
+
'[&_pre]:bg-gray-100 [&_pre]:dark:bg-gray-800 [&_pre]:p-4 [&_pre]:rounded [&_pre]:overflow-x-auto'
|
|
492
|
+
),
|
|
493
|
+
},
|
|
494
|
+
},
|
|
495
|
+
immediatelyRender: false,
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
// AI işlevleri
|
|
499
|
+
const callAI = async (action: string, text: string) => {
|
|
500
|
+
if (!aiSettings.apiKey) {
|
|
501
|
+
toast({
|
|
502
|
+
title: "API Key Required",
|
|
503
|
+
description: "Please configure your AI settings first.",
|
|
504
|
+
variant: "destructive",
|
|
505
|
+
});
|
|
506
|
+
setIsAiSettingsOpen(true);
|
|
507
|
+
return null;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
setIsProcessing(true);
|
|
511
|
+
try {
|
|
512
|
+
const provider = getAIProvider(aiSettings);
|
|
513
|
+
let response;
|
|
514
|
+
|
|
515
|
+
switch (action) {
|
|
516
|
+
case 'rewrite':
|
|
517
|
+
response = await provider.rewrite(text);
|
|
518
|
+
break;
|
|
519
|
+
case 'expand':
|
|
520
|
+
response = await provider.expand(text);
|
|
521
|
+
break;
|
|
522
|
+
case 'summarize':
|
|
523
|
+
response = await provider.summarize(text);
|
|
524
|
+
break;
|
|
525
|
+
case 'fix':
|
|
526
|
+
response = await provider.fixGrammar(text);
|
|
527
|
+
break;
|
|
528
|
+
case 'translate':
|
|
529
|
+
// TODO: Kullanıcıdan hedef dil seçmesini iste
|
|
530
|
+
response = await provider.translate(text, 'Turkish');
|
|
531
|
+
break;
|
|
532
|
+
case 'tone_professional':
|
|
533
|
+
response = await provider.changeTone(text, 'professional');
|
|
534
|
+
break;
|
|
535
|
+
case 'tone_casual':
|
|
536
|
+
response = await provider.changeTone(text, 'casual');
|
|
537
|
+
break;
|
|
538
|
+
case 'tone_friendly':
|
|
539
|
+
response = await provider.changeTone(text, 'friendly');
|
|
540
|
+
break;
|
|
541
|
+
case 'tone_formal':
|
|
542
|
+
response = await provider.changeTone(text, 'formal');
|
|
543
|
+
break;
|
|
544
|
+
case 'continue':
|
|
545
|
+
response = await provider.continueWriting(text);
|
|
546
|
+
break;
|
|
547
|
+
case 'improve':
|
|
548
|
+
response = await provider.improveWriting(text);
|
|
549
|
+
break;
|
|
550
|
+
case 'ideas':
|
|
551
|
+
response = await provider.generateIdeas(text, 5);
|
|
552
|
+
break;
|
|
553
|
+
default:
|
|
554
|
+
response = await provider.complete(action, text);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
if (response.error) {
|
|
558
|
+
toast({
|
|
559
|
+
title: "AI Error",
|
|
560
|
+
description: response.error,
|
|
561
|
+
variant: "destructive",
|
|
562
|
+
});
|
|
563
|
+
return null;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
return response.text;
|
|
567
|
+
} catch {
|
|
568
|
+
toast({
|
|
569
|
+
title: "AI Error",
|
|
570
|
+
description: "Failed to process with AI. Please check your settings.",
|
|
571
|
+
variant: "destructive",
|
|
572
|
+
});
|
|
573
|
+
return null;
|
|
574
|
+
} finally {
|
|
575
|
+
setIsProcessing(false);
|
|
576
|
+
}
|
|
577
|
+
};
|
|
578
|
+
|
|
579
|
+
const handleAIAction = async (action: string) => {
|
|
580
|
+
if (!editor) return;
|
|
581
|
+
|
|
582
|
+
const selection = editor.state.selection;
|
|
583
|
+
const selectedText = editor.state.doc.textBetween(selection.from, selection.to, ' ');
|
|
584
|
+
|
|
585
|
+
if (!selectedText && action !== 'complete') {
|
|
586
|
+
toast({
|
|
587
|
+
title: "No text selected",
|
|
588
|
+
description: "Please select some text first.",
|
|
589
|
+
variant: "destructive",
|
|
590
|
+
});
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
const result = await callAI(action, selectedText || editor.getText());
|
|
595
|
+
|
|
596
|
+
if (result) {
|
|
597
|
+
if (selectedText) {
|
|
598
|
+
editor.chain().focus().deleteSelection().insertContent(result).run();
|
|
599
|
+
} else {
|
|
600
|
+
editor.chain().focus().insertContent(result).run();
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
};
|
|
604
|
+
|
|
605
|
+
const [linkUrl, setLinkUrl] = useState('');
|
|
606
|
+
const [imageUrl, setImageUrl] = useState('');
|
|
607
|
+
const [isLinkDialogOpen, setIsLinkDialogOpen] = useState(false);
|
|
608
|
+
const [isImageDialogOpen, setIsImageDialogOpen] = useState(false);
|
|
609
|
+
const [isTableDialogOpen, setIsTableDialogOpen] = useState(false);
|
|
610
|
+
const [tableRows, setTableRows] = useState(3);
|
|
611
|
+
const [tableCols, setTableCols] = useState(3);
|
|
612
|
+
const [isTableBorderDialogOpen, setIsTableBorderDialogOpen] = useState(false);
|
|
613
|
+
const [tableBorderWidth, setTableBorderWidth] = useState(1);
|
|
614
|
+
const [tableBorderColor, setTableBorderColor] = useState('#000000');
|
|
615
|
+
const [tableBorderStyle, setTableBorderStyle] = useState('solid');
|
|
616
|
+
|
|
617
|
+
const addImage = () => {
|
|
618
|
+
if (imageUrl && editor) {
|
|
619
|
+
editor.chain().focus().setImage({ src: imageUrl }).run();
|
|
620
|
+
setImageUrl('');
|
|
621
|
+
setIsImageDialogOpen(false);
|
|
622
|
+
}
|
|
623
|
+
};
|
|
624
|
+
|
|
625
|
+
const addLink = () => {
|
|
626
|
+
if (linkUrl && editor) {
|
|
627
|
+
if (editor.state.selection.empty) {
|
|
628
|
+
// Eğer seçim yoksa, link'i text olarak ekle
|
|
629
|
+
editor.chain().focus().insertContent(`<a href="${linkUrl}">${linkUrl}</a>`).run();
|
|
630
|
+
} else {
|
|
631
|
+
// Seçili metne link ekle
|
|
632
|
+
editor.chain().focus().extendMarkRange('link').setLink({ href: linkUrl }).run();
|
|
633
|
+
}
|
|
634
|
+
setLinkUrl('');
|
|
635
|
+
setIsLinkDialogOpen(false);
|
|
636
|
+
}
|
|
637
|
+
};
|
|
638
|
+
|
|
639
|
+
const removeLink = () => {
|
|
640
|
+
editor?.chain().focus().unsetLink().run();
|
|
641
|
+
};
|
|
642
|
+
|
|
643
|
+
const createTable = () => {
|
|
644
|
+
if (editor && tableRows > 0 && tableCols > 0) {
|
|
645
|
+
editor.chain().focus().insertTable({
|
|
646
|
+
rows: tableRows,
|
|
647
|
+
cols: tableCols,
|
|
648
|
+
withHeaderRow: true
|
|
649
|
+
}).run();
|
|
650
|
+
// Apply default border styles to the newly created table
|
|
651
|
+
setTimeout(() => {
|
|
652
|
+
applyTableBorders(tableBorderWidth, tableBorderColor, tableBorderStyle);
|
|
653
|
+
}, 100);
|
|
654
|
+
setIsTableDialogOpen(false);
|
|
655
|
+
}
|
|
656
|
+
};
|
|
657
|
+
|
|
658
|
+
const applyTableBorders = (width: number, color: string, style: string) => {
|
|
659
|
+
if (!editor) return;
|
|
660
|
+
|
|
661
|
+
const borderStyle = `${width}px ${style} ${color}`;
|
|
662
|
+
|
|
663
|
+
// Apply borders to the selected table
|
|
664
|
+
editor.view.dom.querySelectorAll('.ProseMirror table').forEach((table) => {
|
|
665
|
+
if (table instanceof HTMLElement) {
|
|
666
|
+
table.style.borderCollapse = 'collapse';
|
|
667
|
+
table.style.border = borderStyle;
|
|
668
|
+
|
|
669
|
+
// Apply borders to all cells
|
|
670
|
+
table.querySelectorAll('td, th').forEach((cell) => {
|
|
671
|
+
if (cell instanceof HTMLElement) {
|
|
672
|
+
cell.style.border = borderStyle;
|
|
673
|
+
cell.style.padding = '8px';
|
|
674
|
+
}
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
});
|
|
678
|
+
};
|
|
679
|
+
|
|
680
|
+
if (!editor) {
|
|
681
|
+
return null;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
return (
|
|
685
|
+
<div className={cn("border rounded-lg overflow-hidden bg-white dark:bg-gray-950", className)}>
|
|
686
|
+
{/* Toolbar */}
|
|
687
|
+
<TooltipProvider>
|
|
688
|
+
<div className="border-b bg-gray-50 dark:bg-gray-900 p-3">
|
|
689
|
+
<div className="flex items-center flex-wrap gap-1">
|
|
690
|
+
{/* Format Group */}
|
|
691
|
+
{(features.bold || features.italic || features.underline || features.strike || features.code) && (
|
|
692
|
+
<div className="flex items-center gap-1 shrink-0">
|
|
693
|
+
{features.bold && (
|
|
694
|
+
<ToolbarButton
|
|
695
|
+
active={editor.isActive('bold')}
|
|
696
|
+
onClick={() => editor.chain().focus().toggleBold().run()}
|
|
697
|
+
tooltip="Bold (Cmd+B)"
|
|
698
|
+
disabled={isSourceView}
|
|
699
|
+
>
|
|
700
|
+
<Bold className="w-4 h-4" />
|
|
701
|
+
</ToolbarButton>
|
|
702
|
+
)}
|
|
703
|
+
{features.italic && (
|
|
704
|
+
<ToolbarButton
|
|
705
|
+
active={editor.isActive('italic')}
|
|
706
|
+
onClick={() => editor.chain().focus().toggleItalic().run()}
|
|
707
|
+
tooltip="Italic (Cmd+I)"
|
|
708
|
+
disabled={isSourceView}
|
|
709
|
+
>
|
|
710
|
+
<Italic className="w-4 h-4" />
|
|
711
|
+
</ToolbarButton>
|
|
712
|
+
)}
|
|
713
|
+
{features.underline && (
|
|
714
|
+
<ToolbarButton
|
|
715
|
+
active={editor.isActive('underline')}
|
|
716
|
+
onClick={() => editor.chain().focus().toggleUnderline().run()}
|
|
717
|
+
tooltip="Underline (Cmd+U)"
|
|
718
|
+
disabled={isSourceView}
|
|
719
|
+
>
|
|
720
|
+
<Underline className="w-4 h-4" />
|
|
721
|
+
</ToolbarButton>
|
|
722
|
+
)}
|
|
723
|
+
{features.strike && (
|
|
724
|
+
<ToolbarButton
|
|
725
|
+
active={editor.isActive('strike')}
|
|
726
|
+
onClick={() => editor.chain().focus().toggleStrike().run()}
|
|
727
|
+
tooltip="Strikethrough"
|
|
728
|
+
disabled={isSourceView}
|
|
729
|
+
>
|
|
730
|
+
<Strikethrough className="w-4 h-4" />
|
|
731
|
+
</ToolbarButton>
|
|
732
|
+
)}
|
|
733
|
+
{features.code && (
|
|
734
|
+
<ToolbarButton
|
|
735
|
+
active={editor.isActive('code')}
|
|
736
|
+
onClick={() => editor.chain().focus().toggleCode().run()}
|
|
737
|
+
tooltip="Inline code"
|
|
738
|
+
disabled={isSourceView}
|
|
739
|
+
>
|
|
740
|
+
<Code className="w-4 h-4" />
|
|
741
|
+
</ToolbarButton>
|
|
742
|
+
)}
|
|
743
|
+
</div>
|
|
744
|
+
)}
|
|
745
|
+
|
|
746
|
+
{/* Separator */}
|
|
747
|
+
{(features.bold || features.italic || features.underline || features.strike || features.code) &&
|
|
748
|
+
(features.heading || features.align) && (
|
|
749
|
+
<div className="w-px h-6 bg-border mx-1 shrink-0"></div>
|
|
750
|
+
)}
|
|
751
|
+
|
|
752
|
+
{/* Typography Group */}
|
|
753
|
+
{(features.heading || features.align) && (
|
|
754
|
+
<div className="flex items-center gap-1 shrink-0">
|
|
755
|
+
{features.heading && (
|
|
756
|
+
<DropdownMenu>
|
|
757
|
+
<DropdownMenuTrigger asChild>
|
|
758
|
+
<Button variant="ghost" size="sm" className="h-8 px-2" disabled={isSourceView}>
|
|
759
|
+
<Type className="w-4 h-4 mr-1" />
|
|
760
|
+
<ChevronDown className="w-3 h-3" />
|
|
761
|
+
</Button>
|
|
762
|
+
</DropdownMenuTrigger>
|
|
763
|
+
<DropdownMenuContent>
|
|
764
|
+
<DropdownMenuItem onClick={() => editor.chain().focus().setParagraph().run()}>
|
|
765
|
+
Paragraph
|
|
766
|
+
</DropdownMenuItem>
|
|
767
|
+
<DropdownMenuItem onClick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()}>
|
|
768
|
+
<Heading1 className="w-4 h-4 mr-2" /> Heading 1
|
|
769
|
+
</DropdownMenuItem>
|
|
770
|
+
<DropdownMenuItem onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}>
|
|
771
|
+
<Heading2 className="w-4 h-4 mr-2" /> Heading 2
|
|
772
|
+
</DropdownMenuItem>
|
|
773
|
+
<DropdownMenuItem onClick={() => editor.chain().focus().toggleHeading({ level: 3 }).run()}>
|
|
774
|
+
<Heading3 className="w-4 h-4 mr-2" /> Heading 3
|
|
775
|
+
</DropdownMenuItem>
|
|
776
|
+
</DropdownMenuContent>
|
|
777
|
+
</DropdownMenu>
|
|
778
|
+
)}
|
|
779
|
+
|
|
780
|
+
{features.align && (
|
|
781
|
+
<>
|
|
782
|
+
<ToolbarButton
|
|
783
|
+
active={editor.isActive({ textAlign: 'left' })}
|
|
784
|
+
onClick={() => editor.chain().focus().setTextAlign('left').run()}
|
|
785
|
+
tooltip="Align left"
|
|
786
|
+
disabled={isSourceView}
|
|
787
|
+
>
|
|
788
|
+
<AlignLeft className="w-4 h-4" />
|
|
789
|
+
</ToolbarButton>
|
|
790
|
+
<ToolbarButton
|
|
791
|
+
active={editor.isActive({ textAlign: 'center' })}
|
|
792
|
+
onClick={() => editor.chain().focus().setTextAlign('center').run()}
|
|
793
|
+
tooltip="Align center"
|
|
794
|
+
disabled={isSourceView}
|
|
795
|
+
>
|
|
796
|
+
<AlignCenter className="w-4 h-4" />
|
|
797
|
+
</ToolbarButton>
|
|
798
|
+
<ToolbarButton
|
|
799
|
+
active={editor.isActive({ textAlign: 'right' })}
|
|
800
|
+
onClick={() => editor.chain().focus().setTextAlign('right').run()}
|
|
801
|
+
tooltip="Align right"
|
|
802
|
+
disabled={isSourceView}
|
|
803
|
+
>
|
|
804
|
+
<AlignRight className="w-4 h-4" />
|
|
805
|
+
</ToolbarButton>
|
|
806
|
+
<ToolbarButton
|
|
807
|
+
active={editor.isActive({ textAlign: 'justify' })}
|
|
808
|
+
onClick={() => editor.chain().focus().setTextAlign('justify').run()}
|
|
809
|
+
tooltip="Justify"
|
|
810
|
+
disabled={isSourceView}
|
|
811
|
+
>
|
|
812
|
+
<AlignJustify className="w-4 h-4" />
|
|
813
|
+
</ToolbarButton>
|
|
814
|
+
</>
|
|
815
|
+
)}
|
|
816
|
+
</div>
|
|
817
|
+
)}
|
|
818
|
+
|
|
819
|
+
{/* Separator */}
|
|
820
|
+
{(features.heading || features.align) &&
|
|
821
|
+
(features.lists || features.blockquote) && (
|
|
822
|
+
<div className="w-px h-6 bg-border mx-1 shrink-0"></div>
|
|
823
|
+
)}
|
|
824
|
+
|
|
825
|
+
{/* Lists & Structure Group */}
|
|
826
|
+
{(features.lists || features.blockquote) && (
|
|
827
|
+
<div className="flex items-center gap-1 shrink-0">
|
|
828
|
+
{features.lists && (
|
|
829
|
+
<>
|
|
830
|
+
<ToolbarButton
|
|
831
|
+
active={editor.isActive('bulletList')}
|
|
832
|
+
onClick={() => editor.chain().focus().toggleBulletList().run()}
|
|
833
|
+
tooltip="Bullet list"
|
|
834
|
+
disabled={isSourceView}
|
|
835
|
+
>
|
|
836
|
+
<List className="w-4 h-4" />
|
|
837
|
+
</ToolbarButton>
|
|
838
|
+
<ToolbarButton
|
|
839
|
+
active={editor.isActive('orderedList')}
|
|
840
|
+
onClick={() => editor.chain().focus().toggleOrderedList().run()}
|
|
841
|
+
tooltip="Numbered list"
|
|
842
|
+
disabled={isSourceView}
|
|
843
|
+
>
|
|
844
|
+
<ListOrdered className="w-4 h-4" />
|
|
845
|
+
</ToolbarButton>
|
|
846
|
+
<ToolbarButton
|
|
847
|
+
active={editor.isActive('taskList')}
|
|
848
|
+
onClick={() => editor.chain().focus().toggleTaskList().run()}
|
|
849
|
+
tooltip="Task list"
|
|
850
|
+
disabled={isSourceView}
|
|
851
|
+
>
|
|
852
|
+
<CheckSquare className="w-4 h-4" />
|
|
853
|
+
</ToolbarButton>
|
|
854
|
+
</>
|
|
855
|
+
)}
|
|
856
|
+
|
|
857
|
+
{features.blockquote && (
|
|
858
|
+
<ToolbarButton
|
|
859
|
+
active={editor.isActive('blockquote')}
|
|
860
|
+
onClick={() => editor.chain().focus().toggleBlockquote().run()}
|
|
861
|
+
tooltip="Quote"
|
|
862
|
+
disabled={isSourceView}
|
|
863
|
+
>
|
|
864
|
+
<Quote className="w-4 h-4" />
|
|
865
|
+
</ToolbarButton>
|
|
866
|
+
)}
|
|
867
|
+
</div>
|
|
868
|
+
)}
|
|
869
|
+
|
|
870
|
+
{/* Separator */}
|
|
871
|
+
{(features.lists || features.blockquote) &&
|
|
872
|
+
(features.color) && (
|
|
873
|
+
<div className="w-px h-6 bg-border mx-1 shrink-0"></div>
|
|
874
|
+
)}
|
|
875
|
+
|
|
876
|
+
{/* Colors Group */}
|
|
877
|
+
{features.color && (
|
|
878
|
+
<div className="flex items-center gap-1 shrink-0">
|
|
879
|
+
<Popover>
|
|
880
|
+
<PopoverTrigger asChild>
|
|
881
|
+
<Button
|
|
882
|
+
variant="ghost"
|
|
883
|
+
size="sm"
|
|
884
|
+
className="h-8 px-2"
|
|
885
|
+
disabled={isSourceView}
|
|
886
|
+
title="Text Color"
|
|
887
|
+
>
|
|
888
|
+
<Palette className="w-4 h-4" />
|
|
889
|
+
</Button>
|
|
890
|
+
</PopoverTrigger>
|
|
891
|
+
<PopoverContent className="w-auto p-0">
|
|
892
|
+
<EditorColorPicker
|
|
893
|
+
currentColor={currentTextColor}
|
|
894
|
+
onColorSelect={(color) => {
|
|
895
|
+
setCurrentTextColor(color);
|
|
896
|
+
editor.chain().focus().setColor(color).run();
|
|
897
|
+
}}
|
|
898
|
+
/>
|
|
899
|
+
</PopoverContent>
|
|
900
|
+
</Popover>
|
|
901
|
+
|
|
902
|
+
<Popover>
|
|
903
|
+
<PopoverTrigger asChild>
|
|
904
|
+
<Button
|
|
905
|
+
variant="ghost"
|
|
906
|
+
size="sm"
|
|
907
|
+
className="h-8 px-2"
|
|
908
|
+
disabled={isSourceView}
|
|
909
|
+
title="Background Color"
|
|
910
|
+
>
|
|
911
|
+
<Highlighter className="w-4 h-4" />
|
|
912
|
+
</Button>
|
|
913
|
+
</PopoverTrigger>
|
|
914
|
+
<PopoverContent className="w-auto p-0">
|
|
915
|
+
<EditorColorPicker
|
|
916
|
+
currentColor={currentBgColor}
|
|
917
|
+
onColorSelect={(color) => {
|
|
918
|
+
setCurrentBgColor(color);
|
|
919
|
+
editor.chain().focus().toggleHighlight({ color }).run();
|
|
920
|
+
}}
|
|
921
|
+
/>
|
|
922
|
+
</PopoverContent>
|
|
923
|
+
</Popover>
|
|
924
|
+
</div>
|
|
925
|
+
)}
|
|
926
|
+
|
|
927
|
+
{/* Separator */}
|
|
928
|
+
{(features.color) &&
|
|
929
|
+
(features.link || features.image || features.table) && (
|
|
930
|
+
<div className="w-px h-6 bg-border mx-1 shrink-0"></div>
|
|
931
|
+
)}
|
|
932
|
+
|
|
933
|
+
{/* Media & Links Group */}
|
|
934
|
+
{(features.link || features.image || features.table) && (
|
|
935
|
+
<div className="flex items-center gap-1 shrink-0">
|
|
936
|
+
{features.link && (
|
|
937
|
+
<Dialog open={isLinkDialogOpen} onOpenChange={setIsLinkDialogOpen}>
|
|
938
|
+
<DialogTrigger asChild>
|
|
939
|
+
<ToolbarButton
|
|
940
|
+
active={editor.isActive('link')}
|
|
941
|
+
tooltip="Add link"
|
|
942
|
+
disabled={isSourceView}
|
|
943
|
+
>
|
|
944
|
+
<Link2 className="w-4 h-4" />
|
|
945
|
+
</ToolbarButton>
|
|
946
|
+
</DialogTrigger>
|
|
947
|
+
<DialogContent className="sm:max-w-[425px]">
|
|
948
|
+
<DialogHeader>
|
|
949
|
+
<DialogTitle>Add Link</DialogTitle>
|
|
950
|
+
<DialogDescription>
|
|
951
|
+
Enter the URL for the link.
|
|
952
|
+
</DialogDescription>
|
|
953
|
+
</DialogHeader>
|
|
954
|
+
<div className="grid gap-4 py-4">
|
|
955
|
+
<div className="grid gap-2">
|
|
956
|
+
<Label htmlFor="url">URL</Label>
|
|
957
|
+
<Input
|
|
958
|
+
id="url"
|
|
959
|
+
value={linkUrl}
|
|
960
|
+
onChange={(e) => setLinkUrl(e.target.value)}
|
|
961
|
+
placeholder="https://example.com"
|
|
962
|
+
onKeyDown={(e) => {
|
|
963
|
+
if (e.key === 'Enter') {
|
|
964
|
+
e.preventDefault();
|
|
965
|
+
addLink();
|
|
966
|
+
}
|
|
967
|
+
}}
|
|
968
|
+
/>
|
|
969
|
+
</div>
|
|
970
|
+
</div>
|
|
971
|
+
<div className="flex justify-between">
|
|
972
|
+
{editor.isActive('link') && (
|
|
973
|
+
<Button
|
|
974
|
+
variant="destructive"
|
|
975
|
+
onClick={() => {
|
|
976
|
+
removeLink();
|
|
977
|
+
setIsLinkDialogOpen(false);
|
|
978
|
+
}}
|
|
979
|
+
>
|
|
980
|
+
Remove Link
|
|
981
|
+
</Button>
|
|
982
|
+
)}
|
|
983
|
+
<div className="ml-auto flex gap-2">
|
|
984
|
+
<Button variant="outline" onClick={() => setIsLinkDialogOpen(false)}>
|
|
985
|
+
Cancel
|
|
986
|
+
</Button>
|
|
987
|
+
<Button onClick={addLink}>Add Link</Button>
|
|
988
|
+
</div>
|
|
989
|
+
</div>
|
|
990
|
+
</DialogContent>
|
|
991
|
+
</Dialog>
|
|
992
|
+
)}
|
|
993
|
+
{features.image && (
|
|
994
|
+
<Dialog open={isImageDialogOpen} onOpenChange={setIsImageDialogOpen}>
|
|
995
|
+
<DialogTrigger asChild>
|
|
996
|
+
<ToolbarButton
|
|
997
|
+
tooltip="Add image"
|
|
998
|
+
disabled={isSourceView}
|
|
999
|
+
>
|
|
1000
|
+
<ImageIcon className="w-4 h-4" />
|
|
1001
|
+
</ToolbarButton>
|
|
1002
|
+
</DialogTrigger>
|
|
1003
|
+
<DialogContent className="sm:max-w-[425px]">
|
|
1004
|
+
<DialogHeader>
|
|
1005
|
+
<DialogTitle>Add Image</DialogTitle>
|
|
1006
|
+
<DialogDescription>
|
|
1007
|
+
Enter the URL for the image.
|
|
1008
|
+
</DialogDescription>
|
|
1009
|
+
</DialogHeader>
|
|
1010
|
+
<div className="grid gap-4 py-4">
|
|
1011
|
+
<div className="grid gap-2">
|
|
1012
|
+
<Label htmlFor="image-url">Image URL</Label>
|
|
1013
|
+
<Input
|
|
1014
|
+
id="image-url"
|
|
1015
|
+
value={imageUrl}
|
|
1016
|
+
onChange={(e) => setImageUrl(e.target.value)}
|
|
1017
|
+
placeholder="https://example.com/image.jpg"
|
|
1018
|
+
onKeyDown={(e) => {
|
|
1019
|
+
if (e.key === 'Enter') {
|
|
1020
|
+
e.preventDefault();
|
|
1021
|
+
addImage();
|
|
1022
|
+
}
|
|
1023
|
+
}}
|
|
1024
|
+
/>
|
|
1025
|
+
</div>
|
|
1026
|
+
</div>
|
|
1027
|
+
<div className="flex justify-end gap-2">
|
|
1028
|
+
<Button variant="outline" onClick={() => setIsImageDialogOpen(false)}>
|
|
1029
|
+
Cancel
|
|
1030
|
+
</Button>
|
|
1031
|
+
<Button onClick={addImage}>Add Image</Button>
|
|
1032
|
+
</div>
|
|
1033
|
+
</DialogContent>
|
|
1034
|
+
</Dialog>
|
|
1035
|
+
)}
|
|
1036
|
+
{features.table && (
|
|
1037
|
+
<>
|
|
1038
|
+
<Dialog open={isTableDialogOpen} onOpenChange={setIsTableDialogOpen}>
|
|
1039
|
+
<DialogTrigger asChild>
|
|
1040
|
+
<ToolbarButton
|
|
1041
|
+
tooltip="Create table"
|
|
1042
|
+
disabled={isSourceView}
|
|
1043
|
+
>
|
|
1044
|
+
<TableIcon className="w-4 h-4" />
|
|
1045
|
+
</ToolbarButton>
|
|
1046
|
+
</DialogTrigger>
|
|
1047
|
+
<DialogContent className="sm:max-w-[425px]">
|
|
1048
|
+
<DialogHeader>
|
|
1049
|
+
<DialogTitle>Create Table</DialogTitle>
|
|
1050
|
+
<DialogDescription>
|
|
1051
|
+
Choose the size for your new table.
|
|
1052
|
+
</DialogDescription>
|
|
1053
|
+
</DialogHeader>
|
|
1054
|
+
<div className="grid gap-4 py-4">
|
|
1055
|
+
<div className="grid grid-cols-2 gap-4">
|
|
1056
|
+
<div className="grid gap-2">
|
|
1057
|
+
<Label htmlFor="rows">Rows</Label>
|
|
1058
|
+
<Input
|
|
1059
|
+
id="rows"
|
|
1060
|
+
type="number"
|
|
1061
|
+
min="1"
|
|
1062
|
+
max="20"
|
|
1063
|
+
value={tableRows}
|
|
1064
|
+
onChange={(e) => setTableRows(parseInt(e.target.value) || 3)}
|
|
1065
|
+
/>
|
|
1066
|
+
</div>
|
|
1067
|
+
<div className="grid gap-2">
|
|
1068
|
+
<Label htmlFor="cols">Columns</Label>
|
|
1069
|
+
<Input
|
|
1070
|
+
id="cols"
|
|
1071
|
+
type="number"
|
|
1072
|
+
min="1"
|
|
1073
|
+
max="10"
|
|
1074
|
+
value={tableCols}
|
|
1075
|
+
onChange={(e) => setTableCols(parseInt(e.target.value) || 3)}
|
|
1076
|
+
/>
|
|
1077
|
+
</div>
|
|
1078
|
+
</div>
|
|
1079
|
+
<div className="flex items-center space-x-2">
|
|
1080
|
+
<input
|
|
1081
|
+
type="checkbox"
|
|
1082
|
+
id="headerRow"
|
|
1083
|
+
defaultChecked
|
|
1084
|
+
className="rounded border-gray-300"
|
|
1085
|
+
/>
|
|
1086
|
+
<Label htmlFor="headerRow" className="text-sm font-normal">
|
|
1087
|
+
Include header row
|
|
1088
|
+
</Label>
|
|
1089
|
+
</div>
|
|
1090
|
+
</div>
|
|
1091
|
+
<div className="flex justify-end gap-2">
|
|
1092
|
+
<Button variant="outline" onClick={() => setIsTableDialogOpen(false)}>
|
|
1093
|
+
Cancel
|
|
1094
|
+
</Button>
|
|
1095
|
+
<Button onClick={createTable}>Create Table</Button>
|
|
1096
|
+
</div>
|
|
1097
|
+
</DialogContent>
|
|
1098
|
+
</Dialog>
|
|
1099
|
+
<DropdownMenu>
|
|
1100
|
+
<DropdownMenuTrigger asChild>
|
|
1101
|
+
<Button variant="ghost" size="sm" className="h-8 px-1">
|
|
1102
|
+
<ChevronDown className="w-3 h-3" />
|
|
1103
|
+
</Button>
|
|
1104
|
+
</DropdownMenuTrigger>
|
|
1105
|
+
<DropdownMenuContent>
|
|
1106
|
+
<DropdownMenuItem onClick={() => editor.chain().focus().addColumnBefore().run()}>
|
|
1107
|
+
Add column before
|
|
1108
|
+
</DropdownMenuItem>
|
|
1109
|
+
<DropdownMenuItem onClick={() => editor.chain().focus().addColumnAfter().run()}>
|
|
1110
|
+
Add column after
|
|
1111
|
+
</DropdownMenuItem>
|
|
1112
|
+
<DropdownMenuItem onClick={() => editor.chain().focus().deleteColumn().run()}>
|
|
1113
|
+
Delete column
|
|
1114
|
+
</DropdownMenuItem>
|
|
1115
|
+
<DropdownMenuSeparator />
|
|
1116
|
+
<DropdownMenuItem onClick={() => editor.chain().focus().addRowBefore().run()}>
|
|
1117
|
+
Add row before
|
|
1118
|
+
</DropdownMenuItem>
|
|
1119
|
+
<DropdownMenuItem onClick={() => editor.chain().focus().addRowAfter().run()}>
|
|
1120
|
+
Add row after
|
|
1121
|
+
</DropdownMenuItem>
|
|
1122
|
+
<DropdownMenuItem onClick={() => editor.chain().focus().deleteRow().run()}>
|
|
1123
|
+
Delete row
|
|
1124
|
+
</DropdownMenuItem>
|
|
1125
|
+
<DropdownMenuSeparator />
|
|
1126
|
+
<DropdownMenuItem onClick={() => setIsTableBorderDialogOpen(true)}>
|
|
1127
|
+
<Settings className="w-4 h-4 mr-2" />
|
|
1128
|
+
Table Borders
|
|
1129
|
+
</DropdownMenuItem>
|
|
1130
|
+
<DropdownMenuSeparator />
|
|
1131
|
+
<DropdownMenuItem onClick={() => editor.chain().focus().deleteTable().run()}>
|
|
1132
|
+
Delete table
|
|
1133
|
+
</DropdownMenuItem>
|
|
1134
|
+
</DropdownMenuContent>
|
|
1135
|
+
</DropdownMenu>
|
|
1136
|
+
</>
|
|
1137
|
+
)}
|
|
1138
|
+
</div>
|
|
1139
|
+
)}
|
|
1140
|
+
|
|
1141
|
+
{/* Separator */}
|
|
1142
|
+
{(features.link || features.image || features.table) &&
|
|
1143
|
+
(true) && (
|
|
1144
|
+
<div className="w-px h-6 bg-border mx-1 shrink-0"></div>
|
|
1145
|
+
)}
|
|
1146
|
+
|
|
1147
|
+
{/* Tools Group */}
|
|
1148
|
+
<div className="flex items-center gap-1 shrink-0">
|
|
1149
|
+
<ToolbarButton
|
|
1150
|
+
onClick={() => editor.chain().focus().undo().run()}
|
|
1151
|
+
disabled={!editor.can().undo() || isSourceView}
|
|
1152
|
+
tooltip="Undo (Cmd+Z)"
|
|
1153
|
+
>
|
|
1154
|
+
<Undo className="w-4 h-4" />
|
|
1155
|
+
</ToolbarButton>
|
|
1156
|
+
<ToolbarButton
|
|
1157
|
+
onClick={() => editor.chain().focus().redo().run()}
|
|
1158
|
+
disabled={!editor.can().redo() || isSourceView}
|
|
1159
|
+
tooltip="Redo (Cmd+Shift+Z)"
|
|
1160
|
+
>
|
|
1161
|
+
<Redo className="w-4 h-4" />
|
|
1162
|
+
</ToolbarButton>
|
|
1163
|
+
|
|
1164
|
+
<ToolbarButton
|
|
1165
|
+
active={isSourceView}
|
|
1166
|
+
onClick={() => {
|
|
1167
|
+
if (isSourceView) {
|
|
1168
|
+
// Apply source changes when switching back to visual mode
|
|
1169
|
+
editor.commands.setContent(sourceContent);
|
|
1170
|
+
} else {
|
|
1171
|
+
// Update source content when switching to source view
|
|
1172
|
+
setSourceContent(editor.getHTML());
|
|
1173
|
+
}
|
|
1174
|
+
setIsSourceView(!isSourceView);
|
|
1175
|
+
}}
|
|
1176
|
+
tooltip={isSourceView ? "Visual Mode" : "Source Code"}
|
|
1177
|
+
>
|
|
1178
|
+
{isSourceView ? <Edit className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
|
|
1179
|
+
</ToolbarButton>
|
|
1180
|
+
|
|
1181
|
+
{/* AI Tools */}
|
|
1182
|
+
{features.ai && (
|
|
1183
|
+
<>
|
|
1184
|
+
<DropdownMenu>
|
|
1185
|
+
<DropdownMenuTrigger asChild>
|
|
1186
|
+
<Button
|
|
1187
|
+
variant="ghost"
|
|
1188
|
+
size="sm"
|
|
1189
|
+
className="h-8 px-3 bg-purple-100 hover:bg-purple-200 dark:bg-purple-900 dark:hover:bg-purple-800"
|
|
1190
|
+
disabled={isProcessing}
|
|
1191
|
+
>
|
|
1192
|
+
{isProcessing ? (
|
|
1193
|
+
<RefreshCw className="w-4 h-4 mr-1 animate-spin" />
|
|
1194
|
+
) : (
|
|
1195
|
+
<Wand2 className="w-4 h-4 mr-1" />
|
|
1196
|
+
)}
|
|
1197
|
+
AI Tools
|
|
1198
|
+
</Button>
|
|
1199
|
+
</DropdownMenuTrigger>
|
|
1200
|
+
<DropdownMenuContent className="w-56">
|
|
1201
|
+
<DropdownMenuItem onClick={() => handleAIAction('rewrite')}>
|
|
1202
|
+
<RefreshCw className="w-4 h-4 mr-2" />
|
|
1203
|
+
Rewrite Selection
|
|
1204
|
+
</DropdownMenuItem>
|
|
1205
|
+
<DropdownMenuItem onClick={() => handleAIAction('improve')}>
|
|
1206
|
+
<Wand2 className="w-4 h-4 mr-2" />
|
|
1207
|
+
Improve Writing
|
|
1208
|
+
</DropdownMenuItem>
|
|
1209
|
+
<DropdownMenuItem onClick={() => handleAIAction('expand')}>
|
|
1210
|
+
<Maximize className="w-4 h-4 mr-2" />
|
|
1211
|
+
Expand Text
|
|
1212
|
+
</DropdownMenuItem>
|
|
1213
|
+
<DropdownMenuItem onClick={() => handleAIAction('summarize')}>
|
|
1214
|
+
<FileText className="w-4 h-4 mr-2" />
|
|
1215
|
+
Summarize
|
|
1216
|
+
</DropdownMenuItem>
|
|
1217
|
+
<DropdownMenuItem onClick={() => handleAIAction('continue')}>
|
|
1218
|
+
<Plus className="w-4 h-4 mr-2" />
|
|
1219
|
+
Continue Writing
|
|
1220
|
+
</DropdownMenuItem>
|
|
1221
|
+
<DropdownMenuSeparator />
|
|
1222
|
+
<DropdownMenuItem onClick={() => handleAIAction('fix')}>
|
|
1223
|
+
<Check className="w-4 h-4 mr-2" />
|
|
1224
|
+
Fix Grammar & Spelling
|
|
1225
|
+
</DropdownMenuItem>
|
|
1226
|
+
<DropdownMenuSeparator />
|
|
1227
|
+
<DropdownMenuItem onClick={() => handleAIAction('tone_professional')}>
|
|
1228
|
+
<Sparkles className="w-4 h-4 mr-2" />
|
|
1229
|
+
Make Professional
|
|
1230
|
+
</DropdownMenuItem>
|
|
1231
|
+
<DropdownMenuItem onClick={() => handleAIAction('tone_casual')}>
|
|
1232
|
+
<Sparkles className="w-4 h-4 mr-2" />
|
|
1233
|
+
Make Casual
|
|
1234
|
+
</DropdownMenuItem>
|
|
1235
|
+
<DropdownMenuItem onClick={() => handleAIAction('tone_friendly')}>
|
|
1236
|
+
<Sparkles className="w-4 h-4 mr-2" />
|
|
1237
|
+
Make Friendly
|
|
1238
|
+
</DropdownMenuItem>
|
|
1239
|
+
<DropdownMenuItem onClick={() => handleAIAction('tone_formal')}>
|
|
1240
|
+
<Sparkles className="w-4 h-4 mr-2" />
|
|
1241
|
+
Make Formal
|
|
1242
|
+
</DropdownMenuItem>
|
|
1243
|
+
<DropdownMenuSeparator />
|
|
1244
|
+
<DropdownMenuItem onClick={() => handleAIAction('translate')}>
|
|
1245
|
+
<Languages className="w-4 h-4 mr-2" />
|
|
1246
|
+
Translate to Turkish
|
|
1247
|
+
</DropdownMenuItem>
|
|
1248
|
+
<DropdownMenuItem onClick={() => handleAIAction('ideas')}>
|
|
1249
|
+
<Sparkles className="w-4 h-4 mr-2" />
|
|
1250
|
+
Generate Ideas
|
|
1251
|
+
</DropdownMenuItem>
|
|
1252
|
+
</DropdownMenuContent>
|
|
1253
|
+
</DropdownMenu>
|
|
1254
|
+
|
|
1255
|
+
<Dialog open={isAiSettingsOpen} onOpenChange={setIsAiSettingsOpen}>
|
|
1256
|
+
<DialogTrigger asChild>
|
|
1257
|
+
<ToolbarButton
|
|
1258
|
+
tooltip="AI Settings - Configure your API keys here"
|
|
1259
|
+
disabled={isSourceView}
|
|
1260
|
+
>
|
|
1261
|
+
<Settings className="w-4 h-4" />
|
|
1262
|
+
</ToolbarButton>
|
|
1263
|
+
</DialogTrigger>
|
|
1264
|
+
<DialogContent className="sm:max-w-[425px]">
|
|
1265
|
+
<DialogHeader>
|
|
1266
|
+
<DialogTitle>AI Settings</DialogTitle>
|
|
1267
|
+
<DialogDescription>
|
|
1268
|
+
Configure your AI provider and API settings.
|
|
1269
|
+
</DialogDescription>
|
|
1270
|
+
</DialogHeader>
|
|
1271
|
+
<div className="grid gap-4 py-4">
|
|
1272
|
+
<div className="grid gap-2">
|
|
1273
|
+
<Label htmlFor="provider">Provider</Label>
|
|
1274
|
+
<Select
|
|
1275
|
+
value={aiSettings.provider}
|
|
1276
|
+
onValueChange={(value: 'openai' | 'claude' | 'gemini' | 'cohere') => setAiSettings({ ...aiSettings, provider: value })}
|
|
1277
|
+
>
|
|
1278
|
+
<SelectTrigger>
|
|
1279
|
+
<SelectValue />
|
|
1280
|
+
</SelectTrigger>
|
|
1281
|
+
<SelectContent>
|
|
1282
|
+
<SelectItem value="openai">OpenAI</SelectItem>
|
|
1283
|
+
<SelectItem value="claude">Claude (Anthropic)</SelectItem>
|
|
1284
|
+
<SelectItem value="gemini">Gemini (Google)</SelectItem>
|
|
1285
|
+
<SelectItem value="cohere">Cohere</SelectItem>
|
|
1286
|
+
</SelectContent>
|
|
1287
|
+
</Select>
|
|
1288
|
+
</div>
|
|
1289
|
+
<div className="grid gap-2">
|
|
1290
|
+
<Label htmlFor="apiKey">API Key</Label>
|
|
1291
|
+
<Input
|
|
1292
|
+
id="apiKey"
|
|
1293
|
+
type="password"
|
|
1294
|
+
value={aiSettings.apiKey}
|
|
1295
|
+
onChange={(e) => setAiSettings({ ...aiSettings, apiKey: e.target.value })}
|
|
1296
|
+
placeholder="sk-..."
|
|
1297
|
+
/>
|
|
1298
|
+
</div>
|
|
1299
|
+
<div className="grid gap-2">
|
|
1300
|
+
<Label htmlFor="model">Model</Label>
|
|
1301
|
+
<Select
|
|
1302
|
+
value={aiSettings.model}
|
|
1303
|
+
onValueChange={(value) => setAiSettings({ ...aiSettings, model: value })}
|
|
1304
|
+
>
|
|
1305
|
+
<SelectTrigger>
|
|
1306
|
+
<SelectValue />
|
|
1307
|
+
</SelectTrigger>
|
|
1308
|
+
<SelectContent>
|
|
1309
|
+
{aiSettings.provider === 'openai' && (
|
|
1310
|
+
<>
|
|
1311
|
+
<SelectItem value="gpt-4">GPT-4</SelectItem>
|
|
1312
|
+
<SelectItem value="gpt-3.5-turbo">GPT-3.5 Turbo</SelectItem>
|
|
1313
|
+
</>
|
|
1314
|
+
)}
|
|
1315
|
+
{aiSettings.provider === 'claude' && (
|
|
1316
|
+
<>
|
|
1317
|
+
<SelectItem value="claude-3-opus">Claude 3 Opus</SelectItem>
|
|
1318
|
+
<SelectItem value="claude-3-sonnet">Claude 3 Sonnet</SelectItem>
|
|
1319
|
+
<SelectItem value="claude-3-haiku">Claude 3 Haiku</SelectItem>
|
|
1320
|
+
</>
|
|
1321
|
+
)}
|
|
1322
|
+
{aiSettings.provider === 'gemini' && (
|
|
1323
|
+
<>
|
|
1324
|
+
<SelectItem value="gemini-pro">Gemini Pro</SelectItem>
|
|
1325
|
+
<SelectItem value="gemini-pro-vision">Gemini Pro Vision</SelectItem>
|
|
1326
|
+
</>
|
|
1327
|
+
)}
|
|
1328
|
+
{aiSettings.provider === 'cohere' && (
|
|
1329
|
+
<>
|
|
1330
|
+
<SelectItem value="command">Command</SelectItem>
|
|
1331
|
+
<SelectItem value="command-light">Command Light</SelectItem>
|
|
1332
|
+
</>
|
|
1333
|
+
)}
|
|
1334
|
+
</SelectContent>
|
|
1335
|
+
</Select>
|
|
1336
|
+
</div>
|
|
1337
|
+
<div className="grid grid-cols-2 gap-4">
|
|
1338
|
+
<div className="grid gap-2">
|
|
1339
|
+
<Label htmlFor="temperature">Temperature</Label>
|
|
1340
|
+
<Input
|
|
1341
|
+
id="temperature"
|
|
1342
|
+
type="number"
|
|
1343
|
+
min="0"
|
|
1344
|
+
max="2"
|
|
1345
|
+
step="0.1"
|
|
1346
|
+
value={aiSettings.temperature}
|
|
1347
|
+
onChange={(e) => setAiSettings({ ...aiSettings, temperature: parseFloat(e.target.value) })}
|
|
1348
|
+
/>
|
|
1349
|
+
</div>
|
|
1350
|
+
<div className="grid gap-2">
|
|
1351
|
+
<Label htmlFor="maxTokens">Max Tokens</Label>
|
|
1352
|
+
<Input
|
|
1353
|
+
id="maxTokens"
|
|
1354
|
+
type="number"
|
|
1355
|
+
min="1"
|
|
1356
|
+
max="4000"
|
|
1357
|
+
value={aiSettings.maxTokens}
|
|
1358
|
+
onChange={(e) => setAiSettings({ ...aiSettings, maxTokens: parseInt(e.target.value) })}
|
|
1359
|
+
/>
|
|
1360
|
+
</div>
|
|
1361
|
+
</div>
|
|
1362
|
+
</div>
|
|
1363
|
+
<div className="flex justify-end">
|
|
1364
|
+
<Button onClick={() => {
|
|
1365
|
+
setIsAiSettingsOpen(false);
|
|
1366
|
+
toast({
|
|
1367
|
+
title: "Settings saved",
|
|
1368
|
+
description: "Your AI settings have been updated.",
|
|
1369
|
+
});
|
|
1370
|
+
}}>
|
|
1371
|
+
Save Settings
|
|
1372
|
+
</Button>
|
|
1373
|
+
</div>
|
|
1374
|
+
</DialogContent>
|
|
1375
|
+
</Dialog>
|
|
1376
|
+
</>
|
|
1377
|
+
)}
|
|
1378
|
+
</div>
|
|
1379
|
+
</div>
|
|
1380
|
+
</div>
|
|
1381
|
+
</TooltipProvider>
|
|
1382
|
+
|
|
1383
|
+
{/* Table Border Settings Dialog */}
|
|
1384
|
+
<Dialog open={isTableBorderDialogOpen} onOpenChange={setIsTableBorderDialogOpen}>
|
|
1385
|
+
<DialogContent className="sm:max-w-[425px]">
|
|
1386
|
+
<DialogHeader>
|
|
1387
|
+
<DialogTitle>Table Border Settings</DialogTitle>
|
|
1388
|
+
<DialogDescription>
|
|
1389
|
+
Customize the appearance of table borders.
|
|
1390
|
+
</DialogDescription>
|
|
1391
|
+
</DialogHeader>
|
|
1392
|
+
<div className="grid gap-6 py-4">
|
|
1393
|
+
<div className="grid gap-3">
|
|
1394
|
+
<Label htmlFor="border-width">Border Width</Label>
|
|
1395
|
+
<div className="flex items-center gap-4">
|
|
1396
|
+
<Slider
|
|
1397
|
+
id="border-width"
|
|
1398
|
+
min={0}
|
|
1399
|
+
max={5}
|
|
1400
|
+
step={1}
|
|
1401
|
+
value={[tableBorderWidth]}
|
|
1402
|
+
onValueChange={(value) => setTableBorderWidth(value[0])}
|
|
1403
|
+
className="flex-1"
|
|
1404
|
+
/>
|
|
1405
|
+
<span className="text-sm font-medium w-8">{tableBorderWidth}px</span>
|
|
1406
|
+
</div>
|
|
1407
|
+
</div>
|
|
1408
|
+
|
|
1409
|
+
<div className="grid gap-3">
|
|
1410
|
+
<Label htmlFor="border-color">Border Color</Label>
|
|
1411
|
+
<div className="flex items-center gap-2">
|
|
1412
|
+
<ColorPicker
|
|
1413
|
+
value={tableBorderColor}
|
|
1414
|
+
onChange={setTableBorderColor}
|
|
1415
|
+
showInput={true}
|
|
1416
|
+
showPresets={false}
|
|
1417
|
+
size="sm"
|
|
1418
|
+
/>
|
|
1419
|
+
<span className="text-sm text-muted-foreground">{tableBorderColor}</span>
|
|
1420
|
+
</div>
|
|
1421
|
+
</div>
|
|
1422
|
+
|
|
1423
|
+
<div className="grid gap-3">
|
|
1424
|
+
<Label htmlFor="border-style">Border Style</Label>
|
|
1425
|
+
<Select value={tableBorderStyle} onValueChange={setTableBorderStyle}>
|
|
1426
|
+
<SelectTrigger>
|
|
1427
|
+
<SelectValue />
|
|
1428
|
+
</SelectTrigger>
|
|
1429
|
+
<SelectContent>
|
|
1430
|
+
<SelectItem value="solid">Solid</SelectItem>
|
|
1431
|
+
<SelectItem value="dashed">Dashed</SelectItem>
|
|
1432
|
+
<SelectItem value="dotted">Dotted</SelectItem>
|
|
1433
|
+
<SelectItem value="double">Double</SelectItem>
|
|
1434
|
+
<SelectItem value="groove">Groove</SelectItem>
|
|
1435
|
+
<SelectItem value="ridge">Ridge</SelectItem>
|
|
1436
|
+
<SelectItem value="inset">Inset</SelectItem>
|
|
1437
|
+
<SelectItem value="outset">Outset</SelectItem>
|
|
1438
|
+
</SelectContent>
|
|
1439
|
+
</Select>
|
|
1440
|
+
</div>
|
|
1441
|
+
|
|
1442
|
+
{/* Preview */}
|
|
1443
|
+
<div className="grid gap-3">
|
|
1444
|
+
<Label>Preview</Label>
|
|
1445
|
+
<div className="p-4 bg-gray-50 dark:bg-gray-900 rounded-lg">
|
|
1446
|
+
<table
|
|
1447
|
+
className="w-full text-sm"
|
|
1448
|
+
style={{
|
|
1449
|
+
borderCollapse: 'collapse',
|
|
1450
|
+
border: `${tableBorderWidth}px ${tableBorderStyle} ${tableBorderColor}`
|
|
1451
|
+
}}
|
|
1452
|
+
>
|
|
1453
|
+
<thead>
|
|
1454
|
+
<tr>
|
|
1455
|
+
<th
|
|
1456
|
+
style={{
|
|
1457
|
+
border: `${tableBorderWidth}px ${tableBorderStyle} ${tableBorderColor}`,
|
|
1458
|
+
padding: '8px',
|
|
1459
|
+
textAlign: 'left'
|
|
1460
|
+
}}
|
|
1461
|
+
>
|
|
1462
|
+
Header 1
|
|
1463
|
+
</th>
|
|
1464
|
+
<th
|
|
1465
|
+
style={{
|
|
1466
|
+
border: `${tableBorderWidth}px ${tableBorderStyle} ${tableBorderColor}`,
|
|
1467
|
+
padding: '8px',
|
|
1468
|
+
textAlign: 'left'
|
|
1469
|
+
}}
|
|
1470
|
+
>
|
|
1471
|
+
Header 2
|
|
1472
|
+
</th>
|
|
1473
|
+
</tr>
|
|
1474
|
+
</thead>
|
|
1475
|
+
<tbody>
|
|
1476
|
+
<tr>
|
|
1477
|
+
<td
|
|
1478
|
+
style={{
|
|
1479
|
+
border: `${tableBorderWidth}px ${tableBorderStyle} ${tableBorderColor}`,
|
|
1480
|
+
padding: '8px'
|
|
1481
|
+
}}
|
|
1482
|
+
>
|
|
1483
|
+
Cell 1
|
|
1484
|
+
</td>
|
|
1485
|
+
<td
|
|
1486
|
+
style={{
|
|
1487
|
+
border: `${tableBorderWidth}px ${tableBorderStyle} ${tableBorderColor}`,
|
|
1488
|
+
padding: '8px'
|
|
1489
|
+
}}
|
|
1490
|
+
>
|
|
1491
|
+
Cell 2
|
|
1492
|
+
</td>
|
|
1493
|
+
</tr>
|
|
1494
|
+
</tbody>
|
|
1495
|
+
</table>
|
|
1496
|
+
</div>
|
|
1497
|
+
</div>
|
|
1498
|
+
</div>
|
|
1499
|
+
<div className="flex justify-end gap-2">
|
|
1500
|
+
<Button variant="outline" onClick={() => setIsTableBorderDialogOpen(false)}>
|
|
1501
|
+
Cancel
|
|
1502
|
+
</Button>
|
|
1503
|
+
<Button onClick={() => {
|
|
1504
|
+
applyTableBorders(tableBorderWidth, tableBorderColor, tableBorderStyle);
|
|
1505
|
+
setIsTableBorderDialogOpen(false);
|
|
1506
|
+
toast({
|
|
1507
|
+
title: "Borders applied",
|
|
1508
|
+
description: "Table borders have been updated.",
|
|
1509
|
+
});
|
|
1510
|
+
}}>
|
|
1511
|
+
Apply Borders
|
|
1512
|
+
</Button>
|
|
1513
|
+
</div>
|
|
1514
|
+
</DialogContent>
|
|
1515
|
+
</Dialog>
|
|
1516
|
+
|
|
1517
|
+
{/* Editor */}
|
|
1518
|
+
<div
|
|
1519
|
+
className="overflow-auto"
|
|
1520
|
+
style={{ height: typeof height === 'number' ? `${height}px` : height }}
|
|
1521
|
+
>
|
|
1522
|
+
{isSourceView ? (
|
|
1523
|
+
<textarea
|
|
1524
|
+
value={sourceContent}
|
|
1525
|
+
onChange={(e) => setSourceContent(e.target.value)}
|
|
1526
|
+
className="w-full h-full p-4 font-mono text-sm resize-none focus:outline-none bg-gray-50 dark:bg-gray-900"
|
|
1527
|
+
placeholder="HTML source code..."
|
|
1528
|
+
/>
|
|
1529
|
+
) : (
|
|
1530
|
+
<EditorContent editor={editor} />
|
|
1531
|
+
)}
|
|
1532
|
+
</div>
|
|
1533
|
+
</div>
|
|
1534
|
+
);
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
export default RichTextEditor;
|