@qwanyx/ai-editor 1.0.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/components/AIEditor.d.ts +15 -0
- package/dist/components/AIEditor.d.ts.map +1 -0
- package/dist/components/AIEditor.js +154 -0
- package/dist/components/AIToolbar.d.ts +22 -0
- package/dist/components/AIToolbar.d.ts.map +1 -0
- package/dist/components/AIToolbar.js +10 -0
- package/dist/components/EditorToolbar.d.ts +8 -0
- package/dist/components/EditorToolbar.d.ts.map +1 -0
- package/dist/components/EditorToolbar.js +241 -0
- package/dist/components/MarkdownPreview.d.ts +7 -0
- package/dist/components/MarkdownPreview.d.ts.map +1 -0
- package/dist/components/MarkdownPreview.js +40 -0
- package/dist/components/PromptModal.d.ts +9 -0
- package/dist/components/PromptModal.d.ts.map +1 -0
- package/dist/components/PromptModal.js +38 -0
- package/dist/components/RichTextEditor.d.ts +15 -0
- package/dist/components/RichTextEditor.d.ts.map +1 -0
- package/dist/components/RichTextEditor.js +253 -0
- package/dist/hooks/useAIEditor.d.ts +15 -0
- package/dist/hooks/useAIEditor.d.ts.map +1 -0
- package/dist/hooks/useAIEditor.js +46 -0
- package/dist/hooks/useSelection.d.ts +12 -0
- package/dist/hooks/useSelection.d.ts.map +1 -0
- package/dist/hooks/useSelection.js +45 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/nodes/ImageLinkNode.d.ts +48 -0
- package/dist/nodes/ImageLinkNode.d.ts.map +1 -0
- package/dist/nodes/ImageLinkNode.js +157 -0
- package/dist/nodes/ImageNode.d.ts +62 -0
- package/dist/nodes/ImageNode.d.ts.map +1 -0
- package/dist/nodes/ImageNode.js +487 -0
- package/dist/nodes/LinkNode.d.ts +33 -0
- package/dist/nodes/LinkNode.d.ts.map +1 -0
- package/dist/nodes/LinkNode.js +108 -0
- package/dist/plugins/ImageLinkPlugin.d.ts +2 -0
- package/dist/plugins/ImageLinkPlugin.d.ts.map +1 -0
- package/dist/plugins/ImageLinkPlugin.js +112 -0
- package/dist/plugins/InsertObjectPlugin.d.ts +4 -0
- package/dist/plugins/InsertObjectPlugin.d.ts.map +1 -0
- package/dist/plugins/InsertObjectPlugin.js +464 -0
- package/dist/plugins/LinkPlugin.d.ts +2 -0
- package/dist/plugins/LinkPlugin.d.ts.map +1 -0
- package/dist/plugins/LinkPlugin.js +45 -0
- package/package.json +45 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface AIEditorProps {
|
|
2
|
+
initialContent?: string;
|
|
3
|
+
onChange?: (content: string) => void;
|
|
4
|
+
onAIRequest?: (params: {
|
|
5
|
+
fullText: string;
|
|
6
|
+
selectedText?: string;
|
|
7
|
+
action: 'rewrite' | 'proofread' | 'custom';
|
|
8
|
+
customPrompt?: string;
|
|
9
|
+
}) => Promise<string>;
|
|
10
|
+
placeholder?: string;
|
|
11
|
+
className?: string;
|
|
12
|
+
minHeight?: string;
|
|
13
|
+
}
|
|
14
|
+
export declare function AIEditor({ initialContent, onChange, onAIRequest, placeholder, className, minHeight }: AIEditorProps): import("react/jsx-runtime").JSX.Element;
|
|
15
|
+
//# sourceMappingURL=AIEditor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AIEditor.d.ts","sourceRoot":"","sources":["../../src/components/AIEditor.tsx"],"names":[],"mappings":"AAOA,MAAM,WAAW,aAAa;IAC5B,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;IACpC,WAAW,CAAC,EAAE,CAAC,MAAM,EAAE;QACrB,QAAQ,EAAE,MAAM,CAAA;QAChB,YAAY,CAAC,EAAE,MAAM,CAAA;QACrB,MAAM,EAAE,SAAS,GAAG,WAAW,GAAG,QAAQ,CAAA;QAC1C,YAAY,CAAC,EAAE,MAAM,CAAA;KACtB,KAAK,OAAO,CAAC,MAAM,CAAC,CAAA;IACrB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,wBAAgB,QAAQ,CAAC,EACvB,cAAmB,EACnB,QAAQ,EACR,WAAW,EACX,WAAqC,EACrC,SAAc,EACd,SAAmB,EACpB,EAAE,aAAa,2CAuOf"}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useRef, useState, useCallback, useEffect } from 'react';
|
|
3
|
+
import { useAIEditor } from '../hooks/useAIEditor';
|
|
4
|
+
import { useSelection } from '../hooks/useSelection';
|
|
5
|
+
import { AIToolbar } from './AIToolbar';
|
|
6
|
+
import { PromptModal } from './PromptModal';
|
|
7
|
+
import { MarkdownPreview } from './MarkdownPreview';
|
|
8
|
+
export function AIEditor({ initialContent = '', onChange, onAIRequest, placeholder = 'Commencez à écrire...', className = '', minHeight = '300px' }) {
|
|
9
|
+
const previewRef = useRef(null);
|
|
10
|
+
const textareaRef = useRef(null);
|
|
11
|
+
const [state, actions] = useAIEditor(initialContent);
|
|
12
|
+
const { selection, clearSelection } = useSelection(previewRef);
|
|
13
|
+
const [isEditMode, setIsEditMode] = useState(true); // Start in edit mode
|
|
14
|
+
const [isPromptModalOpen, setIsPromptModalOpen] = useState(false);
|
|
15
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
16
|
+
// Helper to wrap selected text with markdown syntax
|
|
17
|
+
const wrapSelection = useCallback((before, after) => {
|
|
18
|
+
const textarea = textareaRef.current;
|
|
19
|
+
if (!textarea)
|
|
20
|
+
return;
|
|
21
|
+
const start = textarea.selectionStart;
|
|
22
|
+
const end = textarea.selectionEnd;
|
|
23
|
+
const text = state.content;
|
|
24
|
+
const selectedText = text.substring(start, end);
|
|
25
|
+
const newText = text.substring(0, start) + before + selectedText + after + text.substring(end);
|
|
26
|
+
actions.setContent(newText);
|
|
27
|
+
// Restore cursor position after the inserted text
|
|
28
|
+
setTimeout(() => {
|
|
29
|
+
textarea.focus();
|
|
30
|
+
const newCursorPos = start + before.length + selectedText.length + after.length;
|
|
31
|
+
textarea.setSelectionRange(newCursorPos, newCursorPos);
|
|
32
|
+
}, 0);
|
|
33
|
+
}, [state.content, actions]);
|
|
34
|
+
// Helper to insert text at line start
|
|
35
|
+
const insertAtLineStart = useCallback((prefix) => {
|
|
36
|
+
const textarea = textareaRef.current;
|
|
37
|
+
if (!textarea)
|
|
38
|
+
return;
|
|
39
|
+
const start = textarea.selectionStart;
|
|
40
|
+
const text = state.content;
|
|
41
|
+
// Find the start of the current line
|
|
42
|
+
let lineStart = start;
|
|
43
|
+
while (lineStart > 0 && text[lineStart - 1] !== '\n') {
|
|
44
|
+
lineStart--;
|
|
45
|
+
}
|
|
46
|
+
const newText = text.substring(0, lineStart) + prefix + text.substring(lineStart);
|
|
47
|
+
actions.setContent(newText);
|
|
48
|
+
setTimeout(() => {
|
|
49
|
+
textarea.focus();
|
|
50
|
+
textarea.setSelectionRange(start + prefix.length, start + prefix.length);
|
|
51
|
+
}, 0);
|
|
52
|
+
}, [state.content, actions]);
|
|
53
|
+
// Formatting handlers
|
|
54
|
+
const handleBold = useCallback(() => wrapSelection('**', '**'), [wrapSelection]);
|
|
55
|
+
const handleItalic = useCallback(() => wrapSelection('*', '*'), [wrapSelection]);
|
|
56
|
+
const handleHeading = useCallback((level) => {
|
|
57
|
+
const prefix = '#'.repeat(level) + ' ';
|
|
58
|
+
insertAtLineStart(prefix);
|
|
59
|
+
}, [insertAtLineStart]);
|
|
60
|
+
const handleBulletList = useCallback(() => insertAtLineStart('- '), [insertAtLineStart]);
|
|
61
|
+
const handleNumberedList = useCallback(() => insertAtLineStart('1. '), [insertAtLineStart]);
|
|
62
|
+
const handleQuote = useCallback(() => insertAtLineStart('> '), [insertAtLineStart]);
|
|
63
|
+
const handleLink = useCallback(() => {
|
|
64
|
+
const textarea = textareaRef.current;
|
|
65
|
+
if (!textarea)
|
|
66
|
+
return;
|
|
67
|
+
const start = textarea.selectionStart;
|
|
68
|
+
const end = textarea.selectionEnd;
|
|
69
|
+
const text = state.content;
|
|
70
|
+
const selectedText = text.substring(start, end) || 'texte';
|
|
71
|
+
const newText = text.substring(0, start) + `[${selectedText}](url)` + text.substring(end);
|
|
72
|
+
actions.setContent(newText);
|
|
73
|
+
setTimeout(() => {
|
|
74
|
+
textarea.focus();
|
|
75
|
+
// Select "url" for easy replacement
|
|
76
|
+
const urlStart = start + selectedText.length + 3;
|
|
77
|
+
textarea.setSelectionRange(urlStart, urlStart + 3);
|
|
78
|
+
}, 0);
|
|
79
|
+
}, [state.content, actions]);
|
|
80
|
+
// Notify parent of changes
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
onChange?.(state.content);
|
|
83
|
+
}, [state.content, onChange]);
|
|
84
|
+
// Keyboard shortcuts
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
const handleKeyDown = (e) => {
|
|
87
|
+
if ((e.ctrlKey || e.metaKey) && e.key === 'z') {
|
|
88
|
+
if (e.shiftKey) {
|
|
89
|
+
actions.redo();
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
actions.undo();
|
|
93
|
+
}
|
|
94
|
+
e.preventDefault();
|
|
95
|
+
}
|
|
96
|
+
if ((e.ctrlKey || e.metaKey) && e.key === 'y') {
|
|
97
|
+
actions.redo();
|
|
98
|
+
e.preventDefault();
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
window.addEventListener('keydown', handleKeyDown);
|
|
102
|
+
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
103
|
+
}, [actions]);
|
|
104
|
+
const handleAIAction = useCallback(async (action, customPrompt) => {
|
|
105
|
+
if (!onAIRequest) {
|
|
106
|
+
console.warn('AIEditor: onAIRequest handler not provided');
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
setIsLoading(true);
|
|
110
|
+
try {
|
|
111
|
+
const result = await onAIRequest({
|
|
112
|
+
fullText: state.content,
|
|
113
|
+
selectedText: selection?.text,
|
|
114
|
+
action,
|
|
115
|
+
customPrompt
|
|
116
|
+
});
|
|
117
|
+
if (selection) {
|
|
118
|
+
// Replace only the selected portion
|
|
119
|
+
const before = state.content.substring(0, selection.originalStart);
|
|
120
|
+
const after = state.content.substring(selection.originalEnd);
|
|
121
|
+
actions.setContent(before + result + after);
|
|
122
|
+
clearSelection();
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
// Replace entire content
|
|
126
|
+
actions.setContent(result);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
console.error('AI request failed:', error);
|
|
131
|
+
}
|
|
132
|
+
finally {
|
|
133
|
+
setIsLoading(false);
|
|
134
|
+
setIsPromptModalOpen(false);
|
|
135
|
+
}
|
|
136
|
+
}, [state.content, selection, onAIRequest, actions, clearSelection]);
|
|
137
|
+
const handleRewrite = useCallback(() => {
|
|
138
|
+
handleAIAction('rewrite');
|
|
139
|
+
}, [handleAIAction]);
|
|
140
|
+
const handleProofread = useCallback(() => {
|
|
141
|
+
handleAIAction('proofread');
|
|
142
|
+
}, [handleAIAction]);
|
|
143
|
+
const handleCustomPrompt = useCallback(() => {
|
|
144
|
+
setIsPromptModalOpen(true);
|
|
145
|
+
}, []);
|
|
146
|
+
const handlePromptSubmit = useCallback((prompt) => {
|
|
147
|
+
handleAIAction('custom', prompt);
|
|
148
|
+
}, [handleAIAction]);
|
|
149
|
+
return (_jsxs("div", { className: `border border-gray-200 rounded-lg overflow-hidden ${className}`, children: [_jsx(AIToolbar, { onRewrite: handleRewrite, onProofread: handleProofread, onCustomPrompt: handleCustomPrompt, onUndo: actions.undo, onRedo: actions.redo, canUndo: actions.canUndo, canRedo: actions.canRedo, hasSelection: !!selection, isLoading: isLoading, isEditMode: isEditMode, onToggleMode: () => setIsEditMode(!isEditMode), onBold: handleBold, onItalic: handleItalic, onHeading: handleHeading, onBulletList: handleBulletList, onNumberedList: handleNumberedList, onLink: handleLink, onQuote: handleQuote }), _jsxs("div", { className: "relative", style: { minHeight }, children: [isEditMode ? (
|
|
150
|
+
// Edit mode - raw markdown
|
|
151
|
+
_jsx("textarea", { ref: textareaRef, value: state.content, onChange: (e) => actions.setContent(e.target.value), className: "w-full h-full p-4 font-mono text-sm resize-none border-0 focus:ring-0 focus:outline-none text-gray-900", style: { minHeight }, placeholder: placeholder })) : (
|
|
152
|
+
// Preview mode - rendered markdown with selection
|
|
153
|
+
_jsx("div", { className: "p-4 overflow-auto", style: { minHeight }, children: state.content ? (_jsx(MarkdownPreview, { ref: previewRef, content: state.content, className: "cursor-text" })) : (_jsx("p", { className: "text-gray-400 italic", children: placeholder })) })), isLoading && (_jsx("div", { className: "absolute inset-0 bg-white/80 flex items-center justify-center", children: _jsxs("div", { className: "flex items-center gap-2 text-blue-600", children: [_jsx("span", { className: "material-icons animate-spin", children: "sync" }), _jsx("span", { children: "L'IA travaille..." })] }) }))] }), _jsx(PromptModal, { isOpen: isPromptModalOpen, onClose: () => setIsPromptModalOpen(false), onSubmit: handlePromptSubmit, selectedText: selection?.text, isLoading: isLoading })] }));
|
|
154
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface AIToolbarProps {
|
|
2
|
+
onRewrite: () => void;
|
|
3
|
+
onProofread: () => void;
|
|
4
|
+
onCustomPrompt: () => void;
|
|
5
|
+
onUndo: () => void;
|
|
6
|
+
onRedo: () => void;
|
|
7
|
+
canUndo: boolean;
|
|
8
|
+
canRedo: boolean;
|
|
9
|
+
hasSelection: boolean;
|
|
10
|
+
isLoading: boolean;
|
|
11
|
+
isEditMode: boolean;
|
|
12
|
+
onToggleMode: () => void;
|
|
13
|
+
onBold?: () => void;
|
|
14
|
+
onItalic?: () => void;
|
|
15
|
+
onHeading?: (level: 1 | 2 | 3) => void;
|
|
16
|
+
onBulletList?: () => void;
|
|
17
|
+
onNumberedList?: () => void;
|
|
18
|
+
onLink?: () => void;
|
|
19
|
+
onQuote?: () => void;
|
|
20
|
+
}
|
|
21
|
+
export declare function AIToolbar({ onRewrite, onProofread, onCustomPrompt, onUndo, onRedo, canUndo, canRedo, hasSelection, isLoading, isEditMode, onToggleMode, onBold, onItalic, onHeading, onBulletList, onNumberedList, onLink, onQuote }: AIToolbarProps): import("react/jsx-runtime").JSX.Element;
|
|
22
|
+
//# sourceMappingURL=AIToolbar.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AIToolbar.d.ts","sourceRoot":"","sources":["../../src/components/AIToolbar.tsx"],"names":[],"mappings":"AAEA,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,IAAI,CAAA;IACrB,WAAW,EAAE,MAAM,IAAI,CAAA;IACvB,cAAc,EAAE,MAAM,IAAI,CAAA;IAC1B,MAAM,EAAE,MAAM,IAAI,CAAA;IAClB,MAAM,EAAE,MAAM,IAAI,CAAA;IAClB,OAAO,EAAE,OAAO,CAAA;IAChB,OAAO,EAAE,OAAO,CAAA;IAChB,YAAY,EAAE,OAAO,CAAA;IACrB,SAAS,EAAE,OAAO,CAAA;IAClB,UAAU,EAAE,OAAO,CAAA;IACnB,YAAY,EAAE,MAAM,IAAI,CAAA;IAExB,MAAM,CAAC,EAAE,MAAM,IAAI,CAAA;IACnB,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;IACrB,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,IAAI,CAAA;IACtC,YAAY,CAAC,EAAE,MAAM,IAAI,CAAA;IACzB,cAAc,CAAC,EAAE,MAAM,IAAI,CAAA;IAC3B,MAAM,CAAC,EAAE,MAAM,IAAI,CAAA;IACnB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAA;CACrB;AAED,wBAAgB,SAAS,CAAC,EACxB,SAAS,EACT,WAAW,EACX,cAAc,EACd,MAAM,EACN,MAAM,EACN,OAAO,EACP,OAAO,EACP,YAAY,EACZ,SAAS,EACT,UAAU,EACV,YAAY,EACZ,MAAM,EACN,QAAQ,EACR,SAAS,EACT,YAAY,EACZ,cAAc,EACd,MAAM,EACN,OAAO,EACR,EAAE,cAAc,2CA8LhB"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import React from 'react';
|
|
3
|
+
export function AIToolbar({ onRewrite, onProofread, onCustomPrompt, onUndo, onRedo, canUndo, canRedo, hasSelection, isLoading, isEditMode, onToggleMode, onBold, onItalic, onHeading, onBulletList, onNumberedList, onLink, onQuote }) {
|
|
4
|
+
const [showHeadingMenu, setShowHeadingMenu] = React.useState(false);
|
|
5
|
+
return (_jsxs("div", { className: "flex flex-wrap items-center gap-1 p-2 bg-gray-50 border-b border-gray-200 rounded-t-lg", children: [_jsxs("div", { className: "flex items-center bg-gray-200 rounded-lg p-0.5", children: [_jsx("button", { onClick: onToggleMode, className: `px-2 py-1 text-sm rounded-md transition-colors ${!isEditMode
|
|
6
|
+
? 'bg-white text-gray-900 shadow-sm'
|
|
7
|
+
: 'text-gray-600 hover:text-gray-900'}`, title: "Mode aper\u00E7u", children: _jsx("span", { className: "material-icons text-base", children: "visibility" }) }), _jsx("button", { onClick: onToggleMode, className: `px-2 py-1 text-sm rounded-md transition-colors ${isEditMode
|
|
8
|
+
? 'bg-white text-gray-900 shadow-sm'
|
|
9
|
+
: 'text-gray-600 hover:text-gray-900'}`, title: "Mode \u00E9dition", children: _jsx("span", { className: "material-icons text-base", children: "edit" }) })] }), _jsx("div", { className: "w-px h-6 bg-gray-300 mx-1" }), isEditMode && (_jsxs(_Fragment, { children: [_jsx("button", { onClick: onBold, className: "p-1.5 text-gray-600 hover:text-gray-900 hover:bg-gray-200 rounded transition-colors", title: "Gras (Ctrl+B)", children: _jsx("span", { className: "material-icons text-lg", children: "format_bold" }) }), _jsx("button", { onClick: onItalic, className: "p-1.5 text-gray-600 hover:text-gray-900 hover:bg-gray-200 rounded transition-colors", title: "Italique (Ctrl+I)", children: _jsx("span", { className: "material-icons text-lg", children: "format_italic" }) }), _jsxs("div", { className: "relative", children: [_jsxs("button", { onClick: () => setShowHeadingMenu(!showHeadingMenu), className: "p-1.5 text-gray-600 hover:text-gray-900 hover:bg-gray-200 rounded transition-colors flex items-center", title: "Titres", children: [_jsx("span", { className: "material-icons text-lg", children: "title" }), _jsx("span", { className: "material-icons text-sm", children: "arrow_drop_down" })] }), showHeadingMenu && (_jsxs("div", { className: "absolute top-full left-0 mt-1 bg-white border border-gray-200 rounded-lg shadow-lg z-10 py-1", children: [_jsx("button", { onClick: () => { onHeading?.(1); setShowHeadingMenu(false); }, className: "block w-full px-4 py-1.5 text-left text-lg font-bold hover:bg-gray-100", children: "Titre 1" }), _jsx("button", { onClick: () => { onHeading?.(2); setShowHeadingMenu(false); }, className: "block w-full px-4 py-1.5 text-left text-base font-bold hover:bg-gray-100", children: "Titre 2" }), _jsx("button", { onClick: () => { onHeading?.(3); setShowHeadingMenu(false); }, className: "block w-full px-4 py-1.5 text-left text-sm font-bold hover:bg-gray-100", children: "Titre 3" })] }))] }), _jsx("div", { className: "w-px h-6 bg-gray-300 mx-1" }), _jsx("button", { onClick: onBulletList, className: "p-1.5 text-gray-600 hover:text-gray-900 hover:bg-gray-200 rounded transition-colors", title: "Liste \u00E0 puces", children: _jsx("span", { className: "material-icons text-lg", children: "format_list_bulleted" }) }), _jsx("button", { onClick: onNumberedList, className: "p-1.5 text-gray-600 hover:text-gray-900 hover:bg-gray-200 rounded transition-colors", title: "Liste num\u00E9rot\u00E9e", children: _jsx("span", { className: "material-icons text-lg", children: "format_list_numbered" }) }), _jsx("button", { onClick: onQuote, className: "p-1.5 text-gray-600 hover:text-gray-900 hover:bg-gray-200 rounded transition-colors", title: "Citation", children: _jsx("span", { className: "material-icons text-lg", children: "format_quote" }) }), _jsx("button", { onClick: onLink, className: "p-1.5 text-gray-600 hover:text-gray-900 hover:bg-gray-200 rounded transition-colors", title: "Lien", children: _jsx("span", { className: "material-icons text-lg", children: "link" }) }), _jsx("div", { className: "w-px h-6 bg-gray-300 mx-1" })] })), _jsxs("button", { onClick: onRewrite, disabled: isLoading, className: "flex items-center gap-1 px-3 py-1.5 text-sm bg-blue-500 text-white rounded-lg hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed transition-colors", title: hasSelection ? "Réécrire la sélection" : "Réécrire tout le texte", children: [_jsx("span", { className: "material-icons text-base", children: "auto_fix_high" }), "R\u00E9\u00E9crire"] }), _jsxs("button", { onClick: onProofread, disabled: isLoading, className: "flex items-center gap-1 px-3 py-1.5 text-sm bg-green-500 text-white rounded-lg hover:bg-green-600 disabled:opacity-50 disabled:cursor-not-allowed transition-colors", title: hasSelection ? "Corriger la sélection" : "Corriger tout le texte", children: [_jsx("span", { className: "material-icons text-base", children: "spellcheck" }), "Proof reading"] }), _jsxs("button", { onClick: onCustomPrompt, disabled: isLoading, className: "flex items-center gap-1 px-3 py-1.5 text-sm bg-purple-500 text-white rounded-lg hover:bg-purple-600 disabled:opacity-50 disabled:cursor-not-allowed transition-colors", title: "Instruction personnalis\u00E9e", children: [_jsx("span", { className: "material-icons text-base", children: "smart_toy" }), "Custom..."] }), _jsx("div", { className: "flex-1" }), hasSelection && (_jsx("span", { className: "text-xs text-blue-600 bg-blue-50 px-2 py-1 rounded", children: "Texte s\u00E9lectionn\u00E9" })), isLoading && (_jsxs("span", { className: "text-xs text-amber-600 bg-amber-50 px-2 py-1 rounded flex items-center gap-1", children: [_jsx("span", { className: "material-icons text-sm animate-spin", children: "sync" }), "IA en cours..."] })), _jsx("div", { className: "w-px h-6 bg-gray-300 mx-1" }), _jsx("button", { onClick: onUndo, disabled: !canUndo || isLoading, className: "p-1.5 text-gray-600 hover:text-gray-900 hover:bg-gray-200 rounded disabled:opacity-30 disabled:cursor-not-allowed transition-colors", title: "Annuler (Ctrl+Z)", children: _jsx("span", { className: "material-icons text-xl", children: "undo" }) }), _jsx("button", { onClick: onRedo, disabled: !canRedo || isLoading, className: "p-1.5 text-gray-600 hover:text-gray-900 hover:bg-gray-200 rounded disabled:opacity-30 disabled:cursor-not-allowed transition-colors", title: "Refaire (Ctrl+Y)", children: _jsx("span", { className: "material-icons text-xl", children: "redo" }) })] }));
|
|
10
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface EditorToolbarProps {
|
|
2
|
+
onAIAction?: (action: 'rewrite' | 'proofread' | 'custom', customPrompt?: string) => void;
|
|
3
|
+
isLoading?: boolean;
|
|
4
|
+
isFullscreen?: boolean;
|
|
5
|
+
onToggleFullscreen?: () => void;
|
|
6
|
+
}
|
|
7
|
+
export declare function EditorToolbar({ onAIAction, isLoading, isFullscreen, onToggleFullscreen }: EditorToolbarProps): import("react/jsx-runtime").JSX.Element;
|
|
8
|
+
//# sourceMappingURL=EditorToolbar.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"EditorToolbar.d.ts","sourceRoot":"","sources":["../../src/components/EditorToolbar.tsx"],"names":[],"mappings":"AA8BA,MAAM,WAAW,kBAAkB;IACjC,UAAU,CAAC,EAAE,CAAC,MAAM,EAAE,SAAS,GAAG,WAAW,GAAG,QAAQ,EAAE,YAAY,CAAC,EAAE,MAAM,KAAK,IAAI,CAAA;IACxF,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,kBAAkB,CAAC,EAAE,MAAM,IAAI,CAAA;CAChC;AA0DD,wBAAgB,aAAa,CAAC,EAAE,UAAU,EAAE,SAAiB,EAAE,YAAoB,EAAE,kBAAkB,EAAE,EAAE,kBAAkB,2CA+iB5H"}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useCallback, useEffect, useState, useRef } from 'react';
|
|
4
|
+
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
|
5
|
+
import { FORMAT_TEXT_COMMAND, FORMAT_ELEMENT_COMMAND, UNDO_COMMAND, REDO_COMMAND, $getSelection, $isRangeSelection, $selectAll, CAN_UNDO_COMMAND, CAN_REDO_COMMAND, COMMAND_PRIORITY_CRITICAL, } from 'lexical';
|
|
6
|
+
import { INSERT_OBJECT_COMMAND } from '../plugins/InsertObjectPlugin';
|
|
7
|
+
import { INSERT_UNORDERED_LIST_COMMAND, INSERT_ORDERED_LIST_COMMAND, } from '@lexical/list';
|
|
8
|
+
import { $createHeadingNode, $createQuoteNode, } from '@lexical/rich-text';
|
|
9
|
+
import { $setBlocksType, $patchStyleText } from '@lexical/selection';
|
|
10
|
+
import { $createParagraphNode } from 'lexical';
|
|
11
|
+
const FONT_FAMILIES = [
|
|
12
|
+
{ label: 'Par défaut', value: '' },
|
|
13
|
+
{ label: 'Arial', value: 'Arial, sans-serif' },
|
|
14
|
+
{ label: 'Times New Roman', value: 'Times New Roman, serif' },
|
|
15
|
+
{ label: 'Georgia', value: 'Georgia, serif' },
|
|
16
|
+
{ label: 'Courier New', value: 'Courier New, monospace' },
|
|
17
|
+
{ label: 'Verdana', value: 'Verdana, sans-serif' },
|
|
18
|
+
{ label: 'Trebuchet MS', value: 'Trebuchet MS, sans-serif' },
|
|
19
|
+
{ label: 'Comic Sans MS', value: 'Comic Sans MS, cursive' },
|
|
20
|
+
];
|
|
21
|
+
// Dropdown component using fixed positioning to escape overflow containers
|
|
22
|
+
function FixedDropdown({ show, buttonRef, children }) {
|
|
23
|
+
const [position, setPosition] = useState({ top: 0, left: 0 });
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (show && buttonRef.current) {
|
|
26
|
+
const rect = buttonRef.current.getBoundingClientRect();
|
|
27
|
+
setPosition({
|
|
28
|
+
top: rect.bottom + 4,
|
|
29
|
+
left: rect.left
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}, [show, buttonRef]);
|
|
33
|
+
if (!show)
|
|
34
|
+
return null;
|
|
35
|
+
return (_jsx("div", { style: {
|
|
36
|
+
position: 'fixed',
|
|
37
|
+
top: position.top,
|
|
38
|
+
left: position.left,
|
|
39
|
+
zIndex: 99999,
|
|
40
|
+
backgroundColor: 'white',
|
|
41
|
+
border: '1px solid #e5e7eb',
|
|
42
|
+
borderRadius: '8px',
|
|
43
|
+
boxShadow: '0 10px 15px -3px rgba(0, 0, 0, 0.1)',
|
|
44
|
+
padding: '4px 0',
|
|
45
|
+
minWidth: '180px'
|
|
46
|
+
}, onClick: (e) => e.stopPropagation(), children: children }));
|
|
47
|
+
}
|
|
48
|
+
export function EditorToolbar({ onAIAction, isLoading = false, isFullscreen = false, onToggleFullscreen }) {
|
|
49
|
+
const [editor] = useLexicalComposerContext();
|
|
50
|
+
const [canUndo, setCanUndo] = useState(false);
|
|
51
|
+
const [canRedo, setCanRedo] = useState(false);
|
|
52
|
+
const [isBold, setIsBold] = useState(false);
|
|
53
|
+
const [isItalic, setIsItalic] = useState(false);
|
|
54
|
+
const [isUnderline, setIsUnderline] = useState(false);
|
|
55
|
+
const [isStrikethrough, setIsStrikethrough] = useState(false);
|
|
56
|
+
const [showHeadingMenu, setShowHeadingMenu] = useState(false);
|
|
57
|
+
const [showAIMenu, setShowAIMenu] = useState(false);
|
|
58
|
+
const [showFontMenu, setShowFontMenu] = useState(false);
|
|
59
|
+
const [currentFont, setCurrentFont] = useState('');
|
|
60
|
+
const [fontSize, setFontSize] = useState(16);
|
|
61
|
+
const fontButtonRef = useRef(null);
|
|
62
|
+
const headingButtonRef = useRef(null);
|
|
63
|
+
const aiButtonRef = useRef(null);
|
|
64
|
+
const toolbarRef = useRef(null);
|
|
65
|
+
// Update toolbar state based on selection
|
|
66
|
+
const updateToolbar = useCallback(() => {
|
|
67
|
+
const selection = $getSelection();
|
|
68
|
+
if ($isRangeSelection(selection)) {
|
|
69
|
+
setIsBold(selection.hasFormat('bold'));
|
|
70
|
+
setIsItalic(selection.hasFormat('italic'));
|
|
71
|
+
setIsUnderline(selection.hasFormat('underline'));
|
|
72
|
+
setIsStrikethrough(selection.hasFormat('strikethrough'));
|
|
73
|
+
}
|
|
74
|
+
}, []);
|
|
75
|
+
// Register listeners
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
return editor.registerUpdateListener(({ editorState }) => {
|
|
78
|
+
editorState.read(() => {
|
|
79
|
+
updateToolbar();
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
}, [editor, updateToolbar]);
|
|
83
|
+
// Register undo/redo commands
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
return editor.registerCommand(CAN_UNDO_COMMAND, (payload) => {
|
|
86
|
+
setCanUndo(payload);
|
|
87
|
+
return false;
|
|
88
|
+
}, COMMAND_PRIORITY_CRITICAL);
|
|
89
|
+
}, [editor]);
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
return editor.registerCommand(CAN_REDO_COMMAND, (payload) => {
|
|
92
|
+
setCanRedo(payload);
|
|
93
|
+
return false;
|
|
94
|
+
}, COMMAND_PRIORITY_CRITICAL);
|
|
95
|
+
}, [editor]);
|
|
96
|
+
// Format handlers
|
|
97
|
+
const formatBold = () => editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'bold');
|
|
98
|
+
const formatItalic = () => editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'italic');
|
|
99
|
+
const formatUnderline = () => editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'underline');
|
|
100
|
+
const formatStrikethrough = () => editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'strikethrough');
|
|
101
|
+
const formatHeading = (headingTag) => {
|
|
102
|
+
editor.update(() => {
|
|
103
|
+
const selection = $getSelection();
|
|
104
|
+
if ($isRangeSelection(selection)) {
|
|
105
|
+
$setBlocksType(selection, () => $createHeadingNode(headingTag));
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
setShowHeadingMenu(false);
|
|
109
|
+
};
|
|
110
|
+
const formatParagraph = () => {
|
|
111
|
+
editor.update(() => {
|
|
112
|
+
const selection = $getSelection();
|
|
113
|
+
if ($isRangeSelection(selection)) {
|
|
114
|
+
$setBlocksType(selection, () => $createParagraphNode());
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
setShowHeadingMenu(false);
|
|
118
|
+
};
|
|
119
|
+
const formatQuote = () => {
|
|
120
|
+
editor.update(() => {
|
|
121
|
+
const selection = $getSelection();
|
|
122
|
+
if ($isRangeSelection(selection)) {
|
|
123
|
+
$setBlocksType(selection, () => $createQuoteNode());
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
};
|
|
127
|
+
const openInsertDialog = () => {
|
|
128
|
+
editor.dispatchCommand(INSERT_OBJECT_COMMAND, undefined);
|
|
129
|
+
};
|
|
130
|
+
const formatAlign = (alignment) => {
|
|
131
|
+
editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, alignment);
|
|
132
|
+
};
|
|
133
|
+
const formatBulletList = () => {
|
|
134
|
+
editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, undefined);
|
|
135
|
+
};
|
|
136
|
+
const formatNumberedList = () => {
|
|
137
|
+
editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND, undefined);
|
|
138
|
+
};
|
|
139
|
+
const undo = () => editor.dispatchCommand(UNDO_COMMAND, undefined);
|
|
140
|
+
const redo = () => editor.dispatchCommand(REDO_COMMAND, undefined);
|
|
141
|
+
// Close menus when clicking outside the toolbar
|
|
142
|
+
useEffect(() => {
|
|
143
|
+
const handleClickOutside = (e) => {
|
|
144
|
+
if (toolbarRef.current && !toolbarRef.current.contains(e.target)) {
|
|
145
|
+
setShowHeadingMenu(false);
|
|
146
|
+
setShowAIMenu(false);
|
|
147
|
+
setShowFontMenu(false);
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
151
|
+
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
152
|
+
}, []);
|
|
153
|
+
// Apply font family to selection
|
|
154
|
+
const applyFontFamily = (fontFamily) => {
|
|
155
|
+
editor.update(() => {
|
|
156
|
+
let selection = $getSelection();
|
|
157
|
+
if (!$isRangeSelection(selection) || selection.isCollapsed()) {
|
|
158
|
+
$selectAll();
|
|
159
|
+
selection = $getSelection();
|
|
160
|
+
}
|
|
161
|
+
if ($isRangeSelection(selection)) {
|
|
162
|
+
$patchStyleText(selection, { 'font-family': fontFamily || null });
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
setCurrentFont(fontFamily);
|
|
166
|
+
setShowFontMenu(false);
|
|
167
|
+
};
|
|
168
|
+
// Apply font size to selection
|
|
169
|
+
const applyFontSize = (size) => {
|
|
170
|
+
editor.update(() => {
|
|
171
|
+
let selection = $getSelection();
|
|
172
|
+
if (!$isRangeSelection(selection) || selection.isCollapsed()) {
|
|
173
|
+
$selectAll();
|
|
174
|
+
selection = $getSelection();
|
|
175
|
+
}
|
|
176
|
+
if ($isRangeSelection(selection)) {
|
|
177
|
+
$patchStyleText(selection, { 'font-size': `${size}px` });
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
setFontSize(size);
|
|
181
|
+
};
|
|
182
|
+
const increaseFontSize = () => {
|
|
183
|
+
const newSize = Math.min(fontSize + 2, 72);
|
|
184
|
+
applyFontSize(newSize);
|
|
185
|
+
};
|
|
186
|
+
const decreaseFontSize = () => {
|
|
187
|
+
const newSize = Math.max(fontSize - 2, 8);
|
|
188
|
+
applyFontSize(newSize);
|
|
189
|
+
};
|
|
190
|
+
const handleFontSizeChange = (e) => {
|
|
191
|
+
const value = parseInt(e.target.value, 10);
|
|
192
|
+
if (!isNaN(value) && value >= 8 && value <= 72) {
|
|
193
|
+
applyFontSize(value);
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
setFontSize(value || 16);
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
const handleFontSizeBlur = () => {
|
|
200
|
+
if (fontSize < 8)
|
|
201
|
+
setFontSize(8);
|
|
202
|
+
if (fontSize > 72)
|
|
203
|
+
setFontSize(72);
|
|
204
|
+
};
|
|
205
|
+
return (_jsxs("div", { ref: toolbarRef, className: "flex flex-wrap items-center gap-1 p-2 bg-gray-50 border-b border-gray-200 flex-shrink-0", children: [_jsx("button", { onClick: undo, disabled: !canUndo, className: "p-1.5 text-gray-600 hover:text-gray-900 hover:bg-gray-200 rounded disabled:opacity-30 disabled:cursor-not-allowed transition-colors", title: "Annuler (Ctrl+Z)", children: _jsx("span", { className: "material-icons text-lg", children: "undo" }) }), _jsx("button", { onClick: redo, disabled: !canRedo, className: "p-1.5 text-gray-600 hover:text-gray-900 hover:bg-gray-200 rounded disabled:opacity-30 disabled:cursor-not-allowed transition-colors", title: "Refaire (Ctrl+Y)", children: _jsx("span", { className: "material-icons text-lg", children: "redo" }) }), _jsx("div", { className: "w-px h-6 bg-gray-300 mx-1" }), _jsx("button", { onClick: formatBold, className: `p-1.5 rounded transition-colors ${isBold
|
|
206
|
+
? 'bg-gray-200 text-gray-900'
|
|
207
|
+
: 'text-gray-600 hover:text-gray-900 hover:bg-gray-200'}`, title: "Gras (Ctrl+B)", children: _jsx("span", { className: "material-icons text-lg", children: "format_bold" }) }), _jsx("button", { onClick: formatItalic, className: `p-1.5 rounded transition-colors ${isItalic
|
|
208
|
+
? 'bg-gray-200 text-gray-900'
|
|
209
|
+
: 'text-gray-600 hover:text-gray-900 hover:bg-gray-200'}`, title: "Italique (Ctrl+I)", children: _jsx("span", { className: "material-icons text-lg", children: "format_italic" }) }), _jsx("button", { onClick: formatUnderline, className: `p-1.5 rounded transition-colors ${isUnderline
|
|
210
|
+
? 'bg-gray-200 text-gray-900'
|
|
211
|
+
: 'text-gray-600 hover:text-gray-900 hover:bg-gray-200'}`, title: "Soulign\u00E9 (Ctrl+U)", children: _jsx("span", { className: "material-icons text-lg", children: "format_underlined" }) }), _jsx("button", { onClick: formatStrikethrough, className: `p-1.5 rounded transition-colors ${isStrikethrough
|
|
212
|
+
? 'bg-gray-200 text-gray-900'
|
|
213
|
+
: 'text-gray-600 hover:text-gray-900 hover:bg-gray-200'}`, title: "Barr\u00E9", children: _jsx("span", { className: "material-icons text-lg", children: "strikethrough_s" }) }), _jsx("div", { className: "w-px h-6 bg-gray-300 mx-1" }), _jsxs("div", { style: { position: 'relative' }, children: [_jsxs("button", { ref: fontButtonRef, onClick: (e) => {
|
|
214
|
+
e.stopPropagation();
|
|
215
|
+
setShowFontMenu(!showFontMenu);
|
|
216
|
+
setShowHeadingMenu(false);
|
|
217
|
+
setShowAIMenu(false);
|
|
218
|
+
}, className: "h-8 px-2 text-gray-600 hover:text-gray-900 hover:bg-gray-200 rounded transition-colors flex items-center gap-1 text-sm border border-gray-300 bg-white min-w-[100px]", title: "Police", children: [_jsx("span", { className: "truncate flex-1 text-left", style: { fontFamily: currentFont || 'inherit' }, children: FONT_FAMILIES.find(f => f.value === currentFont)?.label || 'Police' }), _jsx("span", { className: "material-icons text-sm", children: "arrow_drop_down" })] }), _jsxs(FixedDropdown, { show: showFontMenu, buttonRef: fontButtonRef, children: [_jsx("button", { onClick: () => applyFontFamily(''), style: { display: 'block', width: '100%', padding: '8px 16px', textAlign: 'left', fontSize: '14px', backgroundColor: 'transparent', border: 'none', cursor: 'pointer', color: '#374151' }, onMouseEnter: (e) => e.currentTarget.style.backgroundColor = '#f3f4f6', onMouseLeave: (e) => e.currentTarget.style.backgroundColor = 'transparent', children: "Par d\u00E9faut" }), _jsx("button", { onClick: () => applyFontFamily('Arial, sans-serif'), style: { display: 'block', width: '100%', padding: '8px 16px', textAlign: 'left', fontSize: '14px', backgroundColor: 'transparent', border: 'none', cursor: 'pointer', color: '#374151', fontFamily: 'Arial, sans-serif' }, onMouseEnter: (e) => e.currentTarget.style.backgroundColor = '#f3f4f6', onMouseLeave: (e) => e.currentTarget.style.backgroundColor = 'transparent', children: "Arial" }), _jsx("button", { onClick: () => applyFontFamily('Times New Roman, serif'), style: { display: 'block', width: '100%', padding: '8px 16px', textAlign: 'left', fontSize: '14px', backgroundColor: 'transparent', border: 'none', cursor: 'pointer', color: '#374151', fontFamily: 'Times New Roman, serif' }, onMouseEnter: (e) => e.currentTarget.style.backgroundColor = '#f3f4f6', onMouseLeave: (e) => e.currentTarget.style.backgroundColor = 'transparent', children: "Times New Roman" }), _jsx("button", { onClick: () => applyFontFamily('Georgia, serif'), style: { display: 'block', width: '100%', padding: '8px 16px', textAlign: 'left', fontSize: '14px', backgroundColor: 'transparent', border: 'none', cursor: 'pointer', color: '#374151', fontFamily: 'Georgia, serif' }, onMouseEnter: (e) => e.currentTarget.style.backgroundColor = '#f3f4f6', onMouseLeave: (e) => e.currentTarget.style.backgroundColor = 'transparent', children: "Georgia" }), _jsx("button", { onClick: () => applyFontFamily('Courier New, monospace'), style: { display: 'block', width: '100%', padding: '8px 16px', textAlign: 'left', fontSize: '14px', backgroundColor: 'transparent', border: 'none', cursor: 'pointer', color: '#374151', fontFamily: 'Courier New, monospace' }, onMouseEnter: (e) => e.currentTarget.style.backgroundColor = '#f3f4f6', onMouseLeave: (e) => e.currentTarget.style.backgroundColor = 'transparent', children: "Courier New" }), _jsx("button", { onClick: () => applyFontFamily('Verdana, sans-serif'), style: { display: 'block', width: '100%', padding: '8px 16px', textAlign: 'left', fontSize: '14px', backgroundColor: 'transparent', border: 'none', cursor: 'pointer', color: '#374151', fontFamily: 'Verdana, sans-serif' }, onMouseEnter: (e) => e.currentTarget.style.backgroundColor = '#f3f4f6', onMouseLeave: (e) => e.currentTarget.style.backgroundColor = 'transparent', children: "Verdana" })] })] }), _jsxs("div", { className: "flex items-center border border-gray-300 rounded bg-white h-8", children: [_jsx("button", { onClick: decreaseFontSize, className: "px-1 h-full text-gray-600 hover:text-gray-900 hover:bg-gray-100 transition-colors", title: "R\u00E9duire la taille", children: _jsx("span", { className: "material-icons text-sm", children: "remove" }) }), _jsx("input", { type: "number", value: fontSize, onChange: handleFontSizeChange, onBlur: handleFontSizeBlur, className: "w-10 h-full text-center text-sm text-gray-900 border-x border-gray-300 focus:outline-none", min: 8, max: 72, title: "Taille de police (px)" }), _jsx("button", { onClick: increaseFontSize, className: "px-1 h-full text-gray-600 hover:text-gray-900 hover:bg-gray-100 transition-colors", title: "Augmenter la taille", children: _jsx("span", { className: "material-icons text-sm", children: "add" }) })] }), _jsx("div", { className: "w-px h-6 bg-gray-300 mx-1" }), _jsxs("div", { style: { position: 'relative' }, children: [_jsxs("button", { ref: headingButtonRef, onClick: (e) => {
|
|
219
|
+
e.stopPropagation();
|
|
220
|
+
setShowHeadingMenu(!showHeadingMenu);
|
|
221
|
+
setShowAIMenu(false);
|
|
222
|
+
setShowFontMenu(false);
|
|
223
|
+
}, className: "p-1.5 text-gray-600 hover:text-gray-900 hover:bg-gray-200 rounded transition-colors flex items-center", title: "Titres", children: [_jsx("span", { className: "material-icons text-lg", children: "title" }), _jsx("span", { className: "material-icons text-sm", children: "arrow_drop_down" })] }), _jsxs(FixedDropdown, { show: showHeadingMenu, buttonRef: headingButtonRef, children: [_jsx("button", { onClick: formatParagraph, style: { display: 'block', width: '100%', padding: '6px 16px', textAlign: 'left', fontSize: '14px', backgroundColor: 'transparent', border: 'none', cursor: 'pointer', color: '#374151' }, onMouseEnter: (e) => e.currentTarget.style.backgroundColor = '#f3f4f6', onMouseLeave: (e) => e.currentTarget.style.backgroundColor = 'transparent', children: "Paragraphe" }), _jsx("button", { onClick: () => formatHeading('h1'), style: { display: 'block', width: '100%', padding: '6px 16px', textAlign: 'left', fontSize: '20px', fontWeight: 'bold', backgroundColor: 'transparent', border: 'none', cursor: 'pointer', color: '#374151' }, onMouseEnter: (e) => e.currentTarget.style.backgroundColor = '#f3f4f6', onMouseLeave: (e) => e.currentTarget.style.backgroundColor = 'transparent', children: "Titre 1" }), _jsx("button", { onClick: () => formatHeading('h2'), style: { display: 'block', width: '100%', padding: '6px 16px', textAlign: 'left', fontSize: '18px', fontWeight: 'bold', backgroundColor: 'transparent', border: 'none', cursor: 'pointer', color: '#374151' }, onMouseEnter: (e) => e.currentTarget.style.backgroundColor = '#f3f4f6', onMouseLeave: (e) => e.currentTarget.style.backgroundColor = 'transparent', children: "Titre 2" }), _jsx("button", { onClick: () => formatHeading('h3'), style: { display: 'block', width: '100%', padding: '6px 16px', textAlign: 'left', fontSize: '16px', fontWeight: '600', backgroundColor: 'transparent', border: 'none', cursor: 'pointer', color: '#374151' }, onMouseEnter: (e) => e.currentTarget.style.backgroundColor = '#f3f4f6', onMouseLeave: (e) => e.currentTarget.style.backgroundColor = 'transparent', children: "Titre 3" })] })] }), _jsx("div", { className: "w-px h-6 bg-gray-300 mx-1" }), _jsx("button", { onClick: formatBulletList, className: "p-1.5 text-gray-600 hover:text-gray-900 hover:bg-gray-200 rounded transition-colors", title: "Liste \u00E0 puces", children: _jsx("span", { className: "material-icons text-lg", children: "format_list_bulleted" }) }), _jsx("button", { onClick: formatNumberedList, className: "p-1.5 text-gray-600 hover:text-gray-900 hover:bg-gray-200 rounded transition-colors", title: "Liste num\u00E9rot\u00E9e", children: _jsx("span", { className: "material-icons text-lg", children: "format_list_numbered" }) }), _jsx("button", { onClick: formatQuote, className: "p-1.5 text-gray-600 hover:text-gray-900 hover:bg-gray-200 rounded transition-colors", title: "Citation", children: _jsx("span", { className: "material-icons text-lg", children: "format_quote" }) }), _jsx("div", { className: "w-px h-6 bg-gray-300 mx-1" }), _jsx("button", { onClick: () => formatAlign('left'), className: "p-1.5 text-gray-600 hover:text-gray-900 hover:bg-gray-200 rounded transition-colors", title: "Aligner \u00E0 gauche", children: _jsx("span", { className: "material-icons text-lg", children: "format_align_left" }) }), _jsx("button", { onClick: () => formatAlign('center'), className: "p-1.5 text-gray-600 hover:text-gray-900 hover:bg-gray-200 rounded transition-colors", title: "Centrer", children: _jsx("span", { className: "material-icons text-lg", children: "format_align_center" }) }), _jsx("button", { onClick: () => formatAlign('right'), className: "p-1.5 text-gray-600 hover:text-gray-900 hover:bg-gray-200 rounded transition-colors", title: "Aligner \u00E0 droite", children: _jsx("span", { className: "material-icons text-lg", children: "format_align_right" }) }), _jsx("button", { onClick: () => formatAlign('justify'), className: "p-1.5 text-gray-600 hover:text-gray-900 hover:bg-gray-200 rounded transition-colors", title: "Justifier", children: _jsx("span", { className: "material-icons text-lg", children: "format_align_justify" }) }), _jsx("div", { className: "w-px h-6 bg-gray-300 mx-1" }), _jsx("button", { onClick: openInsertDialog, className: "p-1.5 text-gray-600 hover:text-gray-900 hover:bg-gray-200 rounded transition-colors", title: "Ins\u00E9rer un objet (Ctrl+K)", children: _jsx("span", { className: "material-icons text-lg", children: "add_photo_alternate" }) }), _jsx("div", { className: "flex-1" }), onAIAction && (_jsxs("div", { style: { position: 'relative' }, children: [_jsxs("button", { ref: aiButtonRef, onClick: (e) => {
|
|
224
|
+
e.stopPropagation();
|
|
225
|
+
setShowAIMenu(!showAIMenu);
|
|
226
|
+
setShowHeadingMenu(false);
|
|
227
|
+
setShowFontMenu(false);
|
|
228
|
+
}, disabled: isLoading, className: "flex items-center gap-1 px-3 py-1.5 text-sm bg-purple-500 text-white rounded-lg hover:bg-purple-600 disabled:opacity-50 disabled:cursor-not-allowed transition-colors", title: "Actions IA", children: [isLoading ? (_jsx("span", { className: "material-icons text-base animate-spin", children: "sync" })) : (_jsx("span", { className: "material-icons text-base", children: "auto_awesome" })), _jsx("span", { children: "IA" }), _jsx("span", { className: "material-icons text-sm", children: "arrow_drop_down" })] }), !isLoading && (_jsxs(FixedDropdown, { show: showAIMenu, buttonRef: aiButtonRef, children: [_jsxs("button", { onClick: () => {
|
|
229
|
+
onAIAction('rewrite');
|
|
230
|
+
setShowAIMenu(false);
|
|
231
|
+
}, style: { display: 'flex', width: '100%', padding: '8px 16px', textAlign: 'left', fontSize: '14px', backgroundColor: 'transparent', border: 'none', cursor: 'pointer', color: '#374151', alignItems: 'center', gap: '8px' }, onMouseEnter: (e) => e.currentTarget.style.backgroundColor = '#f3f4f6', onMouseLeave: (e) => e.currentTarget.style.backgroundColor = 'transparent', children: [_jsx("span", { className: "material-icons text-base text-blue-500", children: "auto_fix_high" }), "R\u00E9\u00E9crire"] }), _jsxs("button", { onClick: () => {
|
|
232
|
+
onAIAction('proofread');
|
|
233
|
+
setShowAIMenu(false);
|
|
234
|
+
}, style: { display: 'flex', width: '100%', padding: '8px 16px', textAlign: 'left', fontSize: '14px', backgroundColor: 'transparent', border: 'none', cursor: 'pointer', color: '#374151', alignItems: 'center', gap: '8px' }, onMouseEnter: (e) => e.currentTarget.style.backgroundColor = '#f3f4f6', onMouseLeave: (e) => e.currentTarget.style.backgroundColor = 'transparent', children: [_jsx("span", { className: "material-icons text-base text-green-500", children: "spellcheck" }), "Corriger"] }), _jsxs("button", { onClick: () => {
|
|
235
|
+
const prompt = window.prompt('Instruction pour l\'IA:');
|
|
236
|
+
if (prompt) {
|
|
237
|
+
onAIAction('custom', prompt);
|
|
238
|
+
}
|
|
239
|
+
setShowAIMenu(false);
|
|
240
|
+
}, style: { display: 'flex', width: '100%', padding: '8px 16px', textAlign: 'left', fontSize: '14px', backgroundColor: 'transparent', border: 'none', cursor: 'pointer', color: '#374151', alignItems: 'center', gap: '8px' }, onMouseEnter: (e) => e.currentTarget.style.backgroundColor = '#f3f4f6', onMouseLeave: (e) => e.currentTarget.style.backgroundColor = 'transparent', children: [_jsx("span", { className: "material-icons text-base text-purple-500", children: "smart_toy" }), "Personnalis\u00E9..."] })] }))] })), onToggleFullscreen && (_jsx("button", { onClick: onToggleFullscreen, className: "p-1.5 text-gray-600 hover:text-gray-900 hover:bg-gray-200 rounded transition-colors", title: isFullscreen ? "Quitter le plein écran" : "Plein écran", children: _jsx("span", { className: "material-icons text-lg", children: isFullscreen ? 'fullscreen_exit' : 'fullscreen' }) }))] }));
|
|
241
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
export interface MarkdownPreviewProps {
|
|
3
|
+
content: string;
|
|
4
|
+
className?: string;
|
|
5
|
+
}
|
|
6
|
+
export declare const MarkdownPreview: React.ForwardRefExoticComponent<MarkdownPreviewProps & React.RefAttributes<HTMLDivElement>>;
|
|
7
|
+
//# sourceMappingURL=MarkdownPreview.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MarkdownPreview.d.ts","sourceRoot":"","sources":["../../src/components/MarkdownPreview.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAqB,MAAM,OAAO,CAAA;AAEzC,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,MAAM,CAAA;IACf,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAGD,eAAO,MAAM,eAAe,6FA+C3B,CAAA"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef } from 'react';
|
|
3
|
+
// Simple markdown renderer - can be enhanced with react-markdown
|
|
4
|
+
export const MarkdownPreview = forwardRef(({ content, className = '' }, ref) => {
|
|
5
|
+
// Basic markdown to HTML conversion
|
|
6
|
+
const renderMarkdown = (md) => {
|
|
7
|
+
let html = md
|
|
8
|
+
// Escape HTML
|
|
9
|
+
.replace(/&/g, '&')
|
|
10
|
+
.replace(/</g, '<')
|
|
11
|
+
.replace(/>/g, '>')
|
|
12
|
+
// Headers
|
|
13
|
+
.replace(/^### (.*$)/gm, '<h3 class="text-lg font-semibold mt-4 mb-2">$1</h3>')
|
|
14
|
+
.replace(/^## (.*$)/gm, '<h2 class="text-xl font-semibold mt-6 mb-3">$1</h2>')
|
|
15
|
+
.replace(/^# (.*$)/gm, '<h1 class="text-2xl font-bold mt-6 mb-4">$1</h1>')
|
|
16
|
+
// Bold and italic
|
|
17
|
+
.replace(/\*\*\*(.*?)\*\*\*/g, '<strong><em>$1</em></strong>')
|
|
18
|
+
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
|
|
19
|
+
.replace(/\*(.*?)\*/g, '<em>$1</em>')
|
|
20
|
+
// Code blocks
|
|
21
|
+
.replace(/```(\w*)\n([\s\S]*?)```/g, '<pre class="bg-gray-100 p-3 rounded-lg my-3 overflow-x-auto"><code>$2</code></pre>')
|
|
22
|
+
// Inline code
|
|
23
|
+
.replace(/`([^`]+)`/g, '<code class="bg-gray-100 px-1.5 py-0.5 rounded text-sm">$1</code>')
|
|
24
|
+
// Lists
|
|
25
|
+
.replace(/^\s*[-*]\s+(.*$)/gm, '<li class="ml-4">$1</li>')
|
|
26
|
+
// Numbered lists
|
|
27
|
+
.replace(/^\s*\d+\.\s+(.*$)/gm, '<li class="ml-4 list-decimal">$1</li>')
|
|
28
|
+
// Line breaks (double newline = paragraph)
|
|
29
|
+
.replace(/\n\n/g, '</p><p class="mb-3">')
|
|
30
|
+
// Single line breaks
|
|
31
|
+
.replace(/\n/g, '<br/>');
|
|
32
|
+
// Wrap in paragraph
|
|
33
|
+
html = `<p class="mb-3">${html}</p>`;
|
|
34
|
+
// Clean up empty paragraphs
|
|
35
|
+
html = html.replace(/<p class="mb-3"><\/p>/g, '');
|
|
36
|
+
return html;
|
|
37
|
+
};
|
|
38
|
+
return (_jsx("div", { ref: ref, className: `prose prose-sm max-w-none select-text ${className}`, dangerouslySetInnerHTML: { __html: renderMarkdown(content) } }));
|
|
39
|
+
});
|
|
40
|
+
MarkdownPreview.displayName = 'MarkdownPreview';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface PromptModalProps {
|
|
2
|
+
isOpen: boolean;
|
|
3
|
+
onClose: () => void;
|
|
4
|
+
onSubmit: (prompt: string) => void;
|
|
5
|
+
selectedText?: string;
|
|
6
|
+
isLoading: boolean;
|
|
7
|
+
}
|
|
8
|
+
export declare function PromptModal({ isOpen, onClose, onSubmit, selectedText, isLoading }: PromptModalProps): import("react/jsx-runtime").JSX.Element | null;
|
|
9
|
+
//# sourceMappingURL=PromptModal.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PromptModal.d.ts","sourceRoot":"","sources":["../../src/components/PromptModal.tsx"],"names":[],"mappings":"AAEA,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,OAAO,CAAA;IACf,OAAO,EAAE,MAAM,IAAI,CAAA;IACnB,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAA;IAClC,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,SAAS,EAAE,OAAO,CAAA;CACnB;AAED,wBAAgB,WAAW,CAAC,EAC1B,MAAM,EACN,OAAO,EACP,QAAQ,EACR,YAAY,EACZ,SAAS,EACV,EAAE,gBAAgB,kDAkIlB"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useEffect, useRef } from 'react';
|
|
3
|
+
export function PromptModal({ isOpen, onClose, onSubmit, selectedText, isLoading }) {
|
|
4
|
+
const [prompt, setPrompt] = useState('');
|
|
5
|
+
const inputRef = useRef(null);
|
|
6
|
+
useEffect(() => {
|
|
7
|
+
if (isOpen && inputRef.current) {
|
|
8
|
+
inputRef.current.focus();
|
|
9
|
+
}
|
|
10
|
+
}, [isOpen]);
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
const handleKeyDown = (e) => {
|
|
13
|
+
if (e.key === 'Escape' && isOpen) {
|
|
14
|
+
onClose();
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
window.addEventListener('keydown', handleKeyDown);
|
|
18
|
+
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
19
|
+
}, [isOpen, onClose]);
|
|
20
|
+
const handleSubmit = (e) => {
|
|
21
|
+
e.preventDefault();
|
|
22
|
+
if (prompt.trim()) {
|
|
23
|
+
onSubmit(prompt.trim());
|
|
24
|
+
setPrompt('');
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
if (!isOpen)
|
|
28
|
+
return null;
|
|
29
|
+
return (_jsx("div", { className: "fixed inset-0 bg-black/50 flex items-center justify-center z-50", children: _jsxs("div", { className: "bg-white rounded-xl shadow-2xl w-full max-w-lg mx-4 overflow-hidden", children: [_jsxs("div", { className: "px-6 py-4 border-b border-gray-200 flex items-center justify-between", children: [_jsx("h3", { className: "text-lg font-semibold text-gray-900", children: "Instruction IA" }), _jsx("button", { onClick: onClose, className: "p-1 text-gray-400 hover:text-gray-600 transition-colors", children: _jsx("span", { className: "material-icons", children: "close" }) })] }), _jsxs("form", { onSubmit: handleSubmit, className: "p-6", children: [selectedText && (_jsxs("div", { className: "mb-4", children: [_jsx("label", { className: "block text-sm font-medium text-gray-700 mb-1", children: "Texte s\u00E9lectionn\u00E9" }), _jsx("div", { className: "p-3 bg-blue-50 border border-blue-200 rounded-lg text-sm text-gray-700 max-h-32 overflow-y-auto", children: selectedText.length > 200
|
|
30
|
+
? `${selectedText.substring(0, 200)}...`
|
|
31
|
+
: selectedText })] })), _jsxs("div", { className: "mb-4", children: [_jsx("label", { className: "block text-sm font-medium text-gray-700 mb-1", children: selectedText ? 'Que voulez-vous faire avec ce texte ?' : 'Instruction pour l\'IA' }), _jsx("textarea", { ref: inputRef, value: prompt, onChange: (e) => setPrompt(e.target.value), placeholder: "Ex: Rends ce texte plus concis, ajoute des exemples, traduis en anglais...", className: "w-full px-4 py-3 border border-gray-300 rounded-lg focus:border-blue-500 focus:ring-1 focus:ring-blue-500 resize-none", rows: 3, disabled: isLoading })] }), _jsxs("div", { className: "mb-4", children: [_jsx("label", { className: "block text-sm font-medium text-gray-500 mb-2", children: "Actions rapides" }), _jsx("div", { className: "flex flex-wrap gap-2", children: [
|
|
32
|
+
'Rends plus concis',
|
|
33
|
+
'Ajoute des détails',
|
|
34
|
+
'Simplifie le vocabulaire',
|
|
35
|
+
'Traduis en anglais',
|
|
36
|
+
'Reformule autrement'
|
|
37
|
+
].map((action) => (_jsx("button", { type: "button", onClick: () => setPrompt(action), className: "px-3 py-1 text-xs bg-gray-100 text-gray-700 rounded-full hover:bg-gray-200 transition-colors", children: action }, action))) })] }), _jsxs("div", { className: "flex justify-end gap-3", children: [_jsx("button", { type: "button", onClick: onClose, className: "px-4 py-2 text-gray-600 hover:text-gray-900 transition-colors", disabled: isLoading, children: "Annuler" }), _jsx("button", { type: "submit", disabled: !prompt.trim() || isLoading, className: "px-6 py-2 bg-purple-500 text-white rounded-lg hover:bg-purple-600 disabled:opacity-50 disabled:cursor-not-allowed transition-colors flex items-center gap-2", children: isLoading ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "material-icons text-sm animate-spin", children: "sync" }), "Traitement..."] })) : (_jsxs(_Fragment, { children: [_jsx("span", { className: "material-icons text-sm", children: "send" }), "Appliquer"] })) })] })] })] }) }));
|
|
38
|
+
}
|