@moontra/moonui-pro 2.8.10 → 2.8.12

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 CHANGED
@@ -3,7 +3,7 @@ import { twMerge } from 'tailwind-merge';
3
3
  import * as t from 'react';
4
4
  import t__default, { useState, useRef, useCallback, forwardRef, createContext, useEffect, useContext, useMemo, useLayoutEffect, useDebugValue, Component } from 'react';
5
5
  import * as AccordionPrimitive from '@radix-ui/react-accordion';
6
- import { ChevronDown, Info, AlertCircle, AlertTriangle, Check, X, MoreHorizontal, Loader2, Minus, Search, ChevronRight, Circle, ChevronUp, Lock, Sparkles, Plus, CreditCard, Globe, CheckCircle2, XCircle, RotateCcw, Download, Clock, HelpCircle, ChevronLeft, Calendar as Calendar$1, Edit, Trash2, MapPin, User, GripVertical, MessageCircle, Paperclip, Bold as Bold$1, Italic as Italic$1, Underline as Underline$1, Strikethrough, Code as Code$1, Type, Heading1, Heading2, Heading3, AlignLeft, AlignCenter, AlignRight, AlignJustify, List, ListOrdered, CheckSquare, Quote, Palette, Highlighter, Link2, Image as Image$1, Table as Table$1, Settings, Undo, Redo, Eye, RefreshCw, Wand2, Maximize, FileText, Briefcase, MessageSquare, Heart, GraduationCap, Zap, Languages, Lightbulb, TrendingUp, TrendingDown, ZoomOut, ZoomIn, FileSpreadsheet, FileJson, Maximize2, Move, Menu, Bell, CheckCheck, CheckCircle, Settings2, LogOut, Edit3, LayoutGrid, Upload, Share2, Save, Filter, FileDown, ArrowUp, ArrowDown, ArrowUpDown, ChevronsLeft, ChevronsRight, Pin, Sun, Moon, Monitor, Star, ExternalLink, CalendarIcon, DollarSign, Users, Github, GitFork, Activity, Server, EyeOff, RotateCw, Timer, Cpu, MemoryStick, HardDrive, Network, BarChart3, Video, Music, Archive, File, Columns, Grip, Unlock, Minimize2, Map as Map$1, Target, MoreVertical, BellOff, ArrowDownRight, ArrowUpRight } from 'lucide-react';
6
+ import { ChevronDown, Info, AlertCircle, AlertTriangle, Check, X, MoreHorizontal, Loader2, Minus, Search, ChevronRight, Circle, ChevronUp, Lock, Sparkles, Plus, CreditCard, Globe, CheckCircle2, XCircle, RotateCcw, Download, Clock, HelpCircle, ChevronLeft, Calendar as Calendar$1, Edit, Trash2, MapPin, User, GripVertical, MessageCircle, Paperclip, Bold as Bold$1, Italic as Italic$1, Underline as Underline$1, Strikethrough, Code as Code$1, Type, Heading1, Heading2, Heading3, AlignLeft, AlignCenter, AlignRight, AlignJustify, List, ListOrdered, CheckSquare, Quote, Palette, Highlighter, Link2, Image as Image$1, Table as Table$1, Settings, Undo, Redo, Eye, RefreshCw, Wand2, Maximize, FileText, Briefcase, MessageSquare, Heart, GraduationCap, Zap, Languages, Lightbulb, Copy, TrendingUp, TrendingDown, ZoomOut, ZoomIn, FileSpreadsheet, FileJson, Maximize2, Move, Menu, Bell, CheckCheck, CheckCircle, Settings2, LogOut, Edit3, LayoutGrid, Upload, Share2, Save, Filter, FileDown, ArrowUp, ArrowDown, ArrowUpDown, ChevronsLeft, ChevronsRight, Pin, Sun, Moon, Monitor, Star, ExternalLink, CalendarIcon, DollarSign, Users, Github, GitFork, Activity, Server, EyeOff, RotateCw, Timer, Cpu, MemoryStick, HardDrive, Network, BarChart3, Video, Music, Archive, File, Columns, Grip, Unlock, Minimize2, Map as Map$1, Target, MoreVertical, BellOff, ArrowDownRight, ArrowUpRight } from 'lucide-react';
7
7
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
8
8
  import { cva } from 'class-variance-authority';
9
9
  import * as AvatarPrimitive from '@radix-ui/react-avatar';
@@ -4044,7 +4044,7 @@ var me = t.forwardRef((r2, o) => {
4044
4044
  var e;
4045
4045
  return Array.from(((e = I.current) == null ? void 0 : e.querySelectorAll(ce)) || []);
4046
4046
  }
4047
- function X19(e) {
4047
+ function X20(e) {
4048
4048
  let s = V()[e];
4049
4049
  s && E.setState("value", s.getAttribute(T));
4050
4050
  }
@@ -4059,10 +4059,10 @@ var me = t.forwardRef((r2, o) => {
4059
4059
  s = e > 0 ? we(s, N) : De(s, N), i = s == null ? void 0 : s.querySelector(ce);
4060
4060
  i ? E.setState("value", i.getAttribute(T)) : Q(e);
4061
4061
  }
4062
- let oe = () => X19(V().length - 1), ie3 = (e) => {
4062
+ let oe = () => X20(V().length - 1), ie3 = (e) => {
4063
4063
  e.preventDefault(), e.metaKey ? oe() : e.altKey ? re(1) : Q(1);
4064
4064
  }, se = (e) => {
4065
- e.preventDefault(), e.metaKey ? X19(0) : e.altKey ? re(-1) : Q(-1);
4065
+ e.preventDefault(), e.metaKey ? X20(0) : e.altKey ? re(-1) : Q(-1);
4066
4066
  };
4067
4067
  return t.createElement(Primitive2.div, { ref: o, tabIndex: -1, ...O, "cmdk-root": "", onKeyDown: (e) => {
4068
4068
  var s;
@@ -4089,7 +4089,7 @@ var me = t.forwardRef((r2, o) => {
4089
4089
  break;
4090
4090
  }
4091
4091
  case "Home": {
4092
- e.preventDefault(), X19(0);
4092
+ e.preventDefault(), X20(0);
4093
4093
  break;
4094
4094
  }
4095
4095
  case "End": {
@@ -50283,53 +50283,78 @@ var EditorColorPicker = ({
50283
50283
  onColorSelect,
50284
50284
  currentColor = "#000000"
50285
50285
  }) => {
50286
- return /* @__PURE__ */ jsx(
50287
- MoonUIColorPickerPro,
50288
- {
50289
- value: currentColor,
50290
- onChange: onColorSelect,
50291
- showInput: true,
50292
- showPresets: true,
50293
- size: "sm",
50294
- presets: [
50295
- "#000000",
50296
- "#ffffff",
50297
- "#ff0000",
50298
- "#00ff00",
50299
- "#0000ff",
50300
- "#ffff00",
50301
- "#ff00ff",
50302
- "#00ffff",
50303
- "#ff8800",
50304
- "#8800ff",
50305
- "#88ff00",
50306
- "#0088ff",
50307
- "#ff0088",
50308
- "#00ff88",
50309
- "#888888",
50310
- "#f87171",
50311
- "#fb923c",
50312
- "#fbbf24",
50313
- "#facc15",
50314
- "#a3e635",
50315
- "#4ade80",
50316
- "#34d399",
50317
- "#2dd4bf",
50318
- "#22d3ee",
50319
- "#38bdf8",
50320
- "#60a5fa",
50321
- "#818cf8",
50322
- "#a78bfa",
50323
- "#c084fc",
50324
- "#e879f9",
50325
- "#f472b6",
50326
- "#fb7185",
50327
- "#f43f5e",
50328
- "#ef4444",
50329
- "#dc2626"
50330
- ]
50331
- }
50332
- );
50286
+ const [showAdvanced, setShowAdvanced] = t__default.useState(false);
50287
+ const quickColors = [
50288
+ "#000000",
50289
+ // Black
50290
+ "#ef4444",
50291
+ // Red
50292
+ "#f59e0b",
50293
+ // Orange
50294
+ "#10b981",
50295
+ // Green
50296
+ "#3b82f6",
50297
+ // Blue
50298
+ "#8b5cf6",
50299
+ // Purple
50300
+ "#6b7280",
50301
+ // Gray
50302
+ "#ec4899"
50303
+ // Pink
50304
+ ];
50305
+ if (showAdvanced) {
50306
+ return /* @__PURE__ */ jsxs("div", { className: "p-4 w-64", children: [
50307
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between mb-3", children: [
50308
+ /* @__PURE__ */ jsx("span", { className: "text-sm font-medium", children: "Choose Color" }),
50309
+ /* @__PURE__ */ jsx(
50310
+ MoonUIButtonPro,
50311
+ {
50312
+ size: "sm",
50313
+ variant: "ghost",
50314
+ onClick: () => setShowAdvanced(false),
50315
+ children: "Back"
50316
+ }
50317
+ )
50318
+ ] }),
50319
+ /* @__PURE__ */ jsx(
50320
+ MoonUIColorPickerPro,
50321
+ {
50322
+ value: currentColor,
50323
+ onChange: (color) => {
50324
+ onColorSelect(color);
50325
+ setShowAdvanced(false);
50326
+ },
50327
+ showInput: true,
50328
+ showPresets: false,
50329
+ size: "sm"
50330
+ }
50331
+ )
50332
+ ] });
50333
+ }
50334
+ return /* @__PURE__ */ jsx("div", { className: "p-3 w-36", children: /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-3 gap-2", children: [
50335
+ quickColors.map((color) => /* @__PURE__ */ jsx(
50336
+ "button",
50337
+ {
50338
+ onClick: () => onColorSelect(color),
50339
+ className: cn(
50340
+ "w-10 h-10 rounded-md border-2 hover:scale-110 transition-transform",
50341
+ currentColor === color ? "border-primary" : "border-transparent"
50342
+ ),
50343
+ style: { backgroundColor: color },
50344
+ title: color
50345
+ },
50346
+ color
50347
+ )),
50348
+ /* @__PURE__ */ jsx(
50349
+ "button",
50350
+ {
50351
+ onClick: () => setShowAdvanced(true),
50352
+ 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",
50353
+ title: "More colors",
50354
+ children: /* @__PURE__ */ jsx(Palette, { className: "w-5 h-5 text-muted-foreground" })
50355
+ }
50356
+ )
50357
+ ] }) });
50333
50358
  };
50334
50359
  var ToolbarButton = ({
50335
50360
  active,
@@ -50434,6 +50459,8 @@ function RichTextEditor({
50434
50459
  const [isTyping, setIsTyping] = useState(false);
50435
50460
  const typingIntervalRef = useRef(null);
50436
50461
  const [currentAction, setCurrentAction] = useState("");
50462
+ const [remainingText, setRemainingText] = useState("");
50463
+ const isTypingRef = useRef(false);
50437
50464
  const [isSourceView, setIsSourceView] = useState(false);
50438
50465
  const [sourceContent, setSourceContent] = useState("");
50439
50466
  const [currentTextColor, setCurrentTextColor] = useState("#000000");
@@ -50444,6 +50471,10 @@ function RichTextEditor({
50444
50471
  }
50445
50472
  return "en";
50446
50473
  });
50474
+ const [isAiPreviewOpen, setIsAiPreviewOpen] = useState(false);
50475
+ const [previewContent, setPreviewContent] = useState("");
50476
+ const [previewAction, setPreviewAction] = useState("");
50477
+ const [previewOriginalText, setPreviewOriginalText] = useState("");
50447
50478
  useEffect(() => {
50448
50479
  return () => {
50449
50480
  if (typingIntervalRef.current) {
@@ -50451,6 +50482,62 @@ function RichTextEditor({
50451
50482
  }
50452
50483
  };
50453
50484
  }, []);
50485
+ const stopTyping = () => {
50486
+ if (typingIntervalRef.current) {
50487
+ clearTimeout(typingIntervalRef.current);
50488
+ typingIntervalRef.current = null;
50489
+ }
50490
+ setIsTyping(false);
50491
+ isTypingRef.current = false;
50492
+ setTypingText("");
50493
+ setRemainingText("");
50494
+ toast({
50495
+ title: "AI typing stopped",
50496
+ description: "The AI response was interrupted."
50497
+ });
50498
+ };
50499
+ const shouldUseModal = (action) => {
50500
+ const modalActions = ["expand", "summarize", "ideas", "continue"];
50501
+ return modalActions.includes(action);
50502
+ };
50503
+ const applyAIContentToEditor = (content, replaceSelection = true) => {
50504
+ if (!editor)
50505
+ return;
50506
+ setIsTyping(true);
50507
+ isTypingRef.current = true;
50508
+ setTypingText("");
50509
+ if (replaceSelection) {
50510
+ const selection = editor.state.selection;
50511
+ const selectedText = editor.state.doc.textBetween(selection.from, selection.to, " ");
50512
+ if (selectedText) {
50513
+ editor.chain().focus().deleteSelection().run();
50514
+ }
50515
+ }
50516
+ let currentIndex = 0;
50517
+ const typeSpeed = 30;
50518
+ const typeNextChar = () => {
50519
+ if (currentIndex < content.length && isTypingRef.current) {
50520
+ const nextChar = content[currentIndex];
50521
+ setTypingText((prev) => prev + nextChar);
50522
+ editor.chain().focus().insertContent(nextChar).run();
50523
+ currentIndex++;
50524
+ setRemainingText(content.substring(currentIndex));
50525
+ typingIntervalRef.current = setTimeout(typeNextChar, typeSpeed);
50526
+ } else {
50527
+ setIsTyping(false);
50528
+ isTypingRef.current = false;
50529
+ setTypingText("");
50530
+ setRemainingText("");
50531
+ if (currentIndex >= content.length) {
50532
+ toast({
50533
+ title: "AI action completed",
50534
+ description: "Your text has been updated successfully."
50535
+ });
50536
+ }
50537
+ }
50538
+ };
50539
+ typeNextChar();
50540
+ };
50454
50541
  const slashCommands = [
50455
50542
  {
50456
50543
  id: "rewrite",
@@ -50588,10 +50675,18 @@ function RichTextEditor({
50588
50675
  const { from: from2, to } = editor.state.selection;
50589
50676
  const selectedText = editor.state.doc.textBetween(from2, to, " ");
50590
50677
  setIsProcessing(true);
50678
+ setCurrentAction(command2.id);
50591
50679
  try {
50592
50680
  const response = await command2.action(selectedText || editor.getText());
50593
50681
  if (response.text) {
50594
- editor.chain().focus().insertContent(response.text).run();
50682
+ if (shouldUseModal(command2.id)) {
50683
+ setPreviewContent(response.text);
50684
+ setPreviewAction(command2.id);
50685
+ setPreviewOriginalText(selectedText || editor.getText());
50686
+ setIsAiPreviewOpen(true);
50687
+ } else {
50688
+ applyAIContentToEditor(response.text, !!selectedText);
50689
+ }
50595
50690
  } else if (response.error) {
50596
50691
  toast({
50597
50692
  title: "AI Error",
@@ -50731,30 +50826,14 @@ function RichTextEditor({
50731
50826
  const result = await callAI(action, selectedText || editor.getText(), targetLanguage);
50732
50827
  processingToast.dismiss();
50733
50828
  if (result) {
50734
- setIsTyping(true);
50735
- setTypingText("");
50736
- if (selectedText) {
50737
- editor.chain().focus().deleteSelection().run();
50829
+ if (shouldUseModal(action)) {
50830
+ setPreviewContent(result);
50831
+ setPreviewAction(action);
50832
+ setPreviewOriginalText(selectedText || editor.getText());
50833
+ setIsAiPreviewOpen(true);
50834
+ } else {
50835
+ applyAIContentToEditor(result, !!selectedText);
50738
50836
  }
50739
- let currentIndex = 0;
50740
- const typeSpeed = 30;
50741
- const typeNextChar = () => {
50742
- if (currentIndex < result.length) {
50743
- const nextChar = result[currentIndex];
50744
- setTypingText((prev) => prev + nextChar);
50745
- editor.chain().focus().insertContent(nextChar).run();
50746
- currentIndex++;
50747
- typingIntervalRef.current = setTimeout(typeNextChar, typeSpeed);
50748
- } else {
50749
- setIsTyping(false);
50750
- setTypingText("");
50751
- toast({
50752
- title: "AI action completed",
50753
- description: "Your text has been updated successfully."
50754
- });
50755
- }
50756
- };
50757
- typeNextChar();
50758
50837
  }
50759
50838
  };
50760
50839
  const getActionDescription = (action, targetLanguage) => {
@@ -51301,6 +51380,17 @@ function RichTextEditor({
51301
51380
  ]
51302
51381
  }
51303
51382
  ) }),
51383
+ isTyping && /* @__PURE__ */ jsx(
51384
+ MoonUIButtonPro,
51385
+ {
51386
+ variant: "destructive",
51387
+ size: "sm",
51388
+ className: "h-8 px-2",
51389
+ onClick: stopTyping,
51390
+ title: "Stop AI typing",
51391
+ children: /* @__PURE__ */ jsx(X, { className: "w-4 h-4" })
51392
+ }
51393
+ ),
51304
51394
  /* @__PURE__ */ jsxs(MoonUIDropdownMenuContentPro, { className: "w-64", children: [
51305
51395
  /* @__PURE__ */ jsxs("div", { className: "px-2 py-1.5 text-xs font-semibold text-muted-foreground flex items-center gap-2", children: [
51306
51396
  /* @__PURE__ */ jsx(Wand2, { className: "w-3 h-3" }),
@@ -51829,6 +51919,91 @@ function RichTextEditor({
51829
51919
  }, children: "Apply Borders" })
51830
51920
  ] })
51831
51921
  ] }) }),
51922
+ /* @__PURE__ */ jsx(MoonUIDialogPro, { open: isAiPreviewOpen, onOpenChange: setIsAiPreviewOpen, children: /* @__PURE__ */ jsxs(MoonUIDialogContentPro, { className: "max-w-4xl max-h-[80vh] overflow-hidden flex flex-col", children: [
51923
+ /* @__PURE__ */ jsxs(MoonUIDialogHeaderPro, { children: [
51924
+ /* @__PURE__ */ jsxs(MoonUIDialogTitlePro, { className: "flex items-center gap-2", children: [
51925
+ /* @__PURE__ */ jsx(Wand2, { className: "w-5 h-5 text-purple-600 dark:text-purple-400" }),
51926
+ "AI Preview - ",
51927
+ getActionDescription(previewAction).replace("...", "")
51928
+ ] }),
51929
+ /* @__PURE__ */ jsx(MoonUIDialogDescriptionPro, { children: "Review the AI-generated content before applying it to your editor." })
51930
+ ] }),
51931
+ /* @__PURE__ */ jsxs("div", { className: "flex-1 overflow-hidden flex flex-col gap-4", children: [
51932
+ /* @__PURE__ */ jsxs("div", { className: "flex-shrink-0", children: [
51933
+ /* @__PURE__ */ jsx("div", { className: "text-sm font-medium text-muted-foreground mb-2", children: "Original Text:" }),
51934
+ /* @__PURE__ */ jsx("div", { className: "p-3 bg-muted/50 rounded-md border max-h-32 overflow-auto", children: /* @__PURE__ */ jsx("p", { className: "text-sm whitespace-pre-wrap", children: previewOriginalText }) })
51935
+ ] }),
51936
+ /* @__PURE__ */ jsxs("div", { className: "flex-1 overflow-hidden flex flex-col", children: [
51937
+ /* @__PURE__ */ jsxs("div", { className: "text-sm font-medium text-muted-foreground mb-2 flex items-center gap-2", children: [
51938
+ /* @__PURE__ */ jsx(Sparkles, { className: "w-4 h-4" }),
51939
+ "AI Generated Content:"
51940
+ ] }),
51941
+ /* @__PURE__ */ jsx("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", children: /* @__PURE__ */ jsx("div", { className: "prose prose-sm max-w-none dark:prose-invert", children: /* @__PURE__ */ jsx("div", { className: "whitespace-pre-wrap text-sm leading-relaxed", children: previewContent }) }) })
51942
+ ] })
51943
+ ] }),
51944
+ /* @__PURE__ */ jsxs("div", { className: "flex-shrink-0 flex justify-between items-center pt-4 border-t", children: [
51945
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
51946
+ /* @__PURE__ */ jsxs(
51947
+ MoonUIButtonPro,
51948
+ {
51949
+ variant: "outline",
51950
+ size: "sm",
51951
+ onClick: () => {
51952
+ navigator.clipboard.writeText(previewContent);
51953
+ toast({
51954
+ title: "Copied to clipboard",
51955
+ description: "The AI-generated content has been copied."
51956
+ });
51957
+ },
51958
+ children: [
51959
+ /* @__PURE__ */ jsx(Copy, { className: "w-4 h-4 mr-2" }),
51960
+ "Copy"
51961
+ ]
51962
+ }
51963
+ ),
51964
+ /* @__PURE__ */ jsxs(
51965
+ MoonUIButtonPro,
51966
+ {
51967
+ variant: "outline",
51968
+ size: "sm",
51969
+ onClick: () => {
51970
+ setIsAiPreviewOpen(false);
51971
+ handleAIAction(previewAction);
51972
+ },
51973
+ children: [
51974
+ /* @__PURE__ */ jsx(RefreshCw, { className: "w-4 h-4 mr-2" }),
51975
+ "Regenerate"
51976
+ ]
51977
+ }
51978
+ )
51979
+ ] }),
51980
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
51981
+ /* @__PURE__ */ jsx(
51982
+ MoonUIButtonPro,
51983
+ {
51984
+ variant: "outline",
51985
+ onClick: () => setIsAiPreviewOpen(false),
51986
+ children: "Cancel"
51987
+ }
51988
+ ),
51989
+ /* @__PURE__ */ jsxs(
51990
+ MoonUIButtonPro,
51991
+ {
51992
+ onClick: () => {
51993
+ setIsAiPreviewOpen(false);
51994
+ const replaceSelection = previewOriginalText !== editor?.getText();
51995
+ applyAIContentToEditor(previewContent, replaceSelection);
51996
+ },
51997
+ className: "bg-gradient-to-r from-purple-600 to-blue-600 hover:from-purple-700 hover:to-blue-700",
51998
+ children: [
51999
+ /* @__PURE__ */ jsx(Check, { className: "w-4 h-4 mr-2" }),
52000
+ "Apply to Editor"
52001
+ ]
52002
+ }
52003
+ )
52004
+ ] })
52005
+ ] })
52006
+ ] }) }),
51832
52007
  /* @__PURE__ */ jsx(
51833
52008
  "div",
51834
52009
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moontra/moonui-pro",
3
- "version": "2.8.10",
3
+ "version": "2.8.12",
4
4
  "description": "Premium React components for MoonUI - Advanced UI library with 50+ pro components including performance, interactive, and gesture components",
5
5
  "type": "module",
6
6
  "main": "dist/index.mjs",
@@ -73,7 +73,9 @@ import {
73
73
  Heart,
74
74
  GraduationCap,
75
75
  Zap,
76
- Lightbulb
76
+ Lightbulb,
77
+ X,
78
+ Copy
77
79
  } from 'lucide-react';
78
80
  import { cn } from '../../lib/utils';
79
81
  import { Button } from '../ui/button';
@@ -218,23 +220,71 @@ const EditorColorPicker = ({
218
220
  onColorSelect: (color: string) => void;
219
221
  currentColor?: string;
220
222
  }) => {
223
+ const [showAdvanced, setShowAdvanced] = React.useState(false);
224
+
225
+ // Quick colors - 3x3 grid of 8 colors + 1 advanced button
226
+ const quickColors = [
227
+ '#000000', // Black
228
+ '#ef4444', // Red
229
+ '#f59e0b', // Orange
230
+ '#10b981', // Green
231
+ '#3b82f6', // Blue
232
+ '#8b5cf6', // Purple
233
+ '#6b7280', // Gray
234
+ '#ec4899', // Pink
235
+ ];
236
+
237
+ if (showAdvanced) {
238
+ return (
239
+ <div className="p-4 w-64">
240
+ <div className="flex items-center justify-between mb-3">
241
+ <span className="text-sm font-medium">Choose Color</span>
242
+ <Button
243
+ size="sm"
244
+ variant="ghost"
245
+ onClick={() => setShowAdvanced(false)}
246
+ >
247
+ Back
248
+ </Button>
249
+ </div>
250
+ <ColorPicker
251
+ value={currentColor}
252
+ onChange={(color) => {
253
+ onColorSelect(color);
254
+ setShowAdvanced(false);
255
+ }}
256
+ showInput={true}
257
+ showPresets={false}
258
+ size="sm"
259
+ />
260
+ </div>
261
+ );
262
+ }
263
+
221
264
  return (
222
- <ColorPicker
223
- value={currentColor}
224
- onChange={onColorSelect}
225
- showInput={true}
226
- showPresets={true}
227
- size="sm"
228
- presets={[
229
- '#000000', '#ffffff', '#ff0000', '#00ff00', '#0000ff',
230
- '#ffff00', '#ff00ff', '#00ffff', '#ff8800', '#8800ff',
231
- '#88ff00', '#0088ff', '#ff0088', '#00ff88', '#888888',
232
- '#f87171', '#fb923c', '#fbbf24', '#facc15', '#a3e635',
233
- '#4ade80', '#34d399', '#2dd4bf', '#22d3ee', '#38bdf8',
234
- '#60a5fa', '#818cf8', '#a78bfa', '#c084fc', '#e879f9',
235
- '#f472b6', '#fb7185', '#f43f5e', '#ef4444', '#dc2626',
236
- ]}
237
- />
265
+ <div className="p-3 w-36">
266
+ <div className="grid grid-cols-3 gap-2">
267
+ {quickColors.map((color) => (
268
+ <button
269
+ key={color}
270
+ onClick={() => onColorSelect(color)}
271
+ className={cn(
272
+ "w-10 h-10 rounded-md border-2 hover:scale-110 transition-transform",
273
+ currentColor === color ? "border-primary" : "border-transparent"
274
+ )}
275
+ style={{ backgroundColor: color }}
276
+ title={color}
277
+ />
278
+ ))}
279
+ <button
280
+ onClick={() => setShowAdvanced(true)}
281
+ className="w-10 h-10 rounded-md border-2 border-dashed border-muted-foreground/50 hover:border-primary transition-colors flex items-center justify-center"
282
+ title="More colors"
283
+ >
284
+ <Palette className="w-5 h-5 text-muted-foreground" />
285
+ </button>
286
+ </div>
287
+ </div>
238
288
  );
239
289
  };
240
290
 
@@ -379,6 +429,8 @@ export function RichTextEditor({
379
429
  const [isTyping, setIsTyping] = useState(false);
380
430
  const typingIntervalRef = useRef<NodeJS.Timeout | null>(null);
381
431
  const [currentAction, setCurrentAction] = useState('');
432
+ const [remainingText, setRemainingText] = useState(''); // To store remaining text when stopped
433
+ const isTypingRef = useRef(false); // Ref to track typing state in closure
382
434
  const [isSourceView, setIsSourceView] = useState(false);
383
435
  const [sourceContent, setSourceContent] = useState('');
384
436
  const [currentTextColor, setCurrentTextColor] = useState('#000000');
@@ -391,6 +443,12 @@ export function RichTextEditor({
391
443
  return 'en';
392
444
  });
393
445
 
446
+ // AI Preview Modal states
447
+ const [isAiPreviewOpen, setIsAiPreviewOpen] = useState(false);
448
+ const [previewContent, setPreviewContent] = useState('');
449
+ const [previewAction, setPreviewAction] = useState('');
450
+ const [previewOriginalText, setPreviewOriginalText] = useState('');
451
+
394
452
  // Clean up typewriter effect on unmount
395
453
  useEffect(() => {
396
454
  return () => {
@@ -399,6 +457,77 @@ export function RichTextEditor({
399
457
  }
400
458
  };
401
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
+ // Apply AI content to editor with typewriter effect
484
+ const applyAIContentToEditor = (content: string, replaceSelection: boolean = true) => {
485
+ if (!editor) return;
486
+
487
+ // Start typewriter effect
488
+ setIsTyping(true);
489
+ isTypingRef.current = true;
490
+ setTypingText('');
491
+
492
+ if (replaceSelection) {
493
+ const selection = editor.state.selection;
494
+ const selectedText = editor.state.doc.textBetween(selection.from, selection.to, ' ');
495
+ if (selectedText) {
496
+ editor.chain().focus().deleteSelection().run();
497
+ }
498
+ }
499
+
500
+ let currentIndex = 0;
501
+ const typeSpeed = 30; // ms per character
502
+
503
+ const typeNextChar = () => {
504
+ if (currentIndex < content.length && isTypingRef.current) {
505
+ const nextChar = content[currentIndex];
506
+ setTypingText(prev => prev + nextChar);
507
+ editor.chain().focus().insertContent(nextChar).run();
508
+ currentIndex++;
509
+ setRemainingText(content.substring(currentIndex));
510
+ typingIntervalRef.current = setTimeout(typeNextChar, typeSpeed);
511
+ } else {
512
+ // Typing complete or stopped
513
+ setIsTyping(false);
514
+ isTypingRef.current = false;
515
+ setTypingText('');
516
+ setRemainingText('');
517
+
518
+ if (currentIndex >= content.length) {
519
+ // Success toast only if completed
520
+ toast({
521
+ title: "AI action completed",
522
+ description: "Your text has been updated successfully.",
523
+ });
524
+ }
525
+ }
526
+ };
527
+
528
+ // Start typing
529
+ typeNextChar();
530
+ };
402
531
 
403
532
  // Slash commands tanımları
404
533
  const slashCommands: SlashCommand[] = [
@@ -539,10 +668,22 @@ export function RichTextEditor({
539
668
  const selectedText = editor.state.doc.textBetween(from, to, ' ');
540
669
 
541
670
  setIsProcessing(true);
671
+ setCurrentAction(command.id);
672
+
542
673
  try {
543
674
  const response = await command.action(selectedText || editor.getText());
544
675
  if (response.text) {
545
- editor.chain().focus().insertContent(response.text).run();
676
+ // Check if this command should use modal preview
677
+ if (shouldUseModal(command.id)) {
678
+ // Open preview modal
679
+ setPreviewContent(response.text);
680
+ setPreviewAction(command.id);
681
+ setPreviewOriginalText(selectedText || editor.getText());
682
+ setIsAiPreviewOpen(true);
683
+ } else {
684
+ // Apply directly with typewriter effect
685
+ applyAIContentToEditor(response.text, !!selectedText);
686
+ }
546
687
  } else if (response.error) {
547
688
  toast({
548
689
  title: "AI Error",
@@ -697,39 +838,17 @@ export function RichTextEditor({
697
838
  processingToast.dismiss();
698
839
 
699
840
  if (result) {
700
- // Start typewriter effect
701
- setIsTyping(true);
702
- setTypingText('');
703
-
704
- if (selectedText) {
705
- editor.chain().focus().deleteSelection().run();
841
+ // Check if this action should use modal preview
842
+ if (shouldUseModal(action)) {
843
+ // Open preview modal instead of directly applying
844
+ setPreviewContent(result);
845
+ setPreviewAction(action);
846
+ setPreviewOriginalText(selectedText || editor.getText());
847
+ setIsAiPreviewOpen(true);
848
+ } else {
849
+ // Apply directly with typewriter effect (for rewrite, fix grammar, tone changes, etc.)
850
+ applyAIContentToEditor(result, !!selectedText);
706
851
  }
707
-
708
- let currentIndex = 0;
709
- const typeSpeed = 30; // ms per character
710
-
711
- const typeNextChar = () => {
712
- if (currentIndex < result.length) {
713
- const nextChar = result[currentIndex];
714
- setTypingText(prev => prev + nextChar);
715
- editor.chain().focus().insertContent(nextChar).run();
716
- currentIndex++;
717
- typingIntervalRef.current = setTimeout(typeNextChar, typeSpeed);
718
- } else {
719
- // Typing complete
720
- setIsTyping(false);
721
- setTypingText('');
722
-
723
- // Success toast
724
- toast({
725
- title: "AI action completed",
726
- description: "Your text has been updated successfully.",
727
- });
728
- }
729
- };
730
-
731
- // Start typing
732
- typeNextChar();
733
852
  }
734
853
  };
735
854
 
@@ -1364,6 +1483,17 @@ export function RichTextEditor({
1364
1483
  {isTyping ? 'Typing...' : 'AI Tools'}
1365
1484
  </Button>
1366
1485
  </DropdownMenuTrigger>
1486
+ {isTyping && (
1487
+ <Button
1488
+ variant="destructive"
1489
+ size="sm"
1490
+ className="h-8 px-2"
1491
+ onClick={stopTyping}
1492
+ title="Stop AI typing"
1493
+ >
1494
+ <X className="w-4 h-4" />
1495
+ </Button>
1496
+ )}
1367
1497
  <DropdownMenuContent className="w-64">
1368
1498
  <div className="px-2 py-1.5 text-xs font-semibold text-muted-foreground flex items-center gap-2">
1369
1499
  <Wand2 className="w-3 h-3" />
@@ -1886,6 +2016,99 @@ export function RichTextEditor({
1886
2016
  </DialogContent>
1887
2017
  </Dialog>
1888
2018
 
2019
+ {/* AI Preview Modal */}
2020
+ <Dialog open={isAiPreviewOpen} onOpenChange={setIsAiPreviewOpen}>
2021
+ <DialogContent className="max-w-4xl max-h-[80vh] overflow-hidden flex flex-col">
2022
+ <DialogHeader>
2023
+ <DialogTitle className="flex items-center gap-2">
2024
+ <Wand2 className="w-5 h-5 text-purple-600 dark:text-purple-400" />
2025
+ AI Preview - {getActionDescription(previewAction).replace('...', '')}
2026
+ </DialogTitle>
2027
+ <DialogDescription>
2028
+ Review the AI-generated content before applying it to your editor.
2029
+ </DialogDescription>
2030
+ </DialogHeader>
2031
+
2032
+ <div className="flex-1 overflow-hidden flex flex-col gap-4">
2033
+ {/* Original Text Section */}
2034
+ <div className="flex-shrink-0">
2035
+ <div className="text-sm font-medium text-muted-foreground mb-2">Original Text:</div>
2036
+ <div className="p-3 bg-muted/50 rounded-md border max-h-32 overflow-auto">
2037
+ <p className="text-sm whitespace-pre-wrap">{previewOriginalText}</p>
2038
+ </div>
2039
+ </div>
2040
+
2041
+ {/* AI Generated Content */}
2042
+ <div className="flex-1 overflow-hidden flex flex-col">
2043
+ <div className="text-sm font-medium text-muted-foreground mb-2 flex items-center gap-2">
2044
+ <Sparkles className="w-4 h-4" />
2045
+ AI Generated Content:
2046
+ </div>
2047
+ <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">
2048
+ <div className="prose prose-sm max-w-none dark:prose-invert">
2049
+ <div className="whitespace-pre-wrap text-sm leading-relaxed">
2050
+ {previewContent}
2051
+ </div>
2052
+ </div>
2053
+ </div>
2054
+ </div>
2055
+ </div>
2056
+
2057
+ {/* Action Buttons */}
2058
+ <div className="flex-shrink-0 flex justify-between items-center pt-4 border-t">
2059
+ <div className="flex gap-2">
2060
+ <Button
2061
+ variant="outline"
2062
+ size="sm"
2063
+ onClick={() => {
2064
+ navigator.clipboard.writeText(previewContent);
2065
+ toast({
2066
+ title: "Copied to clipboard",
2067
+ description: "The AI-generated content has been copied.",
2068
+ });
2069
+ }}
2070
+ >
2071
+ <Copy className="w-4 h-4 mr-2" />
2072
+ Copy
2073
+ </Button>
2074
+ <Button
2075
+ variant="outline"
2076
+ size="sm"
2077
+ onClick={() => {
2078
+ // Regenerate content
2079
+ setIsAiPreviewOpen(false);
2080
+ handleAIAction(previewAction);
2081
+ }}
2082
+ >
2083
+ <RefreshCw className="w-4 h-4 mr-2" />
2084
+ Regenerate
2085
+ </Button>
2086
+ </div>
2087
+
2088
+ <div className="flex gap-2">
2089
+ <Button
2090
+ variant="outline"
2091
+ onClick={() => setIsAiPreviewOpen(false)}
2092
+ >
2093
+ Cancel
2094
+ </Button>
2095
+ <Button
2096
+ onClick={() => {
2097
+ setIsAiPreviewOpen(false);
2098
+ // Apply with typewriter effect
2099
+ const replaceSelection = previewOriginalText !== editor?.getText();
2100
+ applyAIContentToEditor(previewContent, replaceSelection);
2101
+ }}
2102
+ className="bg-gradient-to-r from-purple-600 to-blue-600 hover:from-purple-700 hover:to-blue-700"
2103
+ >
2104
+ <Check className="w-4 h-4 mr-2" />
2105
+ Apply to Editor
2106
+ </Button>
2107
+ </div>
2108
+ </div>
2109
+ </DialogContent>
2110
+ </Dialog>
2111
+
1889
2112
  {/* Editor */}
1890
2113
  <div
1891
2114
  className="overflow-auto relative"