From 5015800a11d4a0950414a3832354fbaefe6b7ec8 Mon Sep 17 00:00:00 2001 From: Dinesh Singh Date: Sat, 15 Apr 2023 13:35:01 +0530 Subject: [PATCH 1/4] Allow selection editing, Perform AI actions on selection in extension --- apps/extension/src/Components/AIPreview.tsx | 41 +++++ .../src/Components/Content/index.tsx | 2 +- .../src/Components/InternalEvents.tsx | 31 +++- apps/extension/src/Editor/plugins/index.tsx | 4 +- apps/extension/src/Hooks/useSaveChanges.ts | 2 +- apps/extension/src/Styles/GlobalStyle.ts | 15 +- apps/extension/src/Utils/getSelectionHTML.ts | 3 +- apps/extension/src/Utils/requestHandler.ts | 19 +- apps/extension/src/app.tsx | 16 ++ apps/extension/src/background.ts | 22 ++- apps/webapp/src/Components/AIPop/index.tsx | 161 ----------------- .../CreateTodoModal/TaskEditor/index.tsx | 5 +- .../CreateTodoModal/TaskEditor/plugins.ts | 8 +- .../BalloonToolbar/EditorBalloonToolbar.tsx | 2 +- .../src/Components/Editor/Plateless.tsx | 4 +- apps/webapp/src/Components/Filters/Filter.tsx | 7 +- apps/webapp/src/Components/Todo/BoardTask.tsx | 4 +- .../Components/Views/ParentFilters/index.tsx | 20 +-- apps/webapp/src/Data/links.tsx | 2 +- apps/webapp/src/Editor/AIPreviewContainer.tsx | 15 ++ .../webapp/src/Editor/Hooks/useUpdateBlock.ts | 2 +- apps/webapp/src/Editor/MexEditor.tsx | 4 +- apps/webapp/src/Hooks/API/useNamespaceAPI.ts | 11 +- apps/webapp/src/Hooks/useAIOptions.ts | 39 ----- apps/webapp/src/Hooks/useCreateNewMenu.tsx | 65 ++----- apps/webapp/src/Hooks/useRouting.ts | 2 +- apps/webapp/src/Hooks/useSocket.ts | 2 +- apps/webapp/src/Hooks/useTaskFromSelection.ts | 1 + apps/webapp/src/Stores/useAuth.ts | 6 + apps/webapp/src/Utils/nav.ts | 4 +- apps/webapp/src/Views/LinkView.tsx | 4 +- apps/webapp/src/Workers/controller.ts | 14 +- apps/webapp/src/Workers/search.ts | 14 +- libs/core/src/Stores/highlight.store.ts | 6 +- libs/core/src/Stores/history.store.ts | 11 ++ libs/core/src/Stores/link.store.ts | 7 +- libs/core/src/Types/History.ts | 3 + libs/core/src/Utils/defaultShortcutsData.ts | 8 +- libs/core/src/Utils/index.ts | 1 + libs/core/src/Utils/parseData.ts | 16 ++ .../src/Components/AIPreview}/AIHistory.tsx | 2 + .../src/Components/AIPreview/AIResponse.tsx | 71 ++++++++ .../src/Components/AIPreview}/AIResults.tsx | 2 + .../src/Components/AIPreview}/Floater.tsx | 72 ++++---- .../shared/src/Components/AIPreview/index.tsx | 163 ++++++++++++++++++ .../src/Components/AIPreview}/styled.tsx | 14 +- libs/shared/src/Components/AIPreview/types.ts | 10 ++ .../FloatingElements/Autocomplete.style.tsx | 4 + .../FloatingElements/Autocomplete.tsx | 17 +- .../FloatingElements/Dropdown.style.tsx | 13 +- .../Components/FloatingElements/Dropdown.tsx | 9 +- libs/shared/src/Components/InsertMenu.tsx | 83 +++++++++ libs/shared/src/Hooks/useAIOptions.ts | 130 ++++++++++++++ libs/shared/src/Style/Buttons.tsx | 8 +- .../shared}/src/Utils/deserialize.ts | 36 ++-- .../Editor => libs/shared/src/Utils}/utils.ts | 2 +- libs/shared/src/index.ts | 4 + 57 files changed, 866 insertions(+), 377 deletions(-) create mode 100644 apps/extension/src/Components/AIPreview.tsx delete mode 100644 apps/webapp/src/Components/AIPop/index.tsx create mode 100644 apps/webapp/src/Editor/AIPreviewContainer.tsx delete mode 100644 apps/webapp/src/Hooks/useAIOptions.ts rename {apps/webapp/src/Components/AIPop => libs/shared/src/Components/AIPreview}/AIHistory.tsx (96%) create mode 100644 libs/shared/src/Components/AIPreview/AIResponse.tsx rename {apps/webapp/src/Components/AIPop => libs/shared/src/Components/AIPreview}/AIResults.tsx (92%) rename {apps/webapp/src/Components/AIPop => libs/shared/src/Components/AIPreview}/Floater.tsx (54%) create mode 100644 libs/shared/src/Components/AIPreview/index.tsx rename {apps/webapp/src/Components/AIPop => libs/shared/src/Components/AIPreview}/styled.tsx (88%) create mode 100644 libs/shared/src/Components/AIPreview/types.ts create mode 100644 libs/shared/src/Components/InsertMenu.tsx create mode 100644 libs/shared/src/Hooks/useAIOptions.ts rename {apps/extension => libs/shared}/src/Utils/deserialize.ts (100%) rename {apps/webapp/src/Editor => libs/shared/src/Utils}/utils.ts (98%) diff --git a/apps/extension/src/Components/AIPreview.tsx b/apps/extension/src/Components/AIPreview.tsx new file mode 100644 index 000000000..725416acd --- /dev/null +++ b/apps/extension/src/Components/AIPreview.tsx @@ -0,0 +1,41 @@ +import { useMemo } from 'react' + +import { NodeEditorContent } from '@mexit/core' +import { AIPreview, useAIOptions } from '@mexit/shared' + +import { generateEditorPluginsWithComponents } from '../Editor/plugins' +import { useSaveChanges } from '../Hooks/useSaveChanges' +import { getElementById } from '../Utils/cs-utils' + +import components from './Editor/EditorPreviewComponents' + +const AIPreviewContainer = () => { + const { appendAndSave } = useSaveChanges() + const { getAIMenuItems } = useAIOptions() + + const handleOnInsert = (content: NodeEditorContent, nodeId: string) => { + appendAndSave({ nodeid: nodeId, content }) + } + + const plugins = useMemo( + () => + generateEditorPluginsWithComponents(components, { + exclude: { + dnd: true + } + }), + [] + ) + + return ( + + ) +} + +export default AIPreviewContainer diff --git a/apps/extension/src/Components/Content/index.tsx b/apps/extension/src/Components/Content/index.tsx index 6010386e2..98d518d55 100644 --- a/apps/extension/src/Components/Content/index.tsx +++ b/apps/extension/src/Components/Content/index.tsx @@ -3,6 +3,7 @@ import React, { useEffect, useState } from 'react' import { createPlateEditor, createPlateUI } from '@udecode/plate' import { ELEMENT_TAG, getDefaultContent, NodeEditorContent, QuickLinkType, useContentStore } from '@mexit/core' +import { getDeserializeSelectionToNodes } from '@mexit/shared' import { CopyTag } from '../../Editor/components/Tags/CopyTag' import { generateEditorPluginsWithComponents } from '../../Editor/plugins/index' @@ -10,7 +11,6 @@ import { useEditorStore } from '../../Hooks/useEditorStore' import { useSnippets } from '../../Hooks/useSnippets' import { useSputlitContext } from '../../Hooks/useSputlitContext' import { useSputlitStore } from '../../Stores/useSputlitStore' -import { getDeserializeSelectionToNodes } from '../../Utils/deserialize' import Results from '../Results' import { StyledContent } from './styled' diff --git a/apps/extension/src/Components/InternalEvents.tsx b/apps/extension/src/Components/InternalEvents.tsx index a5ade33e9..7bd1f03fc 100644 --- a/apps/extension/src/Components/InternalEvents.tsx +++ b/apps/extension/src/Components/InternalEvents.tsx @@ -6,7 +6,14 @@ import * as Sentry from '@sentry/react' import mixpanel from 'mixpanel-browser' import Highlighter from 'web-highlighter' -import { API_BASE_URLS, mog, useHighlightStore } from '@mexit/core' +import { + API_BASE_URLS, + FloatingElementType, + mog, + useFloatingStore, + useHighlightStore, + useHistoryStore +} from '@mexit/core' import { getScrollbarWidth, isInputField } from '@mexit/shared' import { useEditorStore } from '../Hooks/useEditorStore' @@ -50,6 +57,8 @@ function useToggleHandler() { const { previewMode, setPreviewMode } = useEditorStore() const setTooltipState = useSputlitStore((s) => s.setHighlightTooltipState) const resetSputlitState = useSputlitStore((s) => s.reset) + const addAIEvent = useHistoryStore((store) => store.addInitialEvent) + const setFloatingElement = useFloatingStore((s) => s.setFloatingElement) const timeoutRef = useRef() const runAnimateTimer = useCallback((vs: VisualState.animatingIn | VisualState.animatingOut) => { @@ -71,6 +80,22 @@ function useToggleHandler() { }, ms) }, []) + const handleOpenAIPreview = (highlighter) => { + const { html } = getSelectionHTML() + const range = window.getSelection()?.getRangeAt(0) + const content = sanitizeHTML(html) + + if (content) { + addAIEvent({ role: 'assistant', content, inputFormat: 'html' }) + + highlighter.fromRange(range) + + setFloatingElement(FloatingElementType.AI_POPOVER, { + range + }) + } + } + useEffect(() => { function messageHandler(request: any, sender: chrome.runtime.MessageSender, sendResponse: (response: any) => void) { const highlighter = new Highlighter({ style: { className: 'mexit-highlight' } }) @@ -94,6 +119,10 @@ function useToggleHandler() { setVisualState(VisualState.animatingOut) } sendResponse(true) + break + case 'open-ai-tools': + handleOpenAIPreview(highlighter) + sendResponse(true) } } diff --git a/apps/extension/src/Editor/plugins/index.tsx b/apps/extension/src/Editor/plugins/index.tsx index 2148a283f..67879a9ae 100644 --- a/apps/extension/src/Editor/plugins/index.tsx +++ b/apps/extension/src/Editor/plugins/index.tsx @@ -98,7 +98,7 @@ export const generatePlugins = (options: PluginOptionType) => { // Special Elements createImagePlugin({ options: { - uploadImage: options.uploadImage + uploadImage: options?.uploadImage } }), createLinkPlugin(), // Link @@ -192,7 +192,7 @@ export const useMemoizedPlugins = (components: Record, options?: Pl const plugins = createPlugins( generatePlugins({ ...options, - uploadImage: options.uploadImage ?? uploadImageToWDCDN + uploadImage: options?.uploadImage ?? uploadImageToWDCDN }), { components: wrappedComponents diff --git a/apps/extension/src/Hooks/useSaveChanges.ts b/apps/extension/src/Hooks/useSaveChanges.ts index 8699ffb26..2475635ea 100644 --- a/apps/extension/src/Hooks/useSaveChanges.ts +++ b/apps/extension/src/Hooks/useSaveChanges.ts @@ -37,7 +37,7 @@ export interface AppendAndSaveProps { export function useSaveChanges() { const workspaceDetails = useAuthStore((store) => store.workspaceDetails) - const { setPreviewMode, setNodeContent } = useEditorStore() + const { setPreviewMode } = useEditorStore() const { getParentILink, getEntirePathILinks, updateMultipleILinks, updateSingleILink, createNoteHierarchyString } = useInternalLinks() const { updateDocument, updateBlocks } = useSearch() diff --git a/apps/extension/src/Styles/GlobalStyle.ts b/apps/extension/src/Styles/GlobalStyle.ts index 2a6131ccd..e7033a56f 100644 --- a/apps/extension/src/Styles/GlobalStyle.ts +++ b/apps/extension/src/Styles/GlobalStyle.ts @@ -1,6 +1,6 @@ import { createGlobalStyle } from 'styled-components' -import { customStyles, EditorBalloonStyles, normalize, ThinScrollbar,TippyBalloonStyles } from '@mexit/shared' +import { customStyles, EditorBalloonStyles, normalize, ThinScrollbar, TippyBalloonStyles } from '@mexit/shared' export const GlobalStyle = createGlobalStyle` @@ -36,7 +36,14 @@ export const GlobalStyle = createGlobalStyle` display: none; } - #sputlit-container, #dibba-container, #mexit-tooltip, #ext-side-nav, #notif { + .highlight { + color: ${({ theme }) => theme.tokens.text.heading}; + background: ${({ theme }) => `rgba(${theme.rgbTokens.colors.primary.default}, 0.4)`}; + } + + + + #sputlit-container, #dibba-container, #ai-preview, #mexit-tooltip, #ext-side-nav, #notif, #mexit-ai-performer { ${normalize}; // NormalizeCSS normalization letter-spacing: normal; font-family: "Inter", sans-serif; @@ -66,4 +73,8 @@ export const GlobalStyle = createGlobalStyle` ${({ theme }) => theme.custom && customStyles[theme.custom]} } + #mexit-ai-performer { + font-size: 14px; + } + ` diff --git a/apps/extension/src/Utils/getSelectionHTML.ts b/apps/extension/src/Utils/getSelectionHTML.ts index 32e685b87..560332ce8 100644 --- a/apps/extension/src/Utils/getSelectionHTML.ts +++ b/apps/extension/src/Utils/getSelectionHTML.ts @@ -8,12 +8,13 @@ export function getSelectionHTML() { url = selection?.anchorNode.baseURI const container = document.createElement('div') + for (let i = 0, len = selection.rangeCount; i < len; ++i) { const t = selection.getRangeAt(i).cloneContents() container.appendChild(t) range = selection.getRangeAt(i) } - console.log('Container: ', container) + html = container.innerHTML } } diff --git a/apps/extension/src/Utils/requestHandler.ts b/apps/extension/src/Utils/requestHandler.ts index 7b4609982..b9749061e 100644 --- a/apps/extension/src/Utils/requestHandler.ts +++ b/apps/extension/src/Utils/requestHandler.ts @@ -1,4 +1,4 @@ -import { apiURLs, DEFAULT_NAMESPACE, defaultContent, ListItemType, mog } from '@mexit/core' +import { AIEvent, apiURLs, DEFAULT_NAMESPACE, defaultContent, ListItemType, mog } from '@mexit/core' import { Tab } from '../Types/Tabs' @@ -71,6 +71,23 @@ export const handleCaptureRequest = ({ subType, data }) => { } } +export const handlePerformAIRequest = async ({ data, workspaceId }) => { + return await client + .post(apiURLs.openAi.perform, { + json: data, + headers: { + 'mex-workspace-id': workspaceId + } + }) + .json() + .then((event: AIEvent) => { + return { message: event, error: null } + }) + .catch((error) => { + return { message: null, error: error } + }) +} + export const handleSnippetRequest = ({ data }) => { const reqData = { id: data.id, diff --git a/apps/extension/src/app.tsx b/apps/extension/src/app.tsx index 5ce8f7809..66882efa1 100644 --- a/apps/extension/src/app.tsx +++ b/apps/extension/src/app.tsx @@ -1,5 +1,9 @@ +import React from 'react' +import { createPortal } from 'react-dom' + import { useAuthStore } from '@mexit/core' +import AIPreviewContainer from './Components/AIPreview' import Dibba from './Components/Dibba' import { DibbaPortal } from './Components/Dibba/DibbaPortal' import { InternalEvents } from './Components/InternalEvents' @@ -11,6 +15,15 @@ import Tooltip from './Components/Tooltip' import { TooltipPortal } from './Components/Tooltip/TooltipPortal' import { HighlighterProvider } from './Hooks/useHighlighterContext' import { SputlitProvider } from './Hooks/useSputlitContext' +import { styleSlot } from './Utils/cs-utils' + +interface Props { + children: React.ReactNode +} + +export function AIPreviewPortal(props: Props) { + return createPortal(props.children, styleSlot) +} const Extension = () => { const authenticated = useAuthStore((a) => a.authenticated) @@ -28,6 +41,9 @@ const Extension = () => { + + + )} diff --git a/apps/extension/src/background.ts b/apps/extension/src/background.ts index fd85ba80a..4edbeadda 100644 --- a/apps/extension/src/background.ts +++ b/apps/extension/src/background.ts @@ -11,6 +11,7 @@ import { handleCaptureRequest, handleHighlightRequest, handleNodeContentRequest, + handlePerformAIRequest, handleSharingRequest, handleShortenerRequest, handleSnippetRequest @@ -82,16 +83,22 @@ chrome.action.onClicked.addListener((command) => { }) chrome.contextMenus.create({ - id: 'open-sputlit', + id: 'sputlit', title: 'Open Sputlit', contexts: ['page', 'selection'] }) -chrome.contextMenus.onClicked.addListener((onClickData) => { +chrome.contextMenus.create({ + id: 'open-ai-tools', + title: 'Perform AI enrichment', + contexts: ['page', 'selection'] +}) + +chrome.contextMenus.onClicked.addListener((info) => { chrome.tabs?.query({ active: true, currentWindow: true }, (tabs) => { const tabId = tabs[0].id - chrome.tabs.sendMessage(tabId, { type: 'sputlit' }, (response) => { + chrome.tabs.sendMessage(tabId, { type: info?.menuItemId }, (response) => { handleResponseCallback(tabId, response) }) }) @@ -198,6 +205,13 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { return true } + case 'PERFORM_AI_ACTION': { + handlePerformAIRequest(request).then((res) => { + sendResponse(res?.message) + }) + return true + } + default: { return true } @@ -244,7 +258,7 @@ chrome.notifications.onClosed.addListener((notificationId, byUser) => { chrome.omnibox.onInputChanged.addListener((text, suggest) => { const workspaceDetails = useAuthStore.getState().workspaceDetails const linkCaptures = useLinkStore.getState().links?.filter((item) => item.alias) ?? [] - + console.log('captures', { linkCaptures }) const suggestions = fuzzySearch(linkCaptures, text, (item) => item.alias).map((item) => { return { content: `${API_BASE_URLS.url}/${workspaceDetails.id}/${item.alias}`, diff --git a/apps/webapp/src/Components/AIPop/index.tsx b/apps/webapp/src/Components/AIPop/index.tsx deleted file mode 100644 index 75b90a82d..000000000 --- a/apps/webapp/src/Components/AIPop/index.tsx +++ /dev/null @@ -1,161 +0,0 @@ -import React, { useEffect, useMemo } from 'react' - -import { - deserializeMd, - focusEditor, - getEndPoint, - getPlateEditorRef, - insertNodes, - usePlateEditorRef -} from '@udecode/plate' -import Highlighter from 'web-highlighter' - -import { IconButton } from '@workduck-io/mex-components' - -import { camelCase, generateTempId, SupportedAIEventTypes, useFloatingStore, useHistoryStore } from '@mexit/core' -import { AutoComplete, DefaultMIcons, Group } from '@mexit/shared' - -import { useAIOptions } from '../../Hooks/useAIOptions' -import { useCreateNewMenu } from '../../Hooks/useCreateNewMenu' -import Plateless from '../Editor/Plateless' - -import AIHistory from './AIHistory' -import { - AIContainerFooter, - AIContainerHeader, - AIContainerSection, - AIResponseContainer, - StyledAIContainer -} from './styled' - -const AIResponse = ({ aiResponse, index }) => { - const editor = usePlateEditorRef() - const selected = aiResponse?.at(index)?.at(0) - - if (selected) { - const deserialize = deserializeMd(editor, selected?.content) - - return ( - - - - ) - } - - return <> -} - -interface AIPreviewProps { - onInsert?: (content: string) => void -} - -const AIBlockPopover: React.FC = (props) => { - const aiEventsHistory = useHistoryStore((s) => s.ai) - const activeEventIndex = useHistoryStore((s) => s.activeEventIndex) - const setActiveEventIndex = useHistoryStore((s) => s.setActiveEventIndex) - const clearAIResponses = useHistoryStore((s) => s.clearAIResponses) - const setFloatingElement = useFloatingStore((s) => s.setFloatingElement) - - const { performAIAction } = useAIOptions() - const { getAIMenuItems } = useCreateNewMenu() - - const defaultItems = useMemo(() => { - return getAIMenuItems() - }, []) - - const insertContent = (content: string, replace = true) => { - if (!content) return - - const editor = getPlateEditorRef() - const deserialize = deserializeMd(editor, content)?.map((node) => ({ - ...node, - id: generateTempId() - })) - - if (Array.isArray(deserialize) && deserialize.length > 0) { - const at = replace ? editor.selection : getEndPoint(editor, editor.selection) - - insertNodes(editor, deserialize, { - at, - select: true - }) - - try { - focusEditor(editor) - } catch (err) { - console.error('Unable to focus editor', err) - } - - setFloatingElement(undefined) - } - } - - useEffect(() => { - return () => { - const state = useFloatingStore.getState().state?.AI_POPOVER - if (state?.range) { - const highlight = new Highlighter() - highlight.removeAll() - } - } - }, []) - - const handleOnEnter = async (value: string) => { - try { - await performAIAction(SupportedAIEventTypes.PROMPT, value) - } catch (err) { - console.error('Unable generate prompt result', err) - } - } - - const userQuery = aiEventsHistory?.at(activeEventIndex)?.at(-1) - const defaultValue = - !userQuery?.type || userQuery?.type === SupportedAIEventTypes.PROMPT - ? userQuery?.content - : camelCase(userQuery?.type) - - const disableMenu = useFloatingStore.getState().state?.AI_POPOVER?.disableMenu - - return ( - - - - - - - - - - setActiveEventIndex(index)} /> - - { - const content = aiEventsHistory?.at(activeEventIndex)?.at(0)?.content - insertContent(content) - }} - size={12} - icon={DefaultMIcons.INSERT.value} - /> - { - const content = aiEventsHistory?.at(activeEventIndex)?.at(0)?.content - insertContent(content, false) - }} - /> - - - - ) -} - -export default AIBlockPopover diff --git a/apps/webapp/src/Components/CreateTodoModal/TaskEditor/index.tsx b/apps/webapp/src/Components/CreateTodoModal/TaskEditor/index.tsx index bd7bbb676..02533c795 100644 --- a/apps/webapp/src/Components/CreateTodoModal/TaskEditor/index.tsx +++ b/apps/webapp/src/Components/CreateTodoModal/TaskEditor/index.tsx @@ -20,9 +20,10 @@ type TaskEditorType = { content: NodeEditorContent readOnly?: boolean onChange?: (val: any) => void + withCombobox?: boolean } -const TaskEditor = ({ editorId, readOnly, content, onChange }: TaskEditorType) => { +const TaskEditor = ({ editorId, readOnly, content, onChange, withCombobox = true }: TaskEditorType) => { const config = useEditorPluginConfig(editorId) const { uploadImageToS3 } = useAuth() const { uploadImageToWDCDN } = useUploadToCDN(uploadImageToS3) @@ -66,7 +67,7 @@ const TaskEditor = ({ editorId, readOnly, content, onChange }: TaskEditorType) = onChange={onChangeContent} editableProps={editableProps} > - + {withCombobox && } ) } diff --git a/apps/webapp/src/Components/CreateTodoModal/TaskEditor/plugins.ts b/apps/webapp/src/Components/CreateTodoModal/TaskEditor/plugins.ts index f6649cac7..150c400af 100644 --- a/apps/webapp/src/Components/CreateTodoModal/TaskEditor/plugins.ts +++ b/apps/webapp/src/Components/CreateTodoModal/TaskEditor/plugins.ts @@ -47,7 +47,7 @@ import { optionsCreateNodeIdPlugin, optionsSelectOnBackspacePlugin } from '../.. import { parseTwitterUrl } from '../../../Editor/Plugins/parseTwitterUrl' import Todo from '../../Todo' -const generateTodoPlugins = (uploadImage?: UploadImageFn) => { +const generateTodoPlugins = (uploadImage: UploadImageFn, inline?: boolean) => { return [ // elements createParagraphPlugin(), // paragraph element @@ -106,7 +106,7 @@ const generateTodoPlugins = (uploadImage?: UploadImageFn) => { createMentionPlugin(), // Mentions createILinkPlugin(), // Internal Links ILinks createInlineBlockPlugin(), - createSingleLinePlugin() + inline && createSingleLinePlugin() ] } @@ -129,7 +129,7 @@ export const getComponents = () => [ELEMENT_MEDIA_EMBED]: MediaEmbedElement as any }) -export const getTodoPlugins = (uploadImage?: UploadImageFn) => { - const plugins = createPlugins(generateTodoPlugins(uploadImage), { components: getComponents() }) +export const getTodoPlugins = (uploadImage: UploadImageFn, isInline = true) => { + const plugins = createPlugins(generateTodoPlugins(uploadImage, isInline), { components: getComponents() }) return plugins } diff --git a/apps/webapp/src/Components/Editor/BalloonToolbar/EditorBalloonToolbar.tsx b/apps/webapp/src/Components/Editor/BalloonToolbar/EditorBalloonToolbar.tsx index 880a45161..1ed8c01b1 100644 --- a/apps/webapp/src/Components/Editor/BalloonToolbar/EditorBalloonToolbar.tsx +++ b/apps/webapp/src/Components/Editor/BalloonToolbar/EditorBalloonToolbar.tsx @@ -113,7 +113,7 @@ const BallonMarkToolbarButtons = () => { <> } + icon={} onMouseDown={handleOpenAIPreview} /> diff --git a/apps/webapp/src/Components/Editor/Plateless.tsx b/apps/webapp/src/Components/Editor/Plateless.tsx index 5d8d42011..f14f3dec6 100644 --- a/apps/webapp/src/Components/Editor/Plateless.tsx +++ b/apps/webapp/src/Components/Editor/Plateless.tsx @@ -264,11 +264,11 @@ const RenderPlateless = React.memo( ({ content, typeMap, multiline = false }: RenderPlatelessProps) => { const childrenRender = content && - content.map((node) => { + content.map((node, i) => { if (Object.keys(typeMap).includes(node?.type)) { const RenderItem = typeMap[node?.type] return ( - + ) diff --git a/apps/webapp/src/Components/Filters/Filter.tsx b/apps/webapp/src/Components/Filters/Filter.tsx index b6c7fcdce..e2c40ac1c 100644 --- a/apps/webapp/src/Components/Filters/Filter.tsx +++ b/apps/webapp/src/Components/Filters/Filter.tsx @@ -93,8 +93,8 @@ const FilterRender = ({ filter, onChangeFilter, options, onRemoveFilter, hideJoi {Array.isArray(filter.values) && (filter.values.length > 0 ? ( filter.values.map((value) => ( - <> - + + {value.label} @@ -102,10 +102,11 @@ const FilterRender = ({ filter, onChangeFilter, options, onRemoveFilter, hideJoi height={16} icon={closeLine} onClick={(e) => { + e.stopPropagation() onRemoveFilter(filter) }} /> - + )) ) : ( 0 selected diff --git a/apps/webapp/src/Components/Todo/BoardTask.tsx b/apps/webapp/src/Components/Todo/BoardTask.tsx index 3a6f69246..b32b2d0e5 100644 --- a/apps/webapp/src/Components/Todo/BoardTask.tsx +++ b/apps/webapp/src/Components/Todo/BoardTask.tsx @@ -40,8 +40,8 @@ export const RenderBoardTask = React.memo( const getTodoOfNode = useTodoStore((store) => store.getTodoOfNodeWithoutCreating) const ref = React.useRef(null) - const todo = useMemo(() => getTodoOfNode(nodeid, todoid), [nodeid, todoid]) - const pC = useMemo(() => getPureContent(todo), [id, todo, documentUpdated]) + const todo = useMemo(() => getTodoOfNode(nodeid, todoid), [nodeid, todoid, documentUpdated]) + const pC = useMemo(() => getPureContent(todo), [id, todo]) const { accessWhenShared } = usePermissions() const readOnly = useMemo(() => isReadonly(accessWhenShared(todo?.nodeid)), [todo]) const toggleModal = useModalStore((store) => store.toggleOpen) diff --git a/apps/webapp/src/Components/Views/ParentFilters/index.tsx b/apps/webapp/src/Components/Views/ParentFilters/index.tsx index ebf2c21ad..178facfe5 100644 --- a/apps/webapp/src/Components/Views/ParentFilters/index.tsx +++ b/apps/webapp/src/Components/Views/ParentFilters/index.tsx @@ -4,19 +4,19 @@ import { useParams } from 'react-router-dom' import { Icon } from '@iconify/react' import { useTheme } from 'styled-components' -import { - DefaultMIcons, -FilterJoinDiv, getMIcon, - Group, - IconDisplay, - SearchFilterWrapper - } from '@mexit/shared' +import { DefaultMIcons, FilterJoinDiv, getMIcon, Group, IconDisplay, SearchFilterWrapper } from '@mexit/shared' import { DisplayFilter } from '../../../Components/Filters/Filter' import { getFilterJoinIcon } from '../../../Hooks/useFilterValueIcons' import { useViews } from '../../../Hooks/useViews' -import { CloseContainer,ParentFilter, ParentFilterContainer,ParentFiltersGrouped, StyledParentFilters } from './styled' +import { + CloseContainer, + ParentFilter, + ParentFilterContainer, + ParentFiltersGrouped, + StyledParentFilters +} from './styled' type ParentFilters = { filters: Array @@ -63,9 +63,9 @@ const ParentFilters: React.FC<{ currentViewId?: string; noMargin?: boolean }> = {expanded && ( - {parentFilters.map(({ id, label, filters }) => { + {parentFilters.map(({ id, label, filters }, i) => { return ( - + diff --git a/apps/webapp/src/Data/links.tsx b/apps/webapp/src/Data/links.tsx index 75071e0a7..61074b609 100644 --- a/apps/webapp/src/Data/links.tsx +++ b/apps/webapp/src/Data/links.tsx @@ -88,7 +88,7 @@ const useNavlinks = () => { // // isComingSoon: true // }, { - title: 'Links', + title: 'Captures', path: ROUTE_PATHS.links, icon: GetIcon(linkM), // count: count.reminders diff --git a/apps/webapp/src/Editor/AIPreviewContainer.tsx b/apps/webapp/src/Editor/AIPreviewContainer.tsx new file mode 100644 index 000000000..969b89a7e --- /dev/null +++ b/apps/webapp/src/Editor/AIPreviewContainer.tsx @@ -0,0 +1,15 @@ +import { AIPreview } from '@mexit/shared' + +import { useCreateNewMenu } from '../Hooks/useCreateNewMenu' + +import components from './Components/EditorPreviewComponents' +import { generateEditorPluginsWithComponents } from './Plugins' + +const AIPreviewContainer = () => { + const { getAIMenuItems } = useCreateNewMenu() + const plugins = generateEditorPluginsWithComponents(components, { exclude: { dnd: true } }) + + return +} + +export default AIPreviewContainer diff --git a/apps/webapp/src/Editor/Hooks/useUpdateBlock.ts b/apps/webapp/src/Editor/Hooks/useUpdateBlock.ts index d883c8f30..e218eb92b 100644 --- a/apps/webapp/src/Editor/Hooks/useUpdateBlock.ts +++ b/apps/webapp/src/Editor/Hooks/useUpdateBlock.ts @@ -1,10 +1,10 @@ import { findNodePath, getPlateEditorRef, setNodes } from '@udecode/plate' import { ELEMENT_PARAGRAPH, useContentStore } from '@mexit/core' +import { parseToMarkdown } from '@mexit/shared' import { useBufferStore } from '../../Hooks/useEditorBuffer' import { useUpdater } from '../../Hooks/useUpdater' -import parseToMarkdown from '../utils' type BlockDataType = Record diff --git a/apps/webapp/src/Editor/MexEditor.tsx b/apps/webapp/src/Editor/MexEditor.tsx index 0baebb8f0..be14f1815 100644 --- a/apps/webapp/src/Editor/MexEditor.tsx +++ b/apps/webapp/src/Editor/MexEditor.tsx @@ -7,7 +7,6 @@ import { EditableProps } from 'slate-react/dist/components/editable' import { useBlockHighlightStore, useMultipleEditors } from '@mexit/core' -import Floater from '../Components/AIPop/Floater' import { useGlobalListener } from '../Hooks/useGlobalListener' import { useFocusBlock } from '../Stores/useFocusBlock' @@ -16,6 +15,7 @@ import { MultiComboboxContainer } from './Components/MultiCombobox/multiCombobox import { useMexEditorStore } from './Hooks/useMexEditorStore' import { MexEditorValue } from './Types/Editor' import { ComboboxConfig } from './Types/MultiCombobox' +import AIPreviewContainer from './AIPreviewContainer' import { PluginOptionType } from './Plugins' export interface MexEditorOptions { @@ -112,10 +112,10 @@ export const MexEditorBase = (props: MexEditorProps) => { > {props.options?.withBalloonToolbar && props.BalloonMarkToolbarButtons} {isEmpty && } - {props.options?.withGlobalListener !== false && } {props.debug &&
{JSON.stringify(content, null, 2)}
} + ) } diff --git a/apps/webapp/src/Hooks/API/useNamespaceAPI.ts b/apps/webapp/src/Hooks/API/useNamespaceAPI.ts index da41d694d..45882ef3f 100644 --- a/apps/webapp/src/Hooks/API/useNamespaceAPI.ts +++ b/apps/webapp/src/Hooks/API/useNamespaceAPI.ts @@ -10,13 +10,14 @@ import { MIcon, mog, SingleNamespace, - useDataStore -, userPreferenceStore as useUserPreferenceStore } from '@mexit/core' + useDataStore, + userPreferenceStore as useUserPreferenceStore +} from '@mexit/core' import { DefaultMIcons } from '@mexit/shared' import { deserializeContent } from '../../Utils/serializer' import { WorkerRequestType } from '../../Utils/worker' -import { runBatchWorker } from '../../Workers/controller' +import { getEntitiyInitializer, runBatchWorker } from '../../Workers/controller' import { useUpdater } from '../useUpdater' export const useNamespaceApi = () => { @@ -66,17 +67,17 @@ export const useNamespaceApi = () => { { newILinks: [], archivedILinks: [] } ) + getEntitiyInitializer('initializeHeirarchy', newILinks) + const localILinks = useDataStore.getState().ilinks const allSpaces = namespaces.map((n) => n.ns) const spacePreferences = useUserPreferenceStore.getState().space const ns = allSpaces.filter((s) => !spacePreferences[s?.id]?.hidden) - // console.log('HERE ARE THEY', { ns, spacePreferences }) setAllSpaces(allSpaces) setNamespaces(ns) - // TODO: Also set archive links addInArchive(archivedILinks) diff --git a/apps/webapp/src/Hooks/useAIOptions.ts b/apps/webapp/src/Hooks/useAIOptions.ts deleted file mode 100644 index 558cee7a8..000000000 --- a/apps/webapp/src/Hooks/useAIOptions.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { toast } from 'react-hot-toast' - -import { AIEvent, API, SupportedAIEventTypes, useHistoryStore } from '@mexit/core' - -export const useAIOptions = () => { - const addInAIEventsHistory = useHistoryStore((store) => store.addInAIHistory) - - const performAIAction = async (type: SupportedAIEventTypes, content?: string): Promise => { - const aiEventsHistory = useHistoryStore.getState().ai - const userQuery: AIEvent = { - role: 'user', - type - } - - if (content) { - userQuery.content = content - } - - const reqData = { - context: [...aiEventsHistory.flat().filter((item) => item), userQuery] - } - - try { - const assistantResponse = await API.ai.perform(reqData) - - if (assistantResponse?.content) { - addInAIEventsHistory(userQuery, assistantResponse) - } - } catch (err) { - // * Write cute error message - toast('Something went wrong!') - console.error('Unable to perform AI action', err) - } - } - - return { - performAIAction - } -} diff --git a/apps/webapp/src/Hooks/useCreateNewMenu.tsx b/apps/webapp/src/Hooks/useCreateNewMenu.tsx index a7f8ec87c..a64a7f0fa 100644 --- a/apps/webapp/src/Hooks/useCreateNewMenu.tsx +++ b/apps/webapp/src/Hooks/useCreateNewMenu.tsx @@ -8,10 +8,10 @@ import { defaultContent, DRAFT_NODE, generateSnippetId, + getMenuItem, isReservedNamespace, MIcon, ModalsType, - SupportedAIEventTypes, useDataStore, useLayoutStore, useMetadataStore, @@ -20,7 +20,7 @@ import { useShareModalStore, useSnippetStore } from '@mexit/core' -import { DefaultMIcons, getMIcon, InteractiveToast } from '@mexit/shared' +import { DefaultMIcons, getMIcon, InteractiveToast, useAIOptions } from '@mexit/shared' import { useDeleteStore } from '../Components/Refactor/DeleteModal' import { doesLinkRemain } from '../Components/Refactor/doesLinkRemain' @@ -29,7 +29,6 @@ import { useBlockMenu } from '../Editor/Components/useBlockMenu' import useUpdateBlock from '../Editor/Hooks/useUpdateBlock' import { useViewStore } from '../Stores/useViewStore' -import { useAIOptions } from './useAIOptions' import { useCreateNewNote } from './useCreateNewNote' import { useNamespaces } from './useNamespaces' import { useNavigation } from './useNavigation' @@ -44,24 +43,10 @@ interface MenuListItemType { disabled?: boolean icon?: MIcon onSelect: any + category?: string options?: Array } -export const getMenuItem = ( - label: string, - onSelect: any, - disabled?: boolean, - icon?: MIcon, - options?: Array -) => ({ - id: label, - label, - onSelect, - icon, - disabled, - options -}) - export const useCreateNewMenu = () => { const loadSnippet = useSnippetStore((store) => store.loadSnippet) const toggleModal = useModalStore((store) => store.toggleOpen) @@ -75,11 +60,11 @@ export const useCreateNewMenu = () => { const deleteNamespace = useDataStore((store) => store.deleteNamespace) const { goTo } = useRouting() - const { getSelectionInMarkdown } = useUpdateBlock() const { push } = useNavigation() + const { getSelectionInMarkdown } = useUpdateBlock() const { deleteView } = useViews() const { addSnippet } = useSnippets() - const { performAIAction } = useAIOptions() + const { handleOpenAIPreview, getAIMenuItems } = useAIOptions() const { execRefactorAsync } = useRefactor() const { createNewNote } = useCreateNewNote() const blockMenuItems = useBlockMenu() @@ -289,6 +274,15 @@ export const useCreateNewMenu = () => { const getBlockMenuItems = (): MenuListItemType[] => { return [ + getMenuItem( + 'Enhance', + () => { + const content = getSelectionInMarkdown() + handleOpenAIPreview(content) + }, + false, + DefaultMIcons.AI + ), getMenuItem('Send', blockMenuItems.onSendToClick, false, DefaultMIcons.SEND), getMenuItem('Move', blockMenuItems.onMoveToClick, false, DefaultMIcons.MOVE), getMenuItem('Delete', blockMenuItems.onDeleteClick, false, DefaultMIcons.DELETE) @@ -334,37 +328,6 @@ export const useCreateNewMenu = () => { ] } - // * AI functions - const handleAIQuery = async (type: SupportedAIEventTypes, callback: any) => { - performAIAction(type).then((res) => { - callback(res) - }) - } - - const getAIMenuItems = () => { - return [ - getMenuItem( - 'Continue', - (c) => handleAIQuery(SupportedAIEventTypes.EXPAND, c), - false, - getMIcon('ICON', 'system-uicons:write') - ), - getMenuItem( - 'Explain', - (c) => handleAIQuery(SupportedAIEventTypes.EXPLAIN, c), - false, - getMIcon('ICON', 'ri:question-line') - ), - getMenuItem('Summarize', (c) => handleAIQuery(SupportedAIEventTypes.SUMMARIZE, c), false, DefaultMIcons.AI), - getMenuItem( - 'Actionable', - (c) => handleAIQuery(SupportedAIEventTypes.ACTIONABLE, c), - false, - getMIcon('ICON', 'ic:round-view-list') - ) - ] - } - return { getCreateNewMenuItems, getSnippetsMenuItems, diff --git a/apps/webapp/src/Hooks/useRouting.ts b/apps/webapp/src/Hooks/useRouting.ts index abe136641..9cc240044 100644 --- a/apps/webapp/src/Hooks/useRouting.ts +++ b/apps/webapp/src/Hooks/useRouting.ts @@ -20,7 +20,7 @@ export const ROUTE_PATHS = { snippets: '/snippets', snippet: '/snippets/node', // * /snippets/node/:snippetid prompt: '/snippets/prompt', - links: '/links', + links: '/captures', actions: '/actions', oauth: '/oauth', oauthdesktop: '/oauth/desktop', diff --git a/apps/webapp/src/Hooks/useSocket.ts b/apps/webapp/src/Hooks/useSocket.ts index 7f37aa022..eb1ac7ef6 100644 --- a/apps/webapp/src/Hooks/useSocket.ts +++ b/apps/webapp/src/Hooks/useSocket.ts @@ -50,7 +50,7 @@ const useSocket = () => { config.baseURLs.MEXIT_WEBSOCKET_URL, { onOpen: () => mog('CONNECTION OPENED'), - retryOnError: true, + retryOnError: false, onError: (event) => { mog('Socket Error Occured', { event }) }, diff --git a/apps/webapp/src/Hooks/useTaskFromSelection.ts b/apps/webapp/src/Hooks/useTaskFromSelection.ts index ad355ebd2..955feb020 100644 --- a/apps/webapp/src/Hooks/useTaskFromSelection.ts +++ b/apps/webapp/src/Hooks/useTaskFromSelection.ts @@ -26,6 +26,7 @@ export const useTaskFromSelection = () => { path: todayTaskNodePath, parent: { path: dailyTaskNode?.nodeid, namespace: dailyTaskNode?.namespace }, noteContent: nodeContent, + noRedirect: true, namespace: getDefaultNamespaceId() }) : undefined diff --git a/apps/webapp/src/Stores/useAuth.ts b/apps/webapp/src/Stores/useAuth.ts index e76922dab..598b68a61 100644 --- a/apps/webapp/src/Stores/useAuth.ts +++ b/apps/webapp/src/Stores/useAuth.ts @@ -15,6 +15,8 @@ import { useDataStore, useEditorStore, useHelpStore, + useHighlightStore, + useLinkStore, useMentionStore, useMetadataStore, usePromptStore, @@ -47,7 +49,9 @@ export const useAuthentication = () => { const clearUsersCache = useUserCacheStore((s) => s.clearCache) const clearUserPreferences = useUserPreferenceStore((store) => store.clear) const clearSnippets = useSnippetStore((s) => s.clear) + const clearHighlightsStore = useHighlightStore((s) => s.reset) const resetDataStore = useDataStore((s) => s.resetDataStore) + const resetLinksStore = useLinkStore((s) => s.reset) const resetPublicNodes = usePublicNodeStore((s) => s.reset) const clearRecents = useRecentsStore((s) => s.clear) const clearMentions = useMentionStore((m) => m.reset) @@ -119,7 +123,9 @@ export const useAuthentication = () => { clearRoutesInformation() resetPublicNodes() clearRecents() + clearHighlightsStore() clearReminders() + resetLinksStore() clearSnippets() resetShortcuts() clearAppStore() diff --git a/apps/webapp/src/Utils/nav.ts b/apps/webapp/src/Utils/nav.ts index ebf53de99..8e51e724f 100644 --- a/apps/webapp/src/Utils/nav.ts +++ b/apps/webapp/src/Utils/nav.ts @@ -1,3 +1,5 @@ +import { ROUTE_PATHS } from '../Hooks/useRouting' + export const showNav = (pathname: string): boolean => { if (pathname === '/') return true const showNavPaths = [ @@ -10,7 +12,7 @@ export const showNav = (pathname: string): boolean => { '/tag', '/integrations', '/reminders', - '/links' + ROUTE_PATHS.links ] for (const path of showNavPaths) { diff --git a/apps/webapp/src/Views/LinkView.tsx b/apps/webapp/src/Views/LinkView.tsx index eb28c3137..e20ee15e8 100644 --- a/apps/webapp/src/Views/LinkView.tsx +++ b/apps/webapp/src/Views/LinkView.tsx @@ -123,7 +123,7 @@ const LinkView = () => { return ( - Links + Captures { onSelect={onSelect} onEscapeExit={onEscapeExit} options={{ - inputPlaceholder: 'Search links', + inputPlaceholder: 'Search captures', view: ViewType.List }} onSearch={onSearch} diff --git a/apps/webapp/src/Workers/controller.ts b/apps/webapp/src/Workers/controller.ts index 9da1789b5..2ce773b5d 100644 --- a/apps/webapp/src/Workers/controller.ts +++ b/apps/webapp/src/Workers/controller.ts @@ -18,7 +18,7 @@ import { WorkerRequestType } from '../Utils/worker' import { type AnalysisWorkerInterface } from './analysis' import { type RequestsWorkerInterface } from './requests' -import { type SearchWorkerInterface } from './search' +import { type SearchWorkerInterface, InitializeSearchEntity } from './search' export type AnalysisModifier = SearchRepExtra export interface AnalysisOptions { @@ -84,6 +84,18 @@ export const updateILink = async (ilink: ILink) => { } } +export const getEntitiyInitializer = async ( + updateType: T, + data: InitializeSearchEntity[T] +) => { + try { + if (!searchWorker) throw new Error('Search Worker Not Initialized') + return await searchWorker.initializeEntities(updateType, data) + } catch (error) { + mog('Get SearchX instance', { error }) + } +} + export const analyseContent = async (props: AnalyseContentProps) => { try { if (!analysisWorker) { diff --git a/apps/webapp/src/Workers/search.ts b/apps/webapp/src/Workers/search.ts index f23e14fb1..23ff5bb01 100644 --- a/apps/webapp/src/Workers/search.ts +++ b/apps/webapp/src/Workers/search.ts @@ -1,6 +1,6 @@ import { Indexes, ISearchQuery, IUpdateDoc, SearchResult, SearchX } from '@workduck-io/mex-search' -import { ILink, mog, PersistentData } from '@mexit/core' +import { Highlight, ILink, Link, mog, PersistentData, Reminder } from '@mexit/core' import { exposeX } from './worker-utils' @@ -8,6 +8,13 @@ let searchX = new SearchX() let hasInitialized = false +export interface InitializeSearchEntity { + initializeHeirarchy: ILink[] + initializeHighlights: Highlight[] + initializeLinks: Link[] + initializeReminders: Reminder[] +} + const searchWorker = { getInitState: () => { return hasInitialized @@ -29,6 +36,7 @@ const searchWorker = { contents: fileData.snippets } as any }) + hasInitialized = true } catch (err) { console.log('Error initializing search', err) @@ -87,6 +95,10 @@ const searchWorker = { }) }, + initializeEntities: (updateType: T, data: InitializeSearchEntity[T]) => { + return searchX[updateType](data as any) + }, + searchIndexWithRanking: (indexKey: Indexes, query: ISearchQuery, tags?: Array) => { try { const searchResults = searchX.search({ options: query, indexKey }) diff --git a/libs/core/src/Stores/highlight.store.ts b/libs/core/src/Stores/highlight.store.ts index faad22aee..3ceb1d02e 100644 --- a/libs/core/src/Stores/highlight.store.ts +++ b/libs/core/src/Stores/highlight.store.ts @@ -63,6 +63,10 @@ export const highlightStoreConfig = (set, get) => ({ set({ highlightBlockMap }) }, + reset: () => { + set({ highlights: [], highlightBlockMap: {} }) + }, + setHighlights: (highlights: Highlights) => { set({ highlights }) }, @@ -105,4 +109,4 @@ export const highlightStoreConfig = (set, get) => ({ } }) -export const useHighlightStore = createStore(highlightStoreConfig, StoreIdentifier.HIGHLIGHTS, true) \ No newline at end of file +export const useHighlightStore = createStore(highlightStoreConfig, StoreIdentifier.HIGHLIGHTS, true) diff --git a/libs/core/src/Stores/history.store.ts b/libs/core/src/Stores/history.store.ts index c3e084999..d8209d648 100644 --- a/libs/core/src/Stores/history.store.ts +++ b/libs/core/src/Stores/history.store.ts @@ -13,6 +13,17 @@ export const historyStoreConfig = (set, get) => ({ addInitialEvent: (event: AIEvent) => { set({ ai: [[event, undefined]] }) }, + updateAIEvent: (event: AIEvent, index: number) => { + const aiEventsHistory = get().ai as AIEventsHistory + const checkIndex = index === -1 ? aiEventsHistory.length - 1 : index + const updateEventHistory = aiEventsHistory.map((eventHistory, i) => { + if (i === checkIndex) { + return [event, eventHistory[1]] + } + return eventHistory + }) + set({ ai: updateEventHistory }) + }, addInAIHistory: (userQuery: AIEvent, assistantResponse: AIEvent) => { const aiEventsHistory = get().ai as AIEventsHistory const activeEventIndex = get().activeEventIndex diff --git a/libs/core/src/Stores/link.store.ts b/libs/core/src/Stores/link.store.ts index 2a479e730..b6a1a0d32 100644 --- a/libs/core/src/Stores/link.store.ts +++ b/libs/core/src/Stores/link.store.ts @@ -20,13 +20,16 @@ export interface Link { } export const linkStoreConstructor = (set, get) => ({ - links: [], + links: [] as Link[], setLinks: (links: Link[]) => set({ links }), addLink: (link: Link) => { const oldLinks = get().links set({ links: [...oldLinks, link] }) + }, + reset: () => { + set({ links: [] }) } }) -export const useLinkStore = createStore(linkStoreConstructor, StoreIdentifier.LINKS, true) \ No newline at end of file +export const useLinkStore = createStore(linkStoreConstructor, StoreIdentifier.LINKS, true) diff --git a/libs/core/src/Types/History.ts b/libs/core/src/Types/History.ts index 3ebb47976..1cf3fc04c 100644 --- a/libs/core/src/Types/History.ts +++ b/libs/core/src/Types/History.ts @@ -6,9 +6,12 @@ export enum SupportedAIEventTypes { PROMPT = 'PROMPT' } +export type ContentFormatType = 'markdown' | 'html' | 'audio' | 'video' + export interface AIEvent { role: 'user' | 'assistant' content?: string + inputFormat?: ContentFormatType type?: SupportedAIEventTypes } diff --git a/libs/core/src/Utils/defaultShortcutsData.ts b/libs/core/src/Utils/defaultShortcutsData.ts index a4f0c4c2d..a3111339d 100644 --- a/libs/core/src/Utils/defaultShortcutsData.ts +++ b/libs/core/src/Utils/defaultShortcutsData.ts @@ -22,8 +22,8 @@ export const defaultShortcuts = { category: 'Actions' }, showTasks: { - title: 'Tasks', - keystrokes: 'KeyG KeyT', + title: 'Views', + keystrokes: 'KeyG KeyV', category: 'Navigate' }, showHelp: { @@ -32,8 +32,8 @@ export const defaultShortcuts = { category: 'Navigate' }, goToLinks: { - title: 'Go to Links', - keystrokes: 'KeyG KeyL', + title: 'Go to Captures', + keystrokes: 'KeyG KeyC', category: 'Navigate' }, showIntegrations: { diff --git a/libs/core/src/Utils/index.ts b/libs/core/src/Utils/index.ts index e77b5df56..b1764e5f0 100644 --- a/libs/core/src/Utils/index.ts +++ b/libs/core/src/Utils/index.ts @@ -13,6 +13,7 @@ export * from './helpers' export * from './heuristichelper' export * from './idbStorageAdapter' export * from './idGenerator' +export * from './isExtension' export * from './keyMap' export * from './links' export * from './linkUtils' diff --git a/libs/core/src/Utils/parseData.ts b/libs/core/src/Utils/parseData.ts index 48ed41228..d237aa001 100644 --- a/libs/core/src/Utils/parseData.ts +++ b/libs/core/src/Utils/parseData.ts @@ -1,5 +1,6 @@ import { diskIndex, indexNames } from '../Data/search' import { BlockType } from '../Stores/block.store' +import { MenuListItemType, MIcon } from '../Types' import { NodeEditorContent } from '../Types/Editor' import { GenericSearchData, PersistentData, SearchRepExtra } from '../Types/Search' @@ -297,3 +298,18 @@ export const getBlocks = (content: NodeEditorContent): Record | und return undefined } + +export const getMenuItem = ( + label: string, + onSelect: any, + disabled?: boolean, + icon?: MIcon, + options?: Array +) => ({ + id: label, + label, + onSelect, + icon, + disabled, + options +}) diff --git a/apps/webapp/src/Components/AIPop/AIHistory.tsx b/libs/shared/src/Components/AIPreview/AIHistory.tsx similarity index 96% rename from apps/webapp/src/Components/AIPop/AIHistory.tsx rename to libs/shared/src/Components/AIPreview/AIHistory.tsx index 0b152c973..af759d757 100644 --- a/apps/webapp/src/Components/AIPop/AIHistory.tsx +++ b/libs/shared/src/Components/AIPreview/AIHistory.tsx @@ -1,3 +1,5 @@ +import React from 'react' + import { SupportedAIEventTypes, useHistoryStore } from '@mexit/core' import { StyledAIHistory, StyledAIHistoryContainer } from './styled' diff --git a/libs/shared/src/Components/AIPreview/AIResponse.tsx b/libs/shared/src/Components/AIPreview/AIResponse.tsx new file mode 100644 index 000000000..478b09cc2 --- /dev/null +++ b/libs/shared/src/Components/AIPreview/AIResponse.tsx @@ -0,0 +1,71 @@ +import React, { useEffect, useState } from 'react' + +import { createPlateEditor, deserializeMd, Plate, usePlateEditorRef } from '@udecode/plate' +import { useDebouncedCallback } from 'use-debounce' + +import { ELEMENT_PARAGRAPH, NodeEditorContent, useHistoryStore } from '@mexit/core' + +import { EditorStyles } from '../../Style/Editor' +import { getDeserializeSelectionToNodes } from '../../Utils/deserialize' +import { parseToMarkdown } from '../../Utils/utils' + +import { AIResponseContainer } from './styled' + +const AIResponse = ({ aiResponse, index, plugins }) => { + const editor = usePlateEditorRef() + const [mounted, setMounted] = useState(false) + const selected = aiResponse?.at(index)?.at(0) + const updateAIEvent = useHistoryStore((s) => s.updateAIEvent) + + useEffect(() => { + if (selected?.inputFormat === 'html') { + const baseEditor = createPlateEditor({ plugins }) + + const content = getDeserializeSelectionToNodes( + { + text: selected.content, + metadata: '' + }, + baseEditor + ) + onChange(content) + setMounted(true) + } + }, []) + + const onChange = useDebouncedCallback((value: NodeEditorContent) => { + if (selected && value) { + const markdownContent = parseToMarkdown({ children: value, type: ELEMENT_PARAGRAPH }) + + updateAIEvent( + { + ...selected, + inputFormat: 'markdown', + content: markdownContent + }, + index + ) + } + }, 400) + + if (selected && selected.inputFormat !== 'html') { + const deserialize = deserializeMd(editor, selected?.content) + + return ( + + + onChange(e)} + id={`wd-mexit-ai-response-${aiResponse.length}-${index}-${mounted}`} + /> + + + ) + } + + return <> +} + +export default AIResponse diff --git a/apps/webapp/src/Components/AIPop/AIResults.tsx b/libs/shared/src/Components/AIPreview/AIResults.tsx similarity index 92% rename from apps/webapp/src/Components/AIPop/AIResults.tsx rename to libs/shared/src/Components/AIPreview/AIResults.tsx index a332ac4d6..d6960f578 100644 --- a/apps/webapp/src/Components/AIPop/AIResults.tsx +++ b/libs/shared/src/Components/AIPreview/AIResults.tsx @@ -1,3 +1,5 @@ +import React from 'react' + import { AIResult, StyledAIResults } from './styled' type AIResultsProps = { diff --git a/apps/webapp/src/Components/AIPop/Floater.tsx b/libs/shared/src/Components/AIPreview/Floater.tsx similarity index 54% rename from apps/webapp/src/Components/AIPop/Floater.tsx rename to libs/shared/src/Components/AIPreview/Floater.tsx index e161b1b4d..3c286f536 100644 --- a/apps/webapp/src/Components/AIPop/Floater.tsx +++ b/libs/shared/src/Components/AIPreview/Floater.tsx @@ -1,5 +1,6 @@ -import { useEffect, useRef } from 'react' +import React, { useEffect, useRef } from 'react' import { ErrorBoundary } from 'react-error-boundary' +import { RemoveScroll } from 'react-remove-scroll' import { arrow, @@ -18,12 +19,17 @@ import { import { getSelectionBoundingClientRect } from '@udecode/plate' import { useTheme } from 'styled-components' -import { FloatingElementType, mog, useFloatingStore, useHistoryStore } from '@mexit/core' +import { FloatingElementType, useFloatingStore, useHistoryStore } from '@mexit/core' import { FloaterContainer } from './styled' +import { AIPreviewProps } from './types' import AIBlockPopover from '.' -const DefaultFloater = ({ onClose }) => { +interface DefaultFloaterProps extends AIPreviewProps { + onClose?: () => void +} + +const DefaultFloater: React.FC = ({ onClose, root, ...props }) => { const isOpen = useFloatingStore((store) => store.floatingElement === FloatingElementType.AI_POPOVER) const setIsOpen = useFloatingStore((store) => store.setFloatingElement) @@ -36,7 +42,6 @@ const DefaultFloater = ({ onClose }) => { const state = isOpen ? FloatingElementType.AI_POPOVER : null if (!state && onClose) { - mog('called') onClose() } setIsOpen(state) @@ -72,45 +77,46 @@ const DefaultFloater = ({ onClose }) => { }, [isOpen]) return ( - + {isOpen && ( - - - - - - + + + + + + + + )} ) } -const Floater = () => { +export const AIPreview: React.FC> = (props) => { const clearAIEventsHistory = useHistoryStore((s) => s.clearAIHistory) return ( <>}> - + ) } - -export default Floater diff --git a/libs/shared/src/Components/AIPreview/index.tsx b/libs/shared/src/Components/AIPreview/index.tsx new file mode 100644 index 000000000..56d8d84b6 --- /dev/null +++ b/libs/shared/src/Components/AIPreview/index.tsx @@ -0,0 +1,163 @@ +import React, { useEffect, useMemo } from 'react' + +import { + deserializeMd, + focusEditor, + getEdgePoints, + getEndPoint, + getPlateEditorRef, + getPointAfter, + getPointBefore, + getPointBeforeLocation, + getStartPoint, + insertNodes +} from '@udecode/plate' +import Highlighter from 'web-highlighter' + +import { IconButton } from '@workduck-io/mex-components' + +import { camelCase, generateTempId, SupportedAIEventTypes, useFloatingStore, useHistoryStore } from '@mexit/core' + +import { useAIOptions } from '../../Hooks/useAIOptions' +import { StyledButton } from '../../Style/Buttons' +import { Group } from '../../Style/Layouts' +import { AutoComplete } from '../FloatingElements' +import { IconDisplay } from '../IconDisplay' +import { DefaultMIcons } from '../Icons' +import InsertMenu from '../InsertMenu' + +import AIHistory from './AIHistory' +import AIResponse from './AIResponse' +import { AIContainerFooter, AIContainerHeader, AIContainerSection, StyledAIContainer } from './styled' +import { AIPreviewProps } from './types' + +const AIPreviewContainer: React.FC = (props) => { + const aiEventsHistory = useHistoryStore((s) => s.ai) + const activeEventIndex = useHistoryStore((s) => s.activeEventIndex) + const setActiveEventIndex = useHistoryStore((s) => s.setActiveEventIndex) + const clearAIResponses = useHistoryStore((s) => s.clearAIResponses) + const setFloatingElement = useFloatingStore((s) => s.setFloatingElement) + + const { performAIAction } = useAIOptions() + + const defaultItems = useMemo(() => { + if (props.getDefaultItems) return props.getDefaultItems() + return [] + }, []) + + const getContent = (content: string) => { + if (!content) return + + const editor = getPlateEditorRef() + const deserializedContent = deserializeMd(editor, content)?.map((node) => ({ + ...node, + id: generateTempId() + })) + + return deserializedContent + } + + const insertContent = (content: string, replace = true) => { + const editor = getPlateEditorRef() + const deserializedContent = getContent(content) + + if (Array.isArray(deserializedContent) && deserializedContent.length > 0) { + console.log('EDGES', { + selection: editor.selection, + getPointAfter: getPointAfter(editor, editor.selection), + getPointBefore: getPointBefore(editor, editor.selection), + getEdgePoints: getEdgePoints(editor, editor.selection), + getEndPoint: getEndPoint(editor, editor.selection), + getStartPoint: getStartPoint(editor, editor.selection), + point: getPointBeforeLocation(editor, editor.selection) + }) + const at = replace ? editor?.selection : getPointAfter(editor, editor.selection) + + insertNodes(editor, deserializedContent, { + at + }) + + try { + focusEditor(editor) + } catch (err) { + console.error('Unable to focus editor', err) + } + + setFloatingElement(undefined) + } + } + + useEffect(() => { + return () => { + const state = useFloatingStore.getState().state?.AI_POPOVER + if (state?.range) { + const highlight = new Highlighter() + highlight.removeAll() + } + } + }, []) + + const handleOnEnter = async (value: string) => { + try { + await performAIAction(SupportedAIEventTypes.PROMPT, value) + } catch (err) { + console.error('Unable generate prompt result', err) + } + } + + const handleOnInsert = (id?: string) => { + const content = aiEventsHistory?.at(activeEventIndex)?.at(0)?.content + if (!props.insertInNote) { + insertContent(content, false) + } else { + const deserializedContent = getContent(content) + props.onInsert?.(deserializedContent, id) + } + } + + const userQuery = aiEventsHistory?.at(activeEventIndex)?.at(-1) + const defaultValue = + !userQuery?.type || userQuery?.type === SupportedAIEventTypes.PROMPT + ? userQuery?.content + : camelCase(userQuery?.type) + + const disableMenu = useFloatingStore.getState().state?.AI_POPOVER?.disableMenu + + return ( + + + + + + + + + + setActiveEventIndex(index)} /> + + {props.allowReplace && ( + { + const content = aiEventsHistory?.at(activeEventIndex)?.at(0)?.content + insertContent(content) + }} + > + + Replace + + )} + + + + + ) +} + +export default AIPreviewContainer diff --git a/apps/webapp/src/Components/AIPop/styled.tsx b/libs/shared/src/Components/AIPreview/styled.tsx similarity index 88% rename from apps/webapp/src/Components/AIPop/styled.tsx rename to libs/shared/src/Components/AIPreview/styled.tsx index edee5e1dd..9d434d200 100644 --- a/apps/webapp/src/Components/AIPop/styled.tsx +++ b/libs/shared/src/Components/AIPreview/styled.tsx @@ -1,7 +1,8 @@ import styled, { keyframes } from 'styled-components' import { SupportedAIEventTypes } from '@mexit/core' -import { BodyFont } from '@mexit/shared' + +import { BodyFont } from '../../Style/Search' const getEventColor = (type: SupportedAIEventTypes | undefined, saturation = 100, lightness = 75) => { if (!type) return `hsl(-210, 100%, 75%)` @@ -57,6 +58,7 @@ export const AIContainerHeader = styled.header` export const AIResponseContainer = styled.div` ${BodyFont} + padding: ${({ theme }) => `${theme.spacing.small} ${theme.spacing.medium}`}; max-height: 16rem; ` @@ -98,10 +100,12 @@ export const StyledAIHistory = styled.span<{ type: SupportedAIEventTypes }>` ` export const FloaterContainer = styled.div` - border-radius: ${({ theme }) => theme.borderRadius.large}; - background-color: rgba(${({ theme }) => theme.rgbTokens.surfaces.modal}, 0.5); + color: ${({ theme }) => theme.tokens.text.default}; + border-radius: ${({ theme }) => theme.borderRadius.small}; + background-color: rgba(${({ theme }) => theme.rgbTokens.surfaces.modal}, 0.8); box-shadow: inset ${({ theme }) => theme.tokens.shadow.medium}; backdrop-filter: blur(2rem); + border: 1px solid ${({ theme }) => theme.tokens.surfaces.app}; transform-origin: top; z-index: 11; border: 1px solid ${({ theme }) => theme.tokens.surfaces.s[3]}; @@ -109,9 +113,9 @@ export const FloaterContainer = styled.div` ` export const StyledAIContainer = styled.div` - width: 28rem; + width: 40rem; height: 24rem; - max-width: 28rem; + max-width: 40rem; max-height: 24rem; display: flex; flex-direction: column; diff --git a/libs/shared/src/Components/AIPreview/types.ts b/libs/shared/src/Components/AIPreview/types.ts new file mode 100644 index 000000000..fc1e29bd3 --- /dev/null +++ b/libs/shared/src/Components/AIPreview/types.ts @@ -0,0 +1,10 @@ +import { MenuListItemType, NodeEditorContent } from '@mexit/core' + +export interface AIPreviewProps { + insertInNote?: boolean + onInsert?: (content: NodeEditorContent, noteId?: string) => void + allowReplace?: boolean + plugins: Array + root?: HTMLElement + getDefaultItems?: () => Array +} diff --git a/libs/shared/src/Components/FloatingElements/Autocomplete.style.tsx b/libs/shared/src/Components/FloatingElements/Autocomplete.style.tsx index 54ea68966..68eb0c3fd 100644 --- a/libs/shared/src/Components/FloatingElements/Autocomplete.style.tsx +++ b/libs/shared/src/Components/FloatingElements/Autocomplete.style.tsx @@ -47,6 +47,10 @@ export const AutoCompleteInput = styled.input` width: 100%; color: ${({ theme }) => theme.tokens.text.default}; + ::placeholder { + color: ${({ theme }) => theme.tokens.text.fade}; + } + &:focus-visible, :hover, :focus { diff --git a/libs/shared/src/Components/FloatingElements/Autocomplete.tsx b/libs/shared/src/Components/FloatingElements/Autocomplete.tsx index 1e26e602c..7a27e98f7 100644 --- a/libs/shared/src/Components/FloatingElements/Autocomplete.tsx +++ b/libs/shared/src/Components/FloatingElements/Autocomplete.tsx @@ -13,6 +13,7 @@ import { useListNavigation, useRole } from '@floating-ui/react' +import { useTheme } from 'styled-components' import { fuzzySearch, MenuListItemType } from '@mexit/core' @@ -31,13 +32,18 @@ import { import { MenuItem } from './Dropdown' import { MenuClassName, MenuItemClassName } from './Dropdown.classes' -export const AutoComplete: React.FC<{ +interface AutoCompleteProps { onEnter: any clearOnEnter?: boolean + onCommandEnter?: () => void disableMenu?: boolean defaultItems: Array defaultValue?: string -}> = ({ defaultItems = [], disableMenu, defaultValue, onEnter, clearOnEnter }) => { +} + +export const AutoComplete: React.FC = (props) => { + const { defaultItems = [], disableMenu, defaultValue, onEnter, onCommandEnter, clearOnEnter } = props + const [open, setOpen] = useState(false) const [isLoading, setIsLoading] = useState(false) const [inputValue, setInputValue] = useState(defaultValue ?? '') @@ -65,6 +71,7 @@ export const AutoComplete: React.FC<{ ] }) + const theme = useTheme() const role = useRole(context, { role: 'listbox' }) const dismiss = useDismiss(context) const listNav = useListNavigation(context, { @@ -112,7 +119,7 @@ export const AutoComplete: React.FC<{ return ( <> - + ` }} ` -export const RootMenuWrapper = styled.button<{ border: boolean; noHover?: boolean; noBackground?: boolean }>` +export const RootMenuWrapper = styled.button<{ + $noPadding?: boolean + border: boolean + noHover?: boolean + noBackground?: boolean +}>` display: flex; align-items: center; gap: ${({ theme }) => theme.spacing.tiny}; @@ -86,7 +91,11 @@ export const RootMenuWrapper = styled.button<{ border: boolean; noHover?: boolea border-radius: ${({ theme }) => theme.borderRadius.small}; border: ${({ border, theme }) => (border ? `1px solid ${theme.tokens.surfaces.separator}` : 'none')}; transition: background 0.15s ease-in-out; - padding: ${({ theme }) => theme.spacing.tiny} ${({ theme }) => theme.spacing.small}; + ${({ $noPadding = false }) => + !$noPadding && + css` + padding: ${({ theme }) => theme.spacing.tiny} ${({ theme }) => theme.spacing.small}; + `} :hover { cursor: pointer; diff --git a/libs/shared/src/Components/FloatingElements/Dropdown.tsx b/libs/shared/src/Components/FloatingElements/Dropdown.tsx index 159aee2a2..b09f3542e 100644 --- a/libs/shared/src/Components/FloatingElements/Dropdown.tsx +++ b/libs/shared/src/Components/FloatingElements/Dropdown.tsx @@ -35,6 +35,7 @@ import searchLine from '@iconify/icons-ri/search-line' import { Icon } from '@iconify/react' import cx from 'classnames' import { debounce } from 'lodash' +import { useTheme } from 'styled-components' import { fuzzySearch, MIcon } from '@mexit/core' @@ -127,6 +128,7 @@ interface Props { * Show Button with Border */ border?: boolean + noPadding?: boolean handleKeyDown?: (e: KeyboardEvent) => void @@ -153,6 +155,7 @@ export const MenuComponent = forwardRef(children) const inputRef = React.useRef(null) - + const theme = useTheme() const listItemsRef = useRef>([]) const onSearchChange: React.ChangeEventHandler = (e) => { @@ -329,6 +332,7 @@ export const MenuComponent = forwardRef {allowSearch && children && ( - + + title?: string + root?: any + isMenu?: boolean +} + +const InsertMenu: React.FC = ({ onClick, title = 'Insert', isMenu, root }) => { + if (!isMenu) { + return ( + + + {title} + + ) + } + + const getQuickLinks = () => { + const ilinks = useDataStore.getState().ilinks + const sharedNodes = useDataStore.getState().sharedNodes + const namespaces = useDataStore.getState().spaces + const metadata = useMetadataStore.getState().metadata.notes + + const mLinks = ilinks.map((l) => ({ + label: getNameFromPath(l.path), + icon: metadata[l.nodeid]?.icon, + id: l.nodeid, + category: namespaces.find((n) => n.id === l.namespace)?.name + })) + + const sLinks = sharedNodes.map((l) => ({ + label: getNameFromPath(l.path), + icon: metadata[l.nodeid]?.icon, + id: l.nodeid, + category: SHARED_NAMESPACE.name + })) + + return [...mLinks, ...sLinks] + } + + const noteLinks = getQuickLinks() + + return ( + + + {title} + + } + > + {noteLinks.map((item) => { + return ( + onClick(item.id)} + /> + ) + })} + + ) +} + +export default InsertMenu diff --git a/libs/shared/src/Hooks/useAIOptions.ts b/libs/shared/src/Hooks/useAIOptions.ts new file mode 100644 index 000000000..223d77468 --- /dev/null +++ b/libs/shared/src/Hooks/useAIOptions.ts @@ -0,0 +1,130 @@ +import { toast } from 'react-hot-toast' + +import Highlighter from 'web-highlighter' + +import { + AIEvent, + API, + FloatingElementType, + getMenuItem, + getMIcon, + isExtension, + SupportedAIEventTypes, + useAuthStore, + useFloatingStore, + useHistoryStore +} from '@mexit/core' + +import { DefaultMIcons } from '../Components/Icons' + +export const useAIOptions = () => { + const addAIEvent = useHistoryStore((store) => store.addInitialEvent) + const addInAIEventsHistory = useHistoryStore((store) => store.addInAIHistory) + const setFloatingElement = useFloatingStore((store) => store.setFloatingElement) + + const handleOpenAIPreview = (content: string) => { + const selection = window.getSelection() + + if (content) { + addAIEvent({ role: 'assistant', content }) + } + + const highlight = new Highlighter({ + style: { + className: 'highlight' + } + }) + + const range = selection.getRangeAt(0) + highlight.fromRange(range) + + setFloatingElement(FloatingElementType.AI_POPOVER, { + range + }) + } + + // * AI functions + const handleAIQuery = async (type: SupportedAIEventTypes, callback: any) => { + performAIAction(type).then((res) => { + callback(res) + }) + } + + const getAIMenuItems = () => { + return [ + getMenuItem( + 'Continue', + (c) => handleAIQuery(SupportedAIEventTypes.EXPAND, c), + false, + getMIcon('ICON', 'system-uicons:write') + ), + getMenuItem( + 'Explain', + (c) => handleAIQuery(SupportedAIEventTypes.EXPLAIN, c), + false, + getMIcon('ICON', 'ri:question-line') + ), + getMenuItem('Summarize', (c) => handleAIQuery(SupportedAIEventTypes.SUMMARIZE, c), false, DefaultMIcons.AI), + getMenuItem( + 'Actionable', + (c) => handleAIQuery(SupportedAIEventTypes.ACTIONABLE, c), + false, + getMIcon('ICON', 'ic:round-view-list') + ) + ] + } + + const aiRequestHandler = async (reqData: Record, onSuccess: (res) => void) => { + const inExtension = isExtension() + const workspaceId = useAuthStore.getState().getWorkspaceId() + + if (inExtension) { + const res = await chrome.runtime.sendMessage({ type: 'PERFORM_AI_ACTION', data: reqData, workspaceId }) + onSuccess(res) + } else { + const res = await API.ai.perform(reqData) + onSuccess(res) + } + } + + const getUserQuery = (type: SupportedAIEventTypes, content?: string) => { + const aiEventsHistory = useHistoryStore.getState().ai + const userQuery: AIEvent = { + role: 'user', + type + } + + if (content) { + userQuery.content = content + } + + const context = [...aiEventsHistory.flat().filter((item) => item), userQuery] + + return { + userQuery, + context + } + } + + const performAIAction = async (type: SupportedAIEventTypes, content?: string): Promise => { + const { context, userQuery } = getUserQuery(type, content) + + try { + await aiRequestHandler({ context }, (res) => { + if (res?.content) { + addInAIEventsHistory(userQuery, res) + } + }) + } catch (err) { + // * Write cute error message + toast('Something went wrong!') + console.error('Unable to perform AI action', err) + } + } + + return { + performAIAction, + handleOpenAIPreview, + getAIMenuItems + } +} diff --git a/libs/shared/src/Style/Buttons.tsx b/libs/shared/src/Style/Buttons.tsx index 57948c154..3b58420b9 100644 --- a/libs/shared/src/Style/Buttons.tsx +++ b/libs/shared/src/Style/Buttons.tsx @@ -1,6 +1,12 @@ +import styled from 'styled-components' +import { Button } from '@workduck-io/mex-components' +import { BodyFont } from './Search' +export const StyledButton = styled(Button)` + ${BodyFont} +` // export interface ButtonProps extends React.HTMLProps { // primary?: boolean @@ -204,7 +210,6 @@ // ` // : ''} - // ${({ theme, primary }) => css` // &:hover { // box-shadow: 0px 6px 12px ${transparentize(0.5, primary ? theme.colors.primary : theme.colors.palette.black)}; @@ -267,7 +272,6 @@ // ` // : ''} - // ${({ theme, primary }) => css` // &:hover { // box-shadow: 0px 6px 12px ${transparentize(0.5, primary ? theme.colors.primary : theme.colors.palette.black)}; diff --git a/apps/extension/src/Utils/deserialize.ts b/libs/shared/src/Utils/deserialize.ts similarity index 100% rename from apps/extension/src/Utils/deserialize.ts rename to libs/shared/src/Utils/deserialize.ts index 903c19e9d..6fc3b6d6b 100644 --- a/apps/extension/src/Utils/deserialize.ts +++ b/libs/shared/src/Utils/deserialize.ts @@ -7,6 +7,24 @@ import { BlockType, generateTempId, mog, NodeEditorContent, updateIds } from '@m const isInlineNode = (editor: Pick) => (node: Descendant) => Text.isText(node) || editor.isInline(node) +export const highlightNodes = (blockToHighlight: BlockType, highlight?: boolean) => { + // * if show is true add highlight else remove highlight from nested obj + const block = Object.assign({}, blockToHighlight) + + if (highlight) { + block['highlight'] = true + } else delete block['highlight'] + + return block +} + +export const getMexHTMLDeserializer = (HTMLContent: string, editor: any) => { + const element = htmlStringToDOMNode(HTMLContent ?? '') + const nodes = editor ? htmlBodyToFragment(editor, element) : undefined + + return nodes +} + export const getDeserializeSelectionToNodes = ( selection: { text: string; metadata: string }, editor: any, @@ -42,21 +60,3 @@ export const getDeserializeSelectionToNodes = ( return nodes } - -export const highlightNodes = (blockToHighlight: BlockType, highlight?: boolean) => { - // * if show is true add highlight else remove highlight from nested obj - const block = Object.assign({}, blockToHighlight) - - if (highlight) { - block['highlight'] = true - } else delete block['highlight'] - - return block -} - -export const getMexHTMLDeserializer = (HTMLContent: string, editor: any) => { - const element = htmlStringToDOMNode(HTMLContent ?? '') - const nodes = editor ? htmlBodyToFragment(editor, element) : undefined - - return nodes -} diff --git a/apps/webapp/src/Editor/utils.ts b/libs/shared/src/Utils/utils.ts similarity index 98% rename from apps/webapp/src/Editor/utils.ts rename to libs/shared/src/Utils/utils.ts index 9d7fe0825..c69c23de1 100644 --- a/apps/webapp/src/Editor/utils.ts +++ b/libs/shared/src/Utils/utils.ts @@ -65,7 +65,7 @@ type BlockType = { children: Array } -export default function parseToMarkdown(chunk: any, ignoreParagraphNewline = false, listDepth = 0) { +export function parseToMarkdown(chunk: any, ignoreParagraphNewline = false, listDepth = 0) { const text = chunk.text || '' let type = chunk.type || '' diff --git a/libs/shared/src/index.ts b/libs/shared/src/index.ts index 3af078168..5fe60577a 100644 --- a/libs/shared/src/index.ts +++ b/libs/shared/src/index.ts @@ -22,6 +22,7 @@ export * from './Components/TagLabel' export * from './Components/ToggleButton' export * from './Components/Tooltips' export * from './Hooks/Helpers' +export * from './Hooks/useAIOptions' export * from './Hooks/useBalloonToolbarPopper' export * from './Hooks/useEditorActions' export * from './Hooks/useQuery' @@ -33,6 +34,7 @@ export * from './Types/SearchEntities' export * from './Style/Archive' export * from './Style/BalloonToolbar.styles' // export * from './Style/Buttons' +export * from './Components/AIPreview/Floater' export * from './Components/FeatureFlag' export * from './Style/Card' export * from './Style/Collapse' @@ -97,6 +99,7 @@ export * from './Types/Theme' export * from './Types/Tree' export * from './Utils/blurSelection' export * from './Utils/defaultText' +export * from './Utils/deserialize' export * from './Utils/events' export * from './Utils/helpers' export * from './Utils/icons' @@ -108,3 +111,4 @@ export * from './Utils/tabInfo' // export * from './Utils/themeGenerator' export * from './Utils/uploadToCDN' export * from './Utils/upsertLinkAtSelection' +export * from './Utils/utils' From 26ec10eda11824be2491e3cb337c4c3f31b84724 Mon Sep 17 00:00:00 2001 From: Dinesh Singh Date: Sat, 15 Apr 2023 15:48:33 +0530 Subject: [PATCH 2/4] Remove logs --- apps/extension/src/background.ts | 3 +-- .../src/Components/AIPreview/Floater.tsx | 4 +--- .../shared/src/Components/AIPreview/index.tsx | 22 +------------------ 3 files changed, 3 insertions(+), 26 deletions(-) diff --git a/apps/extension/src/background.ts b/apps/extension/src/background.ts index 4edbeadda..8af95b0a6 100644 --- a/apps/extension/src/background.ts +++ b/apps/extension/src/background.ts @@ -90,7 +90,7 @@ chrome.contextMenus.create({ chrome.contextMenus.create({ id: 'open-ai-tools', - title: 'Perform AI enrichment', + title: 'Enhance with AI', contexts: ['page', 'selection'] }) @@ -258,7 +258,6 @@ chrome.notifications.onClosed.addListener((notificationId, byUser) => { chrome.omnibox.onInputChanged.addListener((text, suggest) => { const workspaceDetails = useAuthStore.getState().workspaceDetails const linkCaptures = useLinkStore.getState().links?.filter((item) => item.alias) ?? [] - console.log('captures', { linkCaptures }) const suggestions = fuzzySearch(linkCaptures, text, (item) => item.alias).map((item) => { return { content: `${API_BASE_URLS.url}/${workspaceDetails.id}/${item.alias}`, diff --git a/libs/shared/src/Components/AIPreview/Floater.tsx b/libs/shared/src/Components/AIPreview/Floater.tsx index 3c286f536..f3d672031 100644 --- a/libs/shared/src/Components/AIPreview/Floater.tsx +++ b/libs/shared/src/Components/AIPreview/Floater.tsx @@ -4,7 +4,6 @@ import { RemoveScroll } from 'react-remove-scroll' import { arrow, - autoUpdate, flip, FloatingArrow, FloatingFocusManager, @@ -56,8 +55,7 @@ const DefaultFloater: React.FC = ({ onClose, root, ...props element: arrowRef, padding: 10 }) - ], - whileElementsMounted: autoUpdate + ] }) const click = useClick(context) diff --git a/libs/shared/src/Components/AIPreview/index.tsx b/libs/shared/src/Components/AIPreview/index.tsx index 56d8d84b6..e1ab4c297 100644 --- a/libs/shared/src/Components/AIPreview/index.tsx +++ b/libs/shared/src/Components/AIPreview/index.tsx @@ -1,17 +1,6 @@ import React, { useEffect, useMemo } from 'react' -import { - deserializeMd, - focusEditor, - getEdgePoints, - getEndPoint, - getPlateEditorRef, - getPointAfter, - getPointBefore, - getPointBeforeLocation, - getStartPoint, - insertNodes -} from '@udecode/plate' +import { deserializeMd, focusEditor, getPlateEditorRef, getPointAfter, insertNodes } from '@udecode/plate' import Highlighter from 'web-highlighter' import { IconButton } from '@workduck-io/mex-components' @@ -62,15 +51,6 @@ const AIPreviewContainer: React.FC = (props) => { const deserializedContent = getContent(content) if (Array.isArray(deserializedContent) && deserializedContent.length > 0) { - console.log('EDGES', { - selection: editor.selection, - getPointAfter: getPointAfter(editor, editor.selection), - getPointBefore: getPointBefore(editor, editor.selection), - getEdgePoints: getEdgePoints(editor, editor.selection), - getEndPoint: getEndPoint(editor, editor.selection), - getStartPoint: getStartPoint(editor, editor.selection), - point: getPointBeforeLocation(editor, editor.selection) - }) const at = replace ? editor?.selection : getPointAfter(editor, editor.selection) insertNodes(editor, deserializedContent, { From df623a60a75899805cee0e234c7b01f6d2409044 Mon Sep 17 00:00:00 2001 From: Dinesh Singh Date: Sat, 15 Apr 2023 16:29:18 +0530 Subject: [PATCH 3/4] changeset added --- .changeset/gentle-hairs-change.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/gentle-hairs-change.md diff --git a/.changeset/gentle-hairs-change.md b/.changeset/gentle-hairs-change.md new file mode 100644 index 000000000..9ae5a9210 --- /dev/null +++ b/.changeset/gentle-hairs-change.md @@ -0,0 +1,6 @@ +--- +'mexit': patch +'mexit-webapp': patch +--- + +AI-powered actions in extension, Editable query selection From ac7d7cac2425a0d82a59c8052c351195c1ba8ddb Mon Sep 17 00:00:00 2001 From: Dinesh Singh Date: Sat, 15 Apr 2023 17:03:07 +0530 Subject: [PATCH 4/4] Added ID in AI Preview --- apps/webapp/src/Components/Modals/Lookup.tsx | 4 +++- apps/webapp/src/Editor/AIPreviewContainer.tsx | 4 ++-- apps/webapp/src/Editor/MexEditor.tsx | 2 +- libs/core/src/Stores/modal.store.ts | 6 +++--- libs/shared/src/Components/AIPreview/AIResponse.tsx | 1 + libs/shared/src/Components/AIPreview/index.tsx | 2 +- libs/shared/src/Components/AIPreview/styled.tsx | 2 +- libs/shared/src/Components/AIPreview/types.ts | 1 + 8 files changed, 13 insertions(+), 9 deletions(-) diff --git a/apps/webapp/src/Components/Modals/Lookup.tsx b/apps/webapp/src/Components/Modals/Lookup.tsx index 6e1bb2564..744a4df8a 100644 --- a/apps/webapp/src/Components/Modals/Lookup.tsx +++ b/apps/webapp/src/Components/Modals/Lookup.tsx @@ -5,7 +5,7 @@ import styled from 'styled-components' import { tinykeys } from '@workduck-io/tinykeys' -import { QuickLinkType, useHelpStore, useSnippetStore } from '@mexit/core' +import { QuickLinkType, useFloatingStore, useHelpStore, useSnippetStore } from '@mexit/core' import { blurEditableElement, Input, StyledCombobox, StyledInputWrapper } from '@mexit/shared' import { useKeyListener } from '../../Hooks/useChangeShortcutListener' @@ -50,6 +50,7 @@ const InputWrapper = styled.div` const Lookup = () => { const [open, setOpen] = useState(false) + const setFloatingElement = useFloatingStore((s) => s.setFloatingElement) const loadSnippet = useSnippetStore((store) => store.loadSnippet) const { goTo, location } = useRouting() @@ -58,6 +59,7 @@ const Lookup = () => { const openModal = () => { setOpen(true) + setFloatingElement(undefined) } const closeModal = () => { diff --git a/apps/webapp/src/Editor/AIPreviewContainer.tsx b/apps/webapp/src/Editor/AIPreviewContainer.tsx index 969b89a7e..20af62f91 100644 --- a/apps/webapp/src/Editor/AIPreviewContainer.tsx +++ b/apps/webapp/src/Editor/AIPreviewContainer.tsx @@ -5,11 +5,11 @@ import { useCreateNewMenu } from '../Hooks/useCreateNewMenu' import components from './Components/EditorPreviewComponents' import { generateEditorPluginsWithComponents } from './Plugins' -const AIPreviewContainer = () => { +const AIPreviewContainer = ({ id }) => { const { getAIMenuItems } = useCreateNewMenu() const plugins = generateEditorPluginsWithComponents(components, { exclude: { dnd: true } }) - return + return } export default AIPreviewContainer diff --git a/apps/webapp/src/Editor/MexEditor.tsx b/apps/webapp/src/Editor/MexEditor.tsx index be14f1815..6f9e552e1 100644 --- a/apps/webapp/src/Editor/MexEditor.tsx +++ b/apps/webapp/src/Editor/MexEditor.tsx @@ -115,7 +115,7 @@ export const MexEditorBase = (props: MexEditorProps) => { {props.options?.withGlobalListener !== false && } {props.debug &&
{JSON.stringify(content, null, 2)}
} - + ) } diff --git a/libs/core/src/Stores/modal.store.ts b/libs/core/src/Stores/modal.store.ts index ab81875c9..6edb09360 100644 --- a/libs/core/src/Stores/modal.store.ts +++ b/libs/core/src/Stores/modal.store.ts @@ -1,5 +1,5 @@ -import { StoreIdentifier } from "../Types/Store" -import { createStore } from "../Utils/storeCreator" +import { StoreIdentifier } from '../Types/Store' +import { createStore } from '../Utils/storeCreator' export enum ModalsType { blocks, @@ -37,4 +37,4 @@ const modalStoreConfig = (set, get) => ({ } }) -export const useModalStore = createStore(modalStoreConfig, StoreIdentifier.MODAL, false) \ No newline at end of file +export const useModalStore = createStore(modalStoreConfig, StoreIdentifier.MODAL, false) diff --git a/libs/shared/src/Components/AIPreview/AIResponse.tsx b/libs/shared/src/Components/AIPreview/AIResponse.tsx index 478b09cc2..a1b071f86 100644 --- a/libs/shared/src/Components/AIPreview/AIResponse.tsx +++ b/libs/shared/src/Components/AIPreview/AIResponse.tsx @@ -55,6 +55,7 @@ const AIResponse = ({ aiResponse, index, plugins }) => { onChange(e)} diff --git a/libs/shared/src/Components/AIPreview/index.tsx b/libs/shared/src/Components/AIPreview/index.tsx index e1ab4c297..60043ae26 100644 --- a/libs/shared/src/Components/AIPreview/index.tsx +++ b/libs/shared/src/Components/AIPreview/index.tsx @@ -47,7 +47,7 @@ const AIPreviewContainer: React.FC = (props) => { } const insertContent = (content: string, replace = true) => { - const editor = getPlateEditorRef() + const editor = getPlateEditorRef(props.id) const deserializedContent = getContent(content) if (Array.isArray(deserializedContent) && deserializedContent.length > 0) { diff --git a/libs/shared/src/Components/AIPreview/styled.tsx b/libs/shared/src/Components/AIPreview/styled.tsx index 9d434d200..d5978fbe8 100644 --- a/libs/shared/src/Components/AIPreview/styled.tsx +++ b/libs/shared/src/Components/AIPreview/styled.tsx @@ -107,7 +107,7 @@ export const FloaterContainer = styled.div` backdrop-filter: blur(2rem); border: 1px solid ${({ theme }) => theme.tokens.surfaces.app}; transform-origin: top; - z-index: 11; + z-index: 9999; border: 1px solid ${({ theme }) => theme.tokens.surfaces.s[3]}; animation: ${float} 150ms ease-out; ` diff --git a/libs/shared/src/Components/AIPreview/types.ts b/libs/shared/src/Components/AIPreview/types.ts index fc1e29bd3..f60dbf2a0 100644 --- a/libs/shared/src/Components/AIPreview/types.ts +++ b/libs/shared/src/Components/AIPreview/types.ts @@ -1,6 +1,7 @@ import { MenuListItemType, NodeEditorContent } from '@mexit/core' export interface AIPreviewProps { + id?: string insertInNote?: boolean onInsert?: (content: NodeEditorContent, noteId?: string) => void allowReplace?: boolean