diff --git a/apps/web/components/Objects/Editor/AI/AIEditorToolkit.tsx b/apps/web/components/Objects/Editor/AI/AIEditorToolkit.tsx index 46a01045..b4a7824b 100644 --- a/apps/web/components/Objects/Editor/AI/AIEditorToolkit.tsx +++ b/apps/web/components/Objects/Editor/AI/AIEditorToolkit.tsx @@ -54,7 +54,7 @@ function AIEditorToolkit(props: AIEditorToolkitProps) { - +
@@ -113,42 +113,58 @@ const UserFeedbackModal = (props: AIEditorToolkitProps) => { if (label === 'Writer') { let ai_message = ''; let prompt = getPrompt({ label: label, selection: message }); + await dispatchAIEditor({ type: 'setIsUserInputEnabled', payload: true }); if (prompt) { + await dispatchAIEditor({ type: 'setIsUserInputEnabled', payload: false }); + await dispatchAIEditor({ type: 'setIsWaitingForResponse' }); ai_message = await sendReqWithMessage(prompt); await fillEditorWithText(ai_message); + await dispatchAIEditor({ type: 'setIsNoLongerWaitingForResponse' }); + await dispatchAIEditor({ type: 'setIsUserInputEnabled', payload: true }); } } else if (label === 'ContinueWriting') { + let ai_message = ''; + let text_selection = getTipTapEditorSelectedTextGlobal(); + let prompt = getPrompt({ label: label, selection: text_selection }); + if (prompt) { + await dispatchAIEditor({ type: 'setIsWaitingForResponse' }); + ai_message = await sendReqWithMessage(prompt); + const message_without_original_text = await removeSentences(text_selection, ai_message); + await fillEditorWithText(message_without_original_text); + await dispatchAIEditor({ type: 'setIsNoLongerWaitingForResponse' }); + } + } else if (label === 'MakeLonger') { let ai_message = ''; let text_selection = getTipTapEditorSelectedText(); let prompt = getPrompt({ label: label, selection: text_selection }); if (prompt) { + await dispatchAIEditor({ type: 'setIsWaitingForResponse' }); ai_message = await sendReqWithMessage(prompt); - const message_without_original_text = await removeSentences(text_selection, ai_message); - await fillEditorWithText(message_without_original_text); + await replaceSelectedTextWithText(ai_message); + await dispatchAIEditor({ type: 'setIsNoLongerWaitingForResponse' }); } - } else if (label === 'MakeLonger') { - // Send message to AI - // Wait for response - // Add response to editor - // Close modal - } else if (label === 'GenerateQuiz') { // Send message to AI // Wait for response // Add response to editor // Close modal } else if (label === 'Translate') { - // Send message to AI - // Wait for response - // Add response to editor - // Close modal + let ai_message = ''; + let text_selection = getTipTapEditorSelectedText(); + let prompt = getPrompt({ label: label, selection: text_selection }); + if (prompt) { + await dispatchAIEditor({ type: 'setIsWaitingForResponse' }); + ai_message = await sendReqWithMessage(prompt); + await replaceSelectedTextWithText(ai_message); + await dispatchAIEditor({ type: 'setIsNoLongerWaitingForResponse' }); + } } } const removeSentences = async (textToRemove: string, originalText: string) => { const phrase = textToRemove.toLowerCase(); const original = originalText.toLowerCase(); - + if (original.includes(phrase)) { const regex = new RegExp(phrase, 'g'); const newText = original.replace(regex, ''); @@ -184,6 +200,35 @@ const UserFeedbackModal = (props: AIEditorToolkitProps) => { } } + async function replaceSelectedTextWithText(text: string) { + const words = text.split(' '); + + // Delete the selected text + props.editor.chain().focus().deleteSelection().run(); + + for (let i = 0; i < words.length; i++) { + const textNode = { + type: 'text', + text: words[i], + }; + + props.editor.chain().focus().insertContent(textNode).run(); + + // Add a space after each word except the last one + if (i < words.length - 1) { + const spaceNode = { + type: 'text', + text: ' ', + }; + + props.editor.chain().focus().insertContent(spaceNode).run(); + } + + // Wait for 0.3 seconds before adding the next word + await new Promise(resolve => setTimeout(resolve, 120)); + } + } + const getPrompt = (args: AIPromptsLabels) => { const { label, selection } = args; @@ -192,7 +237,7 @@ const UserFeedbackModal = (props: AIEditorToolkitProps) => { } else if (label === 'ContinueWriting') { return `Continue writing 3 more sentences based on "${selection}"`; } else if (label === 'MakeLonger') { - return `Make longer this paragraph "${selection}"`; + return `Make longer this text longer : "${selection}"`; } else if (label === 'GenerateQuiz') { return `Generate a quiz about "${selection}", only return an array of objects, every object should respect the following interface: interface Answer { @@ -208,11 +253,12 @@ const UserFeedbackModal = (props: AIEditorToolkitProps) => { } " `; } else if (label === 'Translate') { - return `Translate ${selection} to selected language`; + return `Translate "${selection}" to the ` + aiEditorState.chatInputValue + ` language`; } } - const getTipTapEditorSelectedText = () => { + const getTipTapEditorSelectedTextGlobal = () => { + // Get the entire node/paragraph that the user is in const pos = props.editor.state.selection.$from.pos; // get the cursor position const resolvedPos = props.editor.state.doc.resolve(pos); // resolve the position in the document const start = resolvedPos.before(1); // get the start position of the node @@ -221,6 +267,14 @@ const UserFeedbackModal = (props: AIEditorToolkitProps) => { return paragraph; } + const getTipTapEditorSelectedText = () => { + const selection = props.editor.state.selection; + const from = selection.from; + const to = selection.to; + const text = props.editor.state.doc.textBetween(from, to); + return text; + } + return ( { animate={{ y: 0, opacity: 1, filter: 'blur(0px)' }} exit={{ y: 50, opacity: 0, filter: 'blur(3px)' }} transition={{ type: "spring", bounce: 0.35, duration: 1.7, mass: 0.2, velocity: 2 }} - className='fixed top-0 left-0 w-full h-full z-50 flex justify-center items-center ' + className='backdrop-blur-md fixed top-0 left-0 w-full h-full z-50 flex justify-center items-center ' style={{ pointerEvents: 'none' }} >
+ className="backdrop-blur-md z-50 rounded-2xl max-w-screen-2xl my-10 mx-auto w-[500px] h-[200px] fixed bottom-16 left-1/2 transform -translate-x-1/2 shadow-xl ring-1 ring-inset ring-white/10 text-white p-3 flex-col-reverse">
@@ -271,6 +325,21 @@ const AiEditorToolButton = (props: any) => { await dispatchAIEditor({ type: 'setIsUserInputEnabled', payload: false }); await dispatchAIEditor({ type: 'setIsFeedbackModalOpen' }); } + if (label === 'MakeLonger') { + await dispatchAIEditor({ type: 'setSelectedTool', payload: label }); + await dispatchAIEditor({ type: 'setIsUserInputEnabled', payload: false }); + await dispatchAIEditor({ type: 'setIsFeedbackModalOpen' }); + } + if (label === 'GenerateQuiz') { + await dispatchAIEditor({ type: 'setSelectedTool', payload: label }); + await dispatchAIEditor({ type: 'setIsUserInputEnabled', payload: false }); + await dispatchAIEditor({ type: 'setIsFeedbackModalOpen' }); + } + if (label === 'Translate') { + await dispatchAIEditor({ type: 'setSelectedTool', payload: label }); + await dispatchAIEditor({ type: 'setIsUserInputEnabled', payload: false }); + await dispatchAIEditor({ type: 'setIsFeedbackModalOpen' }); + } } return ( @@ -288,18 +357,60 @@ const AiEditorToolButton = (props: any) => { const AiEditorActionScreen = ({ handleOperation }: { handleOperation: any }) => { const dispatchAIEditor = useAIEditorDispatch() as any; const aiEditorState = useAIEditor() as AIEditorStateTypes; + + const handleChange = async (event: React.ChangeEvent) => { + await dispatchAIEditor({ type: 'setChatInputValue', payload: event.currentTarget.value }); + + } + return (
- {aiEditorState.selectedTool === 'Writer' && -
+ {aiEditorState.selectedTool === 'Writer' && !aiEditorState.isWaitingForResponse && +
Write about...
} - {aiEditorState.selectedTool === 'ContinueWriting' && -
{ - handleOperation(aiEditorState.selectedTool, aiEditorState.chatInputValue) - }} className='flex cursor-pointer space-x-1.5 p-4 items-center bg-white/10 rounded-md outline outline-1 outline-neutral-200/20 text-2xl font-semibold text-white/70 hover:bg-white/20 hover:outline-neutral-200/40 delay-75 ease-linear transition-all'> - + {aiEditorState.selectedTool === 'ContinueWriting' && !aiEditorState.isWaitingForResponse && +
+

Place your cursor at the end of a sentence to continue writing

+
{ + handleOperation(aiEditorState.selectedTool, aiEditorState.chatInputValue) + }} className='flex cursor-pointer space-x-1.5 p-4 mt-4 items-center bg-white/10 rounded-md outline outline-1 outline-neutral-200/20 text-2xl font-semibold text-white/70 hover:bg-white/20 hover:outline-neutral-200/40 delay-75 ease-linear transition-all'> + + +
} + {aiEditorState.selectedTool === 'MakeLonger' && !aiEditorState.isWaitingForResponse && +
+

Select text to make longer

+
{ + handleOperation(aiEditorState.selectedTool, aiEditorState.chatInputValue) + }} className='flex cursor-pointer space-x-1.5 p-4 mt-4 items-center bg-white/10 rounded-md outline outline-1 outline-neutral-200/20 text-2xl font-semibold text-white/70 hover:bg-white/20 hover:outline-neutral-200/40 delay-75 ease-linear transition-all'> + + +
+
} + {aiEditorState.selectedTool === 'Translate' && !aiEditorState.isWaitingForResponse && +
+
+

Translate selected text to

+ +
+
{ + handleOperation(aiEditorState.selectedTool, aiEditorState.chatInputValue) + }} className='flex cursor-pointer space-x-1.5 p-4 mt-4 items-center bg-white/10 rounded-md outline outline-1 outline-neutral-200/20 text-2xl font-semibold text-white/70 hover:bg-white/20 hover:outline-neutral-200/40 delay-75 ease-linear transition-all'> + + +
+
} + {aiEditorState.isWaitingForResponse &&
+ + + + + +

Thinking...

+
} +
) }