Skip to content

Commit c89e5cd

Browse files
authored
Perform AI On Selection In Extension, Allow Editing Of Query Content (#412)
#412 * Allow selection editing, Perform AI actions on selection in extension * Remove logs * changeset added * Added ID in AI Preview
1 parent 915be01 commit c89e5cd

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

59 files changed

+770
-294
lines changed

.changeset/gentle-hairs-change.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'mexit': patch
3+
'mexit-webapp': patch
4+
---
5+
6+
AI-powered actions in extension, Editable query selection
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { useMemo } from 'react'
2+
3+
import { NodeEditorContent } from '@mexit/core'
4+
import { AIPreview, useAIOptions } from '@mexit/shared'
5+
6+
import { generateEditorPluginsWithComponents } from '../Editor/plugins'
7+
import { useSaveChanges } from '../Hooks/useSaveChanges'
8+
import { getElementById } from '../Utils/cs-utils'
9+
10+
import components from './Editor/EditorPreviewComponents'
11+
12+
const AIPreviewContainer = () => {
13+
const { appendAndSave } = useSaveChanges()
14+
const { getAIMenuItems } = useAIOptions()
15+
16+
const handleOnInsert = (content: NodeEditorContent, nodeId: string) => {
17+
appendAndSave({ nodeid: nodeId, content })
18+
}
19+
20+
const plugins = useMemo(
21+
() =>
22+
generateEditorPluginsWithComponents(components, {
23+
exclude: {
24+
dnd: true
25+
}
26+
}),
27+
[]
28+
)
29+
30+
return (
31+
<AIPreview
32+
plugins={plugins}
33+
insertInNote
34+
onInsert={handleOnInsert}
35+
getDefaultItems={getAIMenuItems}
36+
root={getElementById('mexit-container')}
37+
/>
38+
)
39+
}
40+
41+
export default AIPreviewContainer

apps/extension/src/Components/Content/index.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,14 @@ import React, { useEffect, useState } from 'react'
33
import { createPlateEditor, createPlateUI } from '@udecode/plate'
44

55
import { ELEMENT_TAG, getDefaultContent, NodeEditorContent, QuickLinkType, useContentStore } from '@mexit/core'
6+
import { getDeserializeSelectionToNodes } from '@mexit/shared'
67

78
import { CopyTag } from '../../Editor/components/Tags/CopyTag'
89
import { generateEditorPluginsWithComponents } from '../../Editor/plugins/index'
910
import { useEditorStore } from '../../Hooks/useEditorStore'
1011
import { useSnippets } from '../../Hooks/useSnippets'
1112
import { useSputlitContext } from '../../Hooks/useSputlitContext'
1213
import { useSputlitStore } from '../../Stores/useSputlitStore'
13-
import { getDeserializeSelectionToNodes } from '../../Utils/deserialize'
1414
import Results from '../Results'
1515

1616
import { StyledContent } from './styled'

apps/extension/src/Components/InternalEvents.tsx

+30-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,14 @@ import * as Sentry from '@sentry/react'
66
import mixpanel from 'mixpanel-browser'
77
import Highlighter from 'web-highlighter'
88

9-
import { API_BASE_URLS, mog, useHighlightStore } from '@mexit/core'
9+
import {
10+
API_BASE_URLS,
11+
FloatingElementType,
12+
mog,
13+
useFloatingStore,
14+
useHighlightStore,
15+
useHistoryStore
16+
} from '@mexit/core'
1017
import { getScrollbarWidth, isInputField } from '@mexit/shared'
1118

1219
import { useEditorStore } from '../Hooks/useEditorStore'
@@ -50,6 +57,8 @@ function useToggleHandler() {
5057
const { previewMode, setPreviewMode } = useEditorStore()
5158
const setTooltipState = useSputlitStore((s) => s.setHighlightTooltipState)
5259
const resetSputlitState = useSputlitStore((s) => s.reset)
60+
const addAIEvent = useHistoryStore((store) => store.addInitialEvent)
61+
const setFloatingElement = useFloatingStore((s) => s.setFloatingElement)
5362

5463
const timeoutRef = useRef<Timeout>()
5564
const runAnimateTimer = useCallback((vs: VisualState.animatingIn | VisualState.animatingOut) => {
@@ -71,6 +80,22 @@ function useToggleHandler() {
7180
}, ms)
7281
}, [])
7382

83+
const handleOpenAIPreview = (highlighter) => {
84+
const { html } = getSelectionHTML()
85+
const range = window.getSelection()?.getRangeAt(0)
86+
const content = sanitizeHTML(html)
87+
88+
if (content) {
89+
addAIEvent({ role: 'assistant', content, inputFormat: 'html' })
90+
91+
highlighter.fromRange(range)
92+
93+
setFloatingElement(FloatingElementType.AI_POPOVER, {
94+
range
95+
})
96+
}
97+
}
98+
7499
useEffect(() => {
75100
function messageHandler(request: any, sender: chrome.runtime.MessageSender, sendResponse: (response: any) => void) {
76101
const highlighter = new Highlighter({ style: { className: 'mexit-highlight' } })
@@ -94,6 +119,10 @@ function useToggleHandler() {
94119
setVisualState(VisualState.animatingOut)
95120
}
96121
sendResponse(true)
122+
break
123+
case 'open-ai-tools':
124+
handleOpenAIPreview(highlighter)
125+
sendResponse(true)
97126
}
98127
}
99128

apps/extension/src/Editor/plugins/index.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ export const generatePlugins = (options: PluginOptionType) => {
9898
// Special Elements
9999
createImagePlugin({
100100
options: {
101-
uploadImage: options.uploadImage
101+
uploadImage: options?.uploadImage
102102
}
103103
}),
104104
createLinkPlugin(), // Link
@@ -192,7 +192,7 @@ export const useMemoizedPlugins = (components: Record<string, any>, options?: Pl
192192
const plugins = createPlugins(
193193
generatePlugins({
194194
...options,
195-
uploadImage: options.uploadImage ?? uploadImageToWDCDN
195+
uploadImage: options?.uploadImage ?? uploadImageToWDCDN
196196
}),
197197
{
198198
components: wrappedComponents

apps/extension/src/Hooks/useSaveChanges.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export interface AppendAndSaveProps {
3737

3838
export function useSaveChanges() {
3939
const workspaceDetails = useAuthStore((store) => store.workspaceDetails)
40-
const { setPreviewMode, setNodeContent } = useEditorStore()
40+
const { setPreviewMode } = useEditorStore()
4141
const { getParentILink, getEntirePathILinks, updateMultipleILinks, updateSingleILink, createNoteHierarchyString } =
4242
useInternalLinks()
4343
const { updateDocument, updateBlocks } = useSearch()

apps/extension/src/Styles/GlobalStyle.ts

+13-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { createGlobalStyle } from 'styled-components'
22

3-
import { customStyles, EditorBalloonStyles, normalize, ThinScrollbar,TippyBalloonStyles } from '@mexit/shared'
3+
import { customStyles, EditorBalloonStyles, normalize, ThinScrollbar, TippyBalloonStyles } from '@mexit/shared'
44

55
export const GlobalStyle = createGlobalStyle`
66
@@ -36,7 +36,14 @@ export const GlobalStyle = createGlobalStyle`
3636
display: none;
3737
}
3838
39-
#sputlit-container, #dibba-container, #mexit-tooltip, #ext-side-nav, #notif {
39+
.highlight {
40+
color: ${({ theme }) => theme.tokens.text.heading};
41+
background: ${({ theme }) => `rgba(${theme.rgbTokens.colors.primary.default}, 0.4)`};
42+
}
43+
44+
45+
46+
#sputlit-container, #dibba-container, #ai-preview, #mexit-tooltip, #ext-side-nav, #notif, #mexit-ai-performer {
4047
${normalize}; // NormalizeCSS normalization
4148
letter-spacing: normal;
4249
font-family: "Inter", sans-serif;
@@ -66,4 +73,8 @@ export const GlobalStyle = createGlobalStyle`
6673
${({ theme }) => theme.custom && customStyles[theme.custom]}
6774
}
6875
76+
#mexit-ai-performer {
77+
font-size: 14px;
78+
}
79+
6980
`

apps/extension/src/Utils/getSelectionHTML.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,13 @@ export function getSelectionHTML() {
88
url = selection?.anchorNode.baseURI
99

1010
const container = document.createElement('div')
11+
1112
for (let i = 0, len = selection.rangeCount; i < len; ++i) {
1213
const t = selection.getRangeAt(i).cloneContents()
1314
container.appendChild(t)
1415
range = selection.getRangeAt(i)
1516
}
16-
console.log('Container: ', container)
17+
1718
html = container.innerHTML
1819
}
1920
}

apps/extension/src/Utils/requestHandler.ts

+18-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { apiURLs, DEFAULT_NAMESPACE, defaultContent, ListItemType, mog } from '@mexit/core'
1+
import { AIEvent, apiURLs, DEFAULT_NAMESPACE, defaultContent, ListItemType, mog } from '@mexit/core'
22

33
import { Tab } from '../Types/Tabs'
44

@@ -71,6 +71,23 @@ export const handleCaptureRequest = ({ subType, data }) => {
7171
}
7272
}
7373

74+
export const handlePerformAIRequest = async ({ data, workspaceId }) => {
75+
return await client
76+
.post(apiURLs.openAi.perform, {
77+
json: data,
78+
headers: {
79+
'mex-workspace-id': workspaceId
80+
}
81+
})
82+
.json()
83+
.then((event: AIEvent) => {
84+
return { message: event, error: null }
85+
})
86+
.catch((error) => {
87+
return { message: null, error: error }
88+
})
89+
}
90+
7491
export const handleSnippetRequest = ({ data }) => {
7592
const reqData = {
7693
id: data.id,

apps/extension/src/app.tsx

+16
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
1+
import React from 'react'
2+
import { createPortal } from 'react-dom'
3+
14
import { useAuthStore } from '@mexit/core'
25

6+
import AIPreviewContainer from './Components/AIPreview'
37
import Dibba from './Components/Dibba'
48
import { DibbaPortal } from './Components/Dibba/DibbaPortal'
59
import { InternalEvents } from './Components/InternalEvents'
@@ -11,6 +15,15 @@ import Tooltip from './Components/Tooltip'
1115
import { TooltipPortal } from './Components/Tooltip/TooltipPortal'
1216
import { HighlighterProvider } from './Hooks/useHighlighterContext'
1317
import { SputlitProvider } from './Hooks/useSputlitContext'
18+
import { styleSlot } from './Utils/cs-utils'
19+
20+
interface Props {
21+
children: React.ReactNode
22+
}
23+
24+
export function AIPreviewPortal(props: Props) {
25+
return createPortal(props.children, styleSlot)
26+
}
1427

1528
const Extension = () => {
1629
const authenticated = useAuthStore((a) => a.authenticated)
@@ -28,6 +41,9 @@ const Extension = () => {
2841
<TooltipPortal>
2942
<Tooltip />
3043
</TooltipPortal>
44+
<AIPreviewPortal>
45+
<AIPreviewContainer />
46+
</AIPreviewPortal>
3147
</>
3248
)}
3349

apps/extension/src/background.ts

+17-4
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
handleCaptureRequest,
1212
handleHighlightRequest,
1313
handleNodeContentRequest,
14+
handlePerformAIRequest,
1415
handleSharingRequest,
1516
handleShortenerRequest,
1617
handleSnippetRequest
@@ -82,16 +83,22 @@ chrome.action.onClicked.addListener((command) => {
8283
})
8384

8485
chrome.contextMenus.create({
85-
id: 'open-sputlit',
86+
id: 'sputlit',
8687
title: 'Open Sputlit',
8788
contexts: ['page', 'selection']
8889
})
8990

90-
chrome.contextMenus.onClicked.addListener((onClickData) => {
91+
chrome.contextMenus.create({
92+
id: 'open-ai-tools',
93+
title: 'Enhance with AI',
94+
contexts: ['page', 'selection']
95+
})
96+
97+
chrome.contextMenus.onClicked.addListener((info) => {
9198
chrome.tabs?.query({ active: true, currentWindow: true }, (tabs) => {
9299
const tabId = tabs[0].id
93100

94-
chrome.tabs.sendMessage(tabId, { type: 'sputlit' }, (response) => {
101+
chrome.tabs.sendMessage(tabId, { type: info?.menuItemId }, (response) => {
95102
handleResponseCallback(tabId, response)
96103
})
97104
})
@@ -198,6 +205,13 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
198205
return true
199206
}
200207

208+
case 'PERFORM_AI_ACTION': {
209+
handlePerformAIRequest(request).then((res) => {
210+
sendResponse(res?.message)
211+
})
212+
return true
213+
}
214+
201215
default: {
202216
return true
203217
}
@@ -244,7 +258,6 @@ chrome.notifications.onClosed.addListener((notificationId, byUser) => {
244258
chrome.omnibox.onInputChanged.addListener((text, suggest) => {
245259
const workspaceDetails = useAuthStore.getState().workspaceDetails
246260
const linkCaptures = useLinkStore.getState().links?.filter((item) => item.alias) ?? []
247-
248261
const suggestions = fuzzySearch(linkCaptures, text, (item) => item.alias).map((item) => {
249262
return {
250263
content: `${API_BASE_URLS.url}/${workspaceDetails.id}/${item.alias}`,

apps/webapp/src/Components/CreateTodoModal/TaskEditor/index.tsx

+3-2
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,10 @@ type TaskEditorType = {
2020
content: NodeEditorContent
2121
readOnly?: boolean
2222
onChange?: (val: any) => void
23+
withCombobox?: boolean
2324
}
2425

25-
const TaskEditor = ({ editorId, readOnly, content, onChange }: TaskEditorType) => {
26+
const TaskEditor = ({ editorId, readOnly, content, onChange, withCombobox = true }: TaskEditorType) => {
2627
const config = useEditorPluginConfig(editorId)
2728
const { uploadImageToS3 } = useAuth()
2829
const { uploadImageToWDCDN } = useUploadToCDN(uploadImageToS3)
@@ -66,7 +67,7 @@ const TaskEditor = ({ editorId, readOnly, content, onChange }: TaskEditorType) =
6667
onChange={onChangeContent}
6768
editableProps={editableProps}
6869
>
69-
<MultiComboboxContainer config={config.onKeyDownConfig} />
70+
{withCombobox && <MultiComboboxContainer config={config.onKeyDownConfig} />}
7071
</Plate>
7172
)
7273
}

apps/webapp/src/Components/CreateTodoModal/TaskEditor/plugins.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ import { optionsCreateNodeIdPlugin, optionsSelectOnBackspacePlugin } from '../..
4747
import { parseTwitterUrl } from '../../../Editor/Plugins/parseTwitterUrl'
4848
import Todo from '../../Todo'
4949

50-
const generateTodoPlugins = (uploadImage?: UploadImageFn) => {
50+
const generateTodoPlugins = (uploadImage: UploadImageFn, inline?: boolean) => {
5151
return [
5252
// elements
5353
createParagraphPlugin(), // paragraph element
@@ -106,7 +106,7 @@ const generateTodoPlugins = (uploadImage?: UploadImageFn) => {
106106
createMentionPlugin(), // Mentions
107107
createILinkPlugin(), // Internal Links ILinks
108108
createInlineBlockPlugin(),
109-
createSingleLinePlugin()
109+
inline && createSingleLinePlugin()
110110
]
111111
}
112112

@@ -129,7 +129,7 @@ export const getComponents = () =>
129129
[ELEMENT_MEDIA_EMBED]: MediaEmbedElement as any
130130
})
131131

132-
export const getTodoPlugins = (uploadImage?: UploadImageFn) => {
133-
const plugins = createPlugins(generateTodoPlugins(uploadImage), { components: getComponents() })
132+
export const getTodoPlugins = (uploadImage: UploadImageFn, isInline = true) => {
133+
const plugins = createPlugins(generateTodoPlugins(uploadImage, isInline), { components: getComponents() })
134134
return plugins
135135
}

apps/webapp/src/Components/Editor/BalloonToolbar/EditorBalloonToolbar.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ const BallonMarkToolbarButtons = () => {
113113
<>
114114
<ToolbarButton
115115
tooltip={{ content: 'Ask AI anything...', ...tooltip }}
116-
icon={<IconDisplay size={20} icon={DefaultMIcons.AI} />}
116+
icon={<IconDisplay color={theme.tokens.colors.primary.hover} size={20} icon={DefaultMIcons.AI} />}
117117
onMouseDown={handleOpenAIPreview}
118118
/>
119119
<ButtonSeparator />

apps/webapp/src/Components/Editor/Plateless.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -264,11 +264,11 @@ const RenderPlateless = React.memo<RenderPlatelessProps>(
264264
({ content, typeMap, multiline = false }: RenderPlatelessProps) => {
265265
const childrenRender =
266266
content &&
267-
content.map((node) => {
267+
content.map((node, i) => {
268268
if (Object.keys(typeMap).includes(node?.type)) {
269269
const RenderItem = typeMap[node?.type]
270270
return (
271-
<RenderItem node={node}>
271+
<RenderItem key={`${node?.type}-${i}`} node={node}>
272272
<RenderPlateless typeMap={typeMap} content={node.children} multiline={multiline} />
273273
</RenderItem>
274274
)

0 commit comments

Comments
 (0)