import React from 'react' import learnhouseAI_icon from "public/learnhouse_ai_simple.png"; import { motion, AnimatePresence } from 'framer-motion'; import Image from 'next/image'; import { AlertTriangle, BetweenHorizontalStart, FastForward, Feather, FileStack, HelpCircle, Languages, MessageCircle, MoreVertical, Pen, X } from 'lucide-react'; import { Editor } from '@tiptap/react'; import { AIChatBotStateTypes, useAIChatBot, useAIChatBotDispatch } from '@components/Contexts/AI/AIChatBotContext'; import { AIEditorStateTypes, useAIEditor, useAIEditorDispatch } from '@components/Contexts/AI/AIEditorContext'; import { sendActivityAIChatMessage, startActivityAIChatSession } from '@services/ai/ai'; import useGetAIFeatures from '@components/AI/Hooks/useGetAIFeatures'; type AIEditorToolkitProps = { editor: Editor, activity: any } type AIPromptsLabels = { label: 'Writer' | 'ContinueWriting' | 'MakeLonger' | 'GenerateQuiz' | 'Translate', selection: string } function AIEditorToolkit(props: AIEditorToolkitProps) { const dispatchAIEditor = useAIEditorDispatch() as any; const aiEditorState = useAIEditor() as AIEditorStateTypes; const is_ai_feature_enabled = useGetAIFeatures({ feature: 'editor' }); const [isToolkitAvailable, setIsToolkitAvailable] = React.useState(true); React.useEffect(() => { if (is_ai_feature_enabled) { setIsToolkitAvailable(true); } }, [is_ai_feature_enabled]) return ( <> {isToolkitAvailable &&
{aiEditorState.isModalOpen && <> {aiEditorState.isFeedbackModalOpen && }
AI Editor
Promise.all([dispatchAIEditor({ type: 'setIsModalClose' }), dispatchAIEditor({ type: 'setIsFeedbackModalClose' })])} size={20} className='text-white/50 hover:cursor-pointer bg-white/10 p-1 rounded-full items-center' />
}
} ) } const UserFeedbackModal = (props: AIEditorToolkitProps) => { const dispatchAIEditor = useAIEditorDispatch() as any; const aiEditorState = useAIEditor() as AIEditorStateTypes; const handleChange = async (event: React.ChangeEvent) => { await dispatchAIEditor({ type: 'setChatInputValue', payload: event.currentTarget.value }); } const sendReqWithMessage = async (message: string) => { if (aiEditorState.aichat_uuid) { await dispatchAIEditor({ type: 'addMessage', payload: { sender: 'user', message: message, type: 'user' } }); await dispatchAIEditor({ type: 'setIsWaitingForResponse' }); const response = await sendActivityAIChatMessage(message, aiEditorState.aichat_uuid, props.activity.activity_uuid) if (response.success === false) { await dispatchAIEditor({ type: 'setIsNoLongerWaitingForResponse' }); await dispatchAIEditor({ type: 'setIsModalClose' }); // wait for 200ms before opening the modal again await new Promise(resolve => setTimeout(resolve, 200)); await dispatchAIEditor({ type: 'setError', payload: { isError: true, status: response.status, error_message: response.data.detail } }); await dispatchAIEditor({ type: 'setIsModalOpen' }); return ''; } await dispatchAIEditor({ type: 'setIsNoLongerWaitingForResponse' }); await dispatchAIEditor({ type: 'setChatInputValue', payload: '' }); await dispatchAIEditor({ type: 'addMessage', payload: { sender: 'ai', message: response.data.message, type: 'ai' } }); return response.data.message; } else { await dispatchAIEditor({ type: 'addMessage', payload: { sender: 'user', message: message, type: 'user' } }); await dispatchAIEditor({ type: 'setIsWaitingForResponse' }); const response = await startActivityAIChatSession(message, props.activity.activity_uuid) if (response.success === false) { await dispatchAIEditor({ type: 'setIsNoLongerWaitingForResponse' }); await dispatchAIEditor({ type: 'setIsModalClose' }); // wait for 200ms before opening the modal again await new Promise(resolve => setTimeout(resolve, 200)); await dispatchAIEditor({ type: 'setError', payload: { isError: true, status: response.status, error_message: response.data.detail } }); await dispatchAIEditor({ type: 'setIsModalOpen' }); return ''; } await dispatchAIEditor({ type: 'setAichat_uuid', payload: response.data.aichat_uuid }); await dispatchAIEditor({ type: 'setIsNoLongerWaitingForResponse' }); await dispatchAIEditor({ type: 'setChatInputValue', payload: '' }); await dispatchAIEditor({ type: 'addMessage', payload: { sender: 'ai', message: response.data.message, type: 'ai' } }); return response.data.message; } } const handleKeyPress = async (event: React.KeyboardEvent) => { if (event.key === 'Enter') { await handleOperation(aiEditorState.selectedTool, aiEditorState.chatInputValue); } } const handleOperation = async (label: 'Writer' | 'ContinueWriting' | 'MakeLonger' | 'GenerateQuiz' | 'Translate', message: string) => { // Set selected tool await dispatchAIEditor({ type: 'setSelectedTool', payload: label }); // Check what operation that was 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); await replaceSelectedTextWithText(ai_message); await dispatchAIEditor({ type: 'setIsNoLongerWaitingForResponse' }); } } else if (label === 'GenerateQuiz') { // will be implemented in future stages } else if (label === 'Translate') { 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, ''); return newText; } else { return originalText; } } async function fillEditorWithText(text: string) { const words = text.split(' '); 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)); } } 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; if (label === 'Writer') { return `Write 3 sentences about ${selection}`; } else if (label === 'ContinueWriting') { return `Continue writing 3 more sentences based on "${selection}"`; } else if (label === 'MakeLonger') { 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 { answer_id: string; answer: string; correct: boolean; } interface Question { question_id: string; question: string; type: "multiple_choice" answers: Answer[]; } " `; } else if (label === 'Translate') { return `Translate "${selection}" to the ` + aiEditorState.chatInputValue + ` language`; } } 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 const end = resolvedPos.after(1); // get the end position of the node const paragraph = props.editor.state.doc.textBetween(start, end, '\n', '\n'); // get the text of the node 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 (
{aiEditorState.isUserInputEnabled && !aiEditorState.error.isError &&
handleOperation(aiEditorState.selectedTool, aiEditorState.chatInputValue)} className='bg-white/10 px-3 rounded-md outline outline-1 outline-neutral-200/20 py-2 hover:bg-white/20 hover:outline-neutral-200/40 delay-75 ease-linear transition-all'>
}
) } const AiEditorToolButton = (props: any) => { const dispatchAIEditor = useAIEditorDispatch() as any; const aiEditorState = useAIEditor() as AIEditorStateTypes; const handleToolButtonClick = async (label: 'Writer' | 'ContinueWriting' | 'MakeLonger' | 'GenerateQuiz' | 'Translate') => { if (label === 'Writer') { await dispatchAIEditor({ type: 'setSelectedTool', payload: label }); await dispatchAIEditor({ type: 'setIsUserInputEnabled', payload: true }); await dispatchAIEditor({ type: 'setIsFeedbackModalOpen' }); } if (label === 'ContinueWriting') { await dispatchAIEditor({ type: 'setSelectedTool', payload: label }); 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 ( ) } 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.isWaitingForResponse && !aiEditorState.error.isError &&
Write about...
} {aiEditorState.selectedTool === 'ContinueWriting' && !aiEditorState.isWaitingForResponse && !aiEditorState.error.isError &&

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 && !aiEditorState.error.isError &&

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 && !aiEditorState.error.isError &&

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 && !aiEditorState.error.isError &&

Thinking...

} {aiEditorState.error.isError && (

Something wrong happened

{aiEditorState.error.error_message}
) }
) } export default AIEditorToolkit