From a05b298c91d36a46f3e944488afc7d779c662c5a Mon Sep 17 00:00:00 2001 From: swve Date: Thu, 4 Jan 2024 23:12:27 +0100 Subject: [PATCH] feat: init in-course AI features --- .../activity/[activityid]/activity.tsx | 83 +++--- apps/web/components/AI/AIActivityAsk.tsx | 238 +++++++++++------- .../Contexts/AI/AIChatBotContext.tsx | 69 +++++ .../Contexts/Editor/EditorContext.tsx | 31 +++ .../Activities/DynamicCanva/DynamicCanva.tsx | 54 +++- .../DynamicCanva/Elements/AICanvaToolkit.tsx | 109 ++++++++ apps/web/components/Objects/Editor/Editor.tsx | 2 +- .../Extensions/NoTextInput/NoTextInput.tsx | 21 ++ apps/web/package.json | 2 +- pnpm-lock.yaml | 8 +- 10 files changed, 471 insertions(+), 146 deletions(-) create mode 100644 apps/web/components/Contexts/AI/AIChatBotContext.tsx create mode 100644 apps/web/components/Contexts/Editor/EditorContext.tsx create mode 100644 apps/web/components/Objects/Activities/DynamicCanva/Elements/AICanvaToolkit.tsx create mode 100644 apps/web/components/Objects/Editor/Extensions/NoTextInput/NoTextInput.tsx diff --git a/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/activity/[activityid]/activity.tsx b/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/activity/[activityid]/activity.tsx index b490df4d..8119781b 100644 --- a/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/activity/[activityid]/activity.tsx +++ b/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/activity/[activityid]/activity.tsx @@ -14,6 +14,7 @@ import { getCourseThumbnailMediaDirectory } from "@services/media/media"; import { useOrg } from "@components/Contexts/OrgContext"; import { CourseProvider } from "@components/Contexts/CourseContext"; import AIActivityAsk from "@components/AI/AIActivityAsk"; +import AIChatBotProvider from "@components/Contexts/AI/AIChatBotContext"; interface ActivityClientProps { activityid: string; @@ -50,48 +51,50 @@ function ActivityClient(props: ActivityClientProps) { return ( <> - -
-
-
- - - -
-
-

Course

-

{course.name}

-
-
- - -
-
-

Chapter : {getChapterNameByActivityId(course, activity.id)}

-

{activity.name}

-
-
- - - - - -
-
- - {activity ? ( -
-
- {activity.activity_type == "TYPE_DYNAMIC" && } - {/* todo : use apis & streams instead of this */} - {activity.activity_type == "TYPE_VIDEO" && } - {activity.activity_type == "TYPE_DOCUMENT" && } + + +
+
+
+ + + +
+
+

Course

+

{course.name}

- ) : (
)} - {
} -
-
+ + +
+
+

Chapter : {getChapterNameByActivityId(course, activity.id)}

+

{activity.name}

+
+
+ + + + + +
+
+ + {activity ? ( +
+
+ {activity.activity_type == "TYPE_DYNAMIC" && } + {/* todo : use apis & streams instead of this */} + {activity.activity_type == "TYPE_VIDEO" && } + {activity.activity_type == "TYPE_DOCUMENT" && } +
+
+ ) : (
)} + {
} +
+ + ); diff --git a/apps/web/components/AI/AIActivityAsk.tsx b/apps/web/components/AI/AIActivityAsk.tsx index 1411dd06..6ae62c49 100644 --- a/apps/web/components/AI/AIActivityAsk.tsx +++ b/apps/web/components/AI/AIActivityAsk.tsx @@ -1,13 +1,17 @@ import { useSession } from '@components/Contexts/SessionContext' import { sendActivityAIChatMessage, startActivityAIChatSession } from '@services/ai/ai'; +import { BadgeInfo, NotebookTabs } from 'lucide-react'; import Avvvatars from 'avvvatars-react'; import { motion, AnimatePresence } from 'framer-motion'; -import { FlaskConical, Keyboard, MessageCircle, Sparkle, Sparkles, X } from 'lucide-react' +import { FlaskConical, Keyboard, MessageCircle, MessageSquareIcon, Sparkle, Sparkles, X } from 'lucide-react' import Image from 'next/image'; import { send } from 'process'; import learnhouseAI_icon from "public/learnhouse_ai_simple.png"; import learnhouseAI_logo_black from "public/learnhouse_ai_black_logo.png"; import React, { useEffect, useRef } from 'react' +import { AIChatBotStateTypes, useAIChatBot, useAIChatBotDispatch } from '@components/Contexts/AI/AIChatBotContext'; +import FeedbackModal from '@components/Objects/Modals/Feedback/Feedback'; +import Modal from '@components/StyledElements/Modal/Modal'; type AIActivityAskProps = { @@ -16,15 +20,14 @@ type AIActivityAskProps = { function AIActivityAsk(props: AIActivityAskProps) { - const [isAIModalOpen, setIsAIModalOpen] = React.useState(false); - + const dispatchAIChatBot = useAIChatBotDispatch() as any; return (
- +
setIsAIModalOpen(true)} + onClick={() => dispatchAIChatBot({ type: 'setIsModalOpen' })} style={{ background: 'conic-gradient(from 32deg at 53.75% 50%, rgb(35, 40, 93) 4deg, rgba(20, 0, 52, 0.95) 59deg, rgba(164, 45, 238, 0.88) 281deg)', }} @@ -39,7 +42,7 @@ function AIActivityAsk(props: AIActivityAskProps) { ) } -type Message = { +export type AIMessage = { sender: string; message: any; type: 'ai' | 'user'; @@ -47,29 +50,25 @@ type Message = { type ActivityChatMessageBoxProps = { activity: any; - isAIModalOpen?: boolean; - setIsAIModalOpen?: any; } function ActivityChatMessageBox(props: ActivityChatMessageBoxProps) { const session = useSession() as any; - const [messages, setMessages] = React.useState([]) as [Message[], any]; - const [aichat_uuid, setAichat_uuid] = React.useState(''); - const [isWaitingForResponse, setIsWaitingForResponse] = React.useState(false); - const [chatInputValue, setChatInputValue] = React.useState('') as [string, any]; + const aiChatBotState = useAIChatBot() as AIChatBotStateTypes; + const dispatchAIChatBot = useAIChatBotDispatch() as any; // TODO : come up with a better way to handle this - const inputClass = isWaitingForResponse + const inputClass = aiChatBotState.isWaitingForResponse ? 'ring-1 ring-inset ring-white/10 bg-transparent w-full rounded-lg outline-none px-4 py-2 text-white text-sm placeholder:text-white/30 opacity-30 ' : 'ring-1 ring-inset ring-white/10 bg-transparent w-full rounded-lg outline-none px-4 py-2 text-white text-sm placeholder:text-white/30'; useEffect(() => { - if (props.isAIModalOpen) { + if (aiChatBotState.isModalOpen) { document.body.style.overflow = 'hidden'; } else { document.body.style.overflow = 'unset'; } - }, [props.isAIModalOpen]); + }, [aiChatBotState.isModalOpen]); function handleKeyDown(event: React.KeyboardEvent) { if (event.key === 'Enter') { @@ -78,31 +77,33 @@ function ActivityChatMessageBox(props: ActivityChatMessageBoxProps) { } } - const handleChange = (event: React.ChangeEvent) => { - setChatInputValue(event.target.value); + const handleChange = async (event: React.ChangeEvent) => { + await dispatchAIChatBot({ type: 'setChatInputValue', payload: event.currentTarget.value }); + } const sendMessage = async (message: string) => { - if (aichat_uuid) { - setMessages((messages: any) => [...messages, { sender: session.user.user_uuid, message: message, type: 'user' }]); - setIsWaitingForResponse(true); - const response = await sendActivityAIChatMessage(message, aichat_uuid, props.activity.activity_uuid) - setIsWaitingForResponse(false); - setChatInputValue(''); - setMessages((messages: any) => [...messages, { sender: 'ai', message: response.message, type: 'ai' }]); + if (aiChatBotState.aichat_uuid) { + await dispatchAIChatBot({ type: 'addMessage', payload: { sender: 'user', message: message, type: 'user' } }); + await dispatchAIChatBot({ type: 'setIsWaitingForResponse' }); + const response = await sendActivityAIChatMessage(message, aiChatBotState.aichat_uuid, props.activity.activity_uuid) + await dispatchAIChatBot({ type: 'setIsNoLongerWaitingForResponse' }); + await dispatchAIChatBot({ type: 'setChatInputValue', payload: '' }); + await dispatchAIChatBot({ type: 'addMessage', payload: { sender: 'ai', message: response.message, type: 'ai' } }); + } else { - setMessages((messages: any) => [...messages, { sender: session.user.user_uuid, message: message, type: 'user' }]); - setIsWaitingForResponse(true); + await dispatchAIChatBot({ type: 'addMessage', payload: { sender: 'user', message: message, type: 'user' } }); + await dispatchAIChatBot({ type: 'setIsWaitingForResponse' }); const response = await startActivityAIChatSession(message, props.activity.activity_uuid) - setAichat_uuid(response.aichat_uuid); - setIsWaitingForResponse(false); - setChatInputValue(''); - setMessages((messages: any) => [...messages, { sender: 'ai', message: response.message, type: 'ai' }]); + await dispatchAIChatBot({ type: 'setAichat_uuid', payload: response.aichat_uuid }); + await dispatchAIChatBot({ type: 'setIsNoLongerWaitingForResponse' }); + await dispatchAIChatBot({ type: 'setChatInputValue', payload: '' }); + await dispatchAIChatBot({ type: 'addMessage', payload: { sender: 'ai', message: response.message, type: 'ai' } }); } } function closeModal() { - props.setIsAIModalOpen(false); + dispatchAIChatBot({ type: 'setIsModalClose' }); } const messagesEndRef = useRef(null); @@ -112,74 +113,78 @@ function ActivityChatMessageBox(props: ActivityChatMessageBoxProps) { messagesEndRef.current.scrollIntoView({ behavior: 'smooth' }); } - }, [messages, session]); + }, [aiChatBotState.messages, session]); return ( - {props.isAIModalOpen && ( - -
-
-
- -
-
- - Learnhouse AI -
-
- - Experimental -
+ {aiChatBotState.isModalOpen && ( + <> + +
+
+
-
-
- {messages.length > 0 ? ( -
- {messages.map((message: Message, index: number) => { - return ( - - ) - })} -
-
- ) : ( - - )} -
-
- -
-
- + +
+
+ + AI +
+
+ + Experimental +
+ +
+
+ {aiChatBotState.messages.length > 0 ? ( +
+ {aiChatBotState.messages.map((message: AIMessage, index: number) => { + return ( + + ) + })} +
+
+ ) : ( + + )} +
+
+ +
+
+ + +
+
+ sendMessage(aiChatBotState.chatInputValue)} /> +
+
-
- sendMessage(chatInputValue)} /> -
-
-
- + + )} ) } type AIMessageProps = { - message: Message; + message: AIMessage; animated: boolean; } @@ -189,7 +194,7 @@ function AIMessage(props: AIMessageProps) { const words = props.message.message.split(' '); return ( -
+
@@ -216,20 +221,65 @@ function AIMessage(props: AIMessageProps) { const AIMessagePlaceHolder = (props: { activity_uuid: string, sendMessage: any }) => { const session = useSession() as any; + + const [feedbackModal, setFeedbackModal] = React.useState(false); return (
- -

Hello {session.user.username}, How can we help today ?

-
- props.sendMessage('Explain this Course in Simple examples.')} className='py-1.5 px-6 text-xs font-semibold text-white/30 rounded-full shadow-2xl bg-black/20 mb-3 outline outline-gray-400/5 cursor-pointer'>Explain in simple examples - props.sendMessage('Generate flashcards about this course and this lecture.')} className='py-1.5 px-6 text-xs font-semibold text-white/30 rounded-full shadow-2xl bg-black/20 mb-3 outline outline-gray-400/5 cursor-pointer'>Generate flashcards - props.sendMessage('Break down this Course in concepts.')} className='py-1.5 px-6 text-xs font-semibold text-white/30 rounded-full shadow-2xl bg-black/20 mb-3 outline outline-gray-400/5 cursor-pointer'>Break down in concepts -
+ + + +

+ Hello + + {session.user.username}, + + how can we help today ? +

+
+ + + + +
) } +const AIChatPredefinedQuestion = (props: { sendMessage: any, label: string }) => { + function getQuestion(label: string) { + if (label === 'about') { + return `What is this Activity about ?` + } else if (label === 'flashcards') { + return `Generate flashcards about this Activity` + } else if (label === 'examples') { + return `Explain this Activity in practical examples` + } + } + + return ( +
props.sendMessage(getQuestion(props.label))} className='flex space-x-1.5 items-center bg-white/5 cursor-pointer px-4 py-1.5 rounded-xl outline outline-1 outline-neutral-100/10 text-xs font-semibold text-white/40 hover:text-white/60 hover:bg-white/10 hover:outline-neutral-200/40 delay-75 ease-linear transition-all'> + {props.label === 'about' && } + {props.label === 'flashcards' && } + {props.label === 'examples' &&
Ex
} + {getQuestion(props.label)} +
+ ) +} + export default AIActivityAsk \ No newline at end of file diff --git a/apps/web/components/Contexts/AI/AIChatBotContext.tsx b/apps/web/components/Contexts/AI/AIChatBotContext.tsx new file mode 100644 index 00000000..e7bd9cc3 --- /dev/null +++ b/apps/web/components/Contexts/AI/AIChatBotContext.tsx @@ -0,0 +1,69 @@ +import { AIMessage } from '@components/AI/AIActivityAsk'; +import React, { createContext, useContext, useReducer } from 'react' + + + +export const AIChatBotContext = createContext(null) as any; +export const AIChatBotDispatchContext = createContext(null) as any; + +export type AIChatBotStateTypes = { + + messages: AIMessage[], + isModalOpen: boolean, + aichat_uuid: string, + isWaitingForResponse: boolean, + chatInputValue: string +} + +function AIChatBotProvider({ children }: { children: React.ReactNode }) { + const [aiChatBotState, dispatchAIChatBot] = useReducer(aiChatBotReducer, + { + messages: [] as AIMessage[], + isModalOpen: false, + aichat_uuid: null, + isWaitingForResponse: false, + chatInputValue: '' + } + ); + return ( + + + {children} + + + ) +} + +export default AIChatBotProvider + +export function useAIChatBot() { + return useContext(AIChatBotContext); +} + +export function useAIChatBotDispatch() { + return useContext(AIChatBotDispatchContext); +} + +function aiChatBotReducer(state: any, action: any) { + switch (action.type) { + case 'setMessages': + return { ...state, messages: action.payload }; + case 'addMessage': + return { ...state, messages: [...state.messages, action.payload] }; + case 'setIsModalOpen': + return { ...state, isModalOpen: true }; + case 'setIsModalClose': + return { ...state, isModalOpen: false }; + case 'setAichat_uuid': + return { ...state, aichat_uuid: action.payload }; + case 'setIsWaitingForResponse': + return { ...state, isWaitingForResponse: true }; + case 'setIsNoLongerWaitingForResponse': + return { ...state, isWaitingForResponse: false }; + case 'setChatInputValue': + return { ...state, chatInputValue: action.payload }; + + default: + throw new Error(`Unhandled action type: ${action.type}`) + } +} \ No newline at end of file diff --git a/apps/web/components/Contexts/Editor/EditorContext.tsx b/apps/web/components/Contexts/Editor/EditorContext.tsx new file mode 100644 index 00000000..f38f08ff --- /dev/null +++ b/apps/web/components/Contexts/Editor/EditorContext.tsx @@ -0,0 +1,31 @@ +import React, { useState } from 'react' + + +export const EditorProviderContext = React.createContext(null) as any; + +type EditorProviderProps = { + children: React.ReactNode + options: EditorProviderState +} + +type EditorProviderState = { + isEditable: boolean +} + +function EditorOptionsProvider({ children, options }: EditorProviderProps) { + const [editorOptions, setEditorOptions] = useState(options); + + return ( + + {children} + + ) +} + +export default EditorOptionsProvider + +export function useEditorProvider() { + return React.useContext(EditorProviderContext); +} + + diff --git a/apps/web/components/Objects/Activities/DynamicCanva/DynamicCanva.tsx b/apps/web/components/Objects/Activities/DynamicCanva/DynamicCanva.tsx index 8c354034..8d2546f3 100644 --- a/apps/web/components/Objects/Activities/DynamicCanva/DynamicCanva.tsx +++ b/apps/web/components/Objects/Activities/DynamicCanva/DynamicCanva.tsx @@ -1,4 +1,4 @@ -import { useEditor, EditorContent } from "@tiptap/react"; +import { useEditor, EditorContent, BubbleMenu, EditorProvider } from "@tiptap/react"; import StarterKit from "@tiptap/starter-kit"; import styled from "styled-components" import Youtube from "@tiptap/extension-youtube"; @@ -22,16 +22,24 @@ import ts from 'highlight.js/lib/languages/typescript' import html from 'highlight.js/lib/languages/xml' import python from 'highlight.js/lib/languages/python' import java from 'highlight.js/lib/languages/java' +import { NoTextInput } from "@components/Objects/Editor/Extensions/NoTextInput/NoTextInput"; +import EditorOptionsProvider from "@components/Contexts/Editor/EditorContext"; +import AICanvaToolkit from "./Elements/AICanvaToolkit"; interface Editor { content: string; activity: any; - //course: any; } function Canva(props: Editor) { - const isEditable = false; + /** + * Important Note : This is a workaround to enable user interaction features to be implemented easily, like text selection, AI features and other planned features, this is set to true but otherwise it should be set to false. + * Another workaround is implemented below to disable the editor from being edited by the user by setting the caret-color to transparent and using a custom extension to filter out transactions that add/edit/remove text. + * To let the various Custom Extensions know that the editor is not editable, React context (EditorOptionsProvider) will be used instead of props.extension.options.editable. + */ + const isEditable = true; + // Code Block Languages for Lowlight lowlight.register('html', html) @@ -46,6 +54,7 @@ function Canva(props: Editor) { editable: isEditable, extensions: [ StarterKit, + NoTextInput, // Custom Extensions InfoCallout.configure({ editable: isEditable, @@ -87,21 +96,54 @@ function Canva(props: Editor) { content: props.content, }); + return ( - - - + + + + + + + + ); } + + const CanvaWrapper = styled.div` width: 100%; margin: 0 auto; + .bubble-menu { + display: flex; + background-color: #0D0D0D; + padding: 0.2rem; + border-radius: 0.5rem; + + button { + border: none; + background: none; + color: #FFF; + font-size: 0.85rem; + font-weight: 500; + padding: 0 0.2rem; + opacity: 0.6; + + &:hover, + &.is-active { + opacity: 1; + } + } +} + // disable chrome outline .ProseMirror { + // Workaround to disable editor from being edited by the user. + caret-color: transparent; + h1 { font-size: 30px; font-weight: 600; diff --git a/apps/web/components/Objects/Activities/DynamicCanva/Elements/AICanvaToolkit.tsx b/apps/web/components/Objects/Activities/DynamicCanva/Elements/AICanvaToolkit.tsx new file mode 100644 index 00000000..5619bae9 --- /dev/null +++ b/apps/web/components/Objects/Activities/DynamicCanva/Elements/AICanvaToolkit.tsx @@ -0,0 +1,109 @@ +import React from 'react' +import { Editor } from '@tiptap/core'; +import learnhouseAI_icon from "public/learnhouse_ai_simple.png"; +import Image from 'next/image'; +import { BookOpen, FormInput, Languages, MoreVertical } from 'lucide-react'; +import { BubbleMenu } from '@tiptap/react'; +import ToolTip from '@components/StyledElements/Tooltip/Tooltip'; +import { AIChatBotStateTypes, useAIChatBot, useAIChatBotDispatch } from '@components/Contexts/AI/AIChatBotContext'; +import { sendActivityAIChatMessage, startActivityAIChatSession } from '@services/ai/ai'; + + + +type AICanvaToolkitProps = { + editor: Editor, + activity: any +} + +function AICanvaToolkit(props: AICanvaToolkitProps) { + return ( + +
+
AI
+
+ +
+
+ + + + +
+
+
+ ) +} + +function AIActionButton(props: { editor: Editor, label: string, activity: any }) { + const dispatchAIChatBot = useAIChatBotDispatch() as any; + const aiChatBotState = useAIChatBot() as AIChatBotStateTypes; + const [aichat_uuid, setAichat_uuid] = React.useState(''); + + async function handleAction(label: string) { + const selection = getTipTapEditorSelectedText(); + const prompt = getPrompt(label, selection); + dispatchAIChatBot({ type: 'setIsModalOpen' }); + await sendMessage(prompt); + + + } + + 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; + } + + const getPrompt = (label: string, selection: string) => { + if (label === 'Explain') { + return `Explain this part of the course "${selection}" keep this course context in mind.` + } else if (label === 'Summarize') { + return `Summarize this "${selection}" with the course context in mind.` + } else if (label === 'Translate') { + return `Translate "${selection}" to another language.` + } else { + return `Give examples to understand "${selection}" better, if possible give context in the course.` + } + } + + const sendMessage = async (message: string) => { + if (aiChatBotState.aichat_uuid) { + await dispatchAIChatBot({ type: 'addMessage', payload: { sender: 'user', message: message, type: 'user' } }); + await dispatchAIChatBot({ type: 'setIsWaitingForResponse' }); + const response = await sendActivityAIChatMessage(message, aiChatBotState.aichat_uuid, props.activity.activity_uuid) + await dispatchAIChatBot({ type: 'setIsNoLongerWaitingForResponse' }); + await dispatchAIChatBot({ type: 'setChatInputValue', payload: '' }); + await dispatchAIChatBot({ type: 'addMessage', payload: { sender: 'ai', message: response.message, type: 'ai' } }); + + } else { + await dispatchAIChatBot({ type: 'addMessage', payload: { sender: 'user', message: message, type: 'user' } }); + await dispatchAIChatBot({ type: 'setIsWaitingForResponse' }); + const response = await startActivityAIChatSession(message, props.activity.activity_uuid) + await dispatchAIChatBot({ type: 'setAichat_uuid', payload: response.aichat_uuid }); + await dispatchAIChatBot({ type: 'setIsNoLongerWaitingForResponse' }); + await dispatchAIChatBot({ type: 'setChatInputValue', payload: '' }); + await dispatchAIChatBot({ type: 'addMessage', payload: { sender: 'ai', message: response.message, type: 'ai' } }); + } + } + + const tooltipLabel = props.label === 'Explain' ? 'Explain a word or a sentence with AI' : props.label === 'Summarize' ? 'Summarize a long paragraph or text with AI' : props.label === 'Translate' ? 'Translate to different languages with AI' : 'Give examples to understand better with AI' + return ( +
+ + + +
+ ) +} + +export default AICanvaToolkit \ No newline at end of file diff --git a/apps/web/components/Objects/Editor/Editor.tsx b/apps/web/components/Objects/Editor/Editor.tsx index 5d9028dd..93cac96b 100644 --- a/apps/web/components/Objects/Editor/Editor.tsx +++ b/apps/web/components/Objects/Editor/Editor.tsx @@ -1,6 +1,6 @@ 'use client'; import React from "react"; -import { useEditor, EditorContent } from "@tiptap/react"; +import { useEditor, EditorContent, BubbleMenu } from "@tiptap/react"; import StarterKit from "@tiptap/starter-kit"; import learnhouseIcon from "public/learnhouse_icon.png"; import { ToolbarButtons } from "./Toolbar/ToolbarButtons"; diff --git a/apps/web/components/Objects/Editor/Extensions/NoTextInput/NoTextInput.tsx b/apps/web/components/Objects/Editor/Extensions/NoTextInput/NoTextInput.tsx new file mode 100644 index 00000000..a9e5116b --- /dev/null +++ b/apps/web/components/Objects/Editor/Extensions/NoTextInput/NoTextInput.tsx @@ -0,0 +1,21 @@ +import { Extension } from '@tiptap/core'; +import { Plugin, PluginKey } from 'prosemirror-state'; + +export const NoTextInput = Extension.create({ + name: 'noTextInput', + + addProseMirrorPlugins() { + return [ + new Plugin({ + key: new PluginKey('noTextInput'), + filterTransaction: (transaction) => { + // If the transaction is adding text, stop it + return !transaction.docChanged || transaction.steps.every((step) => { + const { slice } = step.toJSON(); + return !slice || !slice.content.some((node: { type: string; }) => node.type === 'text'); + }); + }, + }), + ]; + }, + }); \ No newline at end of file diff --git a/apps/web/package.json b/apps/web/package.json index ac6a098e..54d55a89 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -29,7 +29,7 @@ "formik": "^2.2.9", "framer-motion": "^10.16.1", "lowlight": "^3.0.0", - "lucide-react": "^0.268.0", + "lucide-react": "^0.307.0", "next": "14.0.4", "re-resizable": "^6.9.9", "react": "^18.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 186be655..7c9c1e5b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -81,8 +81,8 @@ importers: specifier: ^3.0.0 version: 3.1.0 lucide-react: - specifier: ^0.268.0 - version: 0.268.0(react@18.2.0) + specifier: ^0.307.0 + version: 0.307.0(react@18.2.0) next: specifier: 14.0.4 version: 14.0.4(@babel/core@7.23.2)(react-dom@18.2.0)(react@18.2.0) @@ -4824,8 +4824,8 @@ packages: yallist: 4.0.0 dev: true - /lucide-react@0.268.0(react@18.2.0): - resolution: {integrity: sha512-XP/xY3ASJAViqNqVnDRcEfdxfRB7uNST8sqTLwZhL983ikmHMQ7qQak7ZxrnXOVhB3QDBawdr3ANq0P+iWHP/g==} + /lucide-react@0.307.0(react@18.2.0): + resolution: {integrity: sha512-+vZ+vUiWPZTMnLHURg4aoIaz6NHOWXVVcVd8iLROu1k4LbyjcnHIKmbjXHCmulz7XAYLWRVXzhJJgIr+Aq3vOg==} peerDependencies: react: ^16.5.1 || ^17.0.0 || ^18.0.0 dependencies: