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, MoreVertical, X, } from 'lucide-react' import { Editor } from '@tiptap/react' 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{' '} PRE-ALPHA
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