Skip to content

Commit 4639e33

Browse files
committed
fix(context-menu): add Firefox-specific context menu types and functionality
- Introduced a new module `wxt/browser` with types for Firefox context menus. - Implemented utility functions for base64 encoding/decoding and image handling. - Updated context menu management to support asynchronous operations and improved error handling. - Enhanced user prompt handling to support images alongside text. - Added new user configuration options for chat with images. - Refactored selection bounding rectangle calculations to improve accuracy. - Updated web request rules to handle both HTTP and HTTPS protocols. - Adjusted permissions in the configuration for Firefox compatibility.
1 parent 727cd19 commit 4639e33

File tree

31 files changed

+1059
-114
lines changed

31 files changed

+1059
-114
lines changed

entrypoints/background/index.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,21 @@ export default defineBackground(() => {
8787
logger.debug('Extension is suspending')
8888
})
8989

90+
if (import.meta.env.FIREFOX) {
91+
// In Chrome extensions, selection and page type context menus are mutually exclusive, so we don't need to handle onShown event
92+
// In Firefox, selection and page type context menus can coexist, so we need to handle onShown event
93+
// The logic here is: if the current context menu is selection type and text is selected, don't show the translate page context menu
94+
// If the current context menu is page type, show the translate page context menu
95+
// This prevents the translate page context menu from appearing when text is selected
96+
browser.menus.onShown.addListener(async (info) => {
97+
const shouldShowTranslateMenu = !(info.contexts.includes(browser.contextMenus.ContextType.SELECTION) && info.selectionText)
98+
const instance = await ContextMenuManager.getInstance()
99+
await instance.updateContextMenu(CONTEXT_MENU_ITEM_TRANSLATE_PAGE.id, {
100+
visible: shouldShowTranslateMenu,
101+
})
102+
})
103+
}
104+
90105
browser.runtime.onInstalled.addListener(async () => {
91106
ContextMenuManager.getInstance().then((instance) => {
92107
for (const menu of CONTEXT_MENU) {

entrypoints/content/components/Chat/index.vue

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@
5252
<div>
5353
<TabSelector v-model:selectedTabs="contextTabs" />
5454
</div>
55+
<div v-if="enableChatWithImage">
56+
<ImageSelector v-model:selectedImages="contextImages" />
57+
</div>
5558
<div class="flex gap-1 relative">
5659
<ScrollContainer
5760
class="max-h-72 grow shadow-02 bg-white rounded-md overflow-hidden"
@@ -118,8 +121,10 @@ import {
118121
initChatSideEffects,
119122
} from '@/entrypoints/content/utils/chat/index'
120123
import { useI18n } from '@/utils/i18n'
124+
import { getUserConfig } from '@/utils/user-config'
121125
122126
import { showSettings } from '../../utils/settings'
127+
import ImageSelector from '../ImageSelector.vue'
123128
import MarkdownViewer from '../MarkdownViewer.vue'
124129
import TabSelector from '../TabSelector.vue'
125130
import MessageAction from './Messages/Action.vue'
@@ -137,9 +142,13 @@ const isComposing = ref(false)
137142
const chat = await Chat.getInstance()
138143
const scrollContainerRef = ref<InstanceType<typeof ScrollContainer>>()
139144
const contextTabs = chat.contextTabs
145+
const contextImages = chat.contextImages
140146
141147
initChatSideEffects()
142148
149+
const userConfig = await getUserConfig()
150+
const enableChatWithImage = userConfig.chat.chatWithImage.enable.toRef()
151+
143152
const actionEventHandler = Chat.createActionEventHandler((actionEvent) => {
144153
if (actionEvent.action === 'customInput') {
145154
chat.ask((actionEvent as ActionEvent<'customInput'>).data.prompt)

entrypoints/content/components/DebugSettings/index.vue

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,37 @@
158158
</div>
159159
</div>
160160
</Block>
161+
<Block title="Chat">
162+
<div class="flex gap-3 justify-start items-center">
163+
<div>
164+
Chat with image
165+
</div>
166+
<Switch
167+
v-model="enableChatWithImage"
168+
slotClass="rounded-lg border-gray-200 border bg-white"
169+
itemClass="h-6 flex items-center justify-center text-xs px-2"
170+
thumbClass="bg-blue-500 rounded-md"
171+
activeItemClass="text-white"
172+
:items="[
173+
{
174+
label: 'Enable',
175+
key: true,
176+
},
177+
{
178+
label: 'Disable',
179+
key: false,
180+
activeThumbClass: 'bg-gray-200',
181+
},
182+
]"
183+
>
184+
<template #label="{ item }">
185+
<div class="flex p-2 items-center justify-center text-xs">
186+
{{ item.label }}
187+
</div>
188+
</template>
189+
</Switch>
190+
</div>
191+
</Block>
161192
<Block title="System Prompts">
162193
<div>
163194
<div class="flex flex-col gap-3 justify-start items-stretch">
@@ -397,6 +428,7 @@ const writingToolsRewritePrompt = userConfig.writingTools.rewrite.systemPrompt.t
397428
const writingToolsProofreadPrompt = userConfig.writingTools.proofread.systemPrompt.toRef()
398429
const writingToolsListPrompt = userConfig.writingTools.list.systemPrompt.toRef()
399430
const writingToolsSparklePrompt = userConfig.writingTools.sparkle.systemPrompt.toRef()
431+
const enableChatWithImage = userConfig.chat.chatWithImage.enable.toRef()
400432
const endpointType = userConfig.llm.endpointType.toRef()
401433
const localeInConfig = userConfig.locale.current.toRef()
402434
const translationSystemPromptError = ref('')
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
<template>
2+
<div
3+
class="relative"
4+
@click="chooseImages"
5+
>
6+
<Button
7+
class="p-1"
8+
variant="secondary"
9+
>
10+
choose images
11+
</Button>
12+
<div>
13+
<ScrollContainer
14+
ref="tabsContainerRef"
15+
class="shrink grow min-w-0"
16+
itemContainerClass="flex gap-2 w-max items-center"
17+
:redirect="{ vertical: 'horizontal', horizontal: 'horizontal' }"
18+
:arrivalShadow="{ left: { color: '#E9E9EC', size: 60 }, right: { color: '#E9E9EC', size: 60 } }"
19+
>
20+
<img
21+
v-for="(imageUrl, index) in base64Urls"
22+
:key="index"
23+
:src="imageUrl"
24+
class="w-10 h-10 object-cover rounded-md m-1"
25+
>
26+
</ScrollContainer>
27+
</div>
28+
</div>
29+
</template>
30+
31+
<script setup lang="ts">
32+
import { useFileDialog, useVModel } from '@vueuse/core'
33+
import { computed, watch } from 'vue'
34+
35+
import ScrollContainer from '@/components/ScrollContainer.vue'
36+
import Button from '@/components/ui/Button.vue'
37+
import { convertImageFileToJpegBase64 } from '@/utils/image'
38+
39+
type Base64 = string
40+
41+
interface ImageData {
42+
data: Base64
43+
type: string
44+
}
45+
46+
const props = defineProps<{
47+
selectedImages: ImageData[]
48+
}>()
49+
50+
const emit = defineEmits<{
51+
(e: 'update:selectedImages', tabs: ImageData[]): void
52+
}>()
53+
54+
const selectedImages = useVModel(props, 'selectedImages', emit)
55+
const { files, open } = useFileDialog({
56+
accept: 'image/*', // Set to accept only image files
57+
multiple: true, // Allow multiple file selection
58+
})
59+
60+
watch(files, async (newFiles) => {
61+
if (newFiles) {
62+
const fileList = Array.from(newFiles)
63+
const fileData: ImageData[] = []
64+
for (const file of fileList) {
65+
fileData.push({
66+
data: await convertImageFileToJpegBase64(file),
67+
type: 'image/jpeg', // Assuming all images are converted to JPEG
68+
})
69+
}
70+
selectedImages.value = [...fileData]
71+
}
72+
})
73+
74+
const base64Urls = computed(() => selectedImages.value.map((image) => {
75+
return `data:${image.type};base64,${image.data}`
76+
}))
77+
78+
const chooseImages = async () => {
79+
open()
80+
}
81+
82+
</script>
83+
84+
<style lang="scss">
85+
.selector-enter-active,
86+
.selector-leave-active {
87+
transition: all 0.3s var(--ease-cubic-1);
88+
transform: translateY(0%);
89+
}
90+
91+
.selector-enter-from,
92+
.selector-leave-to {
93+
opacity: 0;
94+
transform: translateY(50%);
95+
}
96+
</style>

entrypoints/content/components/TabSelector.vue

Lines changed: 53 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,71 @@
11
<template>
22
<div class="relative">
3-
<Transition name="selector">
3+
<Transition
4+
enterActiveClass="transition-all duration-300 ease-cubic-1"
5+
leaveActiveClass="transition-all duration-300 ease-cubic-1"
6+
enterFromClass="opacity-0 translate-y-8"
7+
leaveToClass="opacity-0 translate-y-8"
8+
enterToClass="opacity-100 translate-y-0"
9+
leaveFromClass="opacity-100 translate-y-0"
10+
>
411
<div
512
v-if="isShowSelector"
613
ref="selectorListContainer"
7-
class="absolute top-0 w-full z-50 translate-y-[calc(-100%-1rem)] bg-bg-component rounded-lg shadow-01 p-1"
14+
class="absolute top-0 h-0 w-full z-50"
815
>
9-
<div class="flex flex-col">
10-
<div class="w-full mb-px">
11-
<div
12-
class="w-full flex items-center gap-1 px-1 py-2 cursor-pointer rounded-sm"
13-
:class="[isAllTabSelected ? 'bg-[#DFE1E5]' : 'hover:bg-[#EAECEF]']"
14-
@click="selectAllTabs"
15-
>
16-
<IconTab class="w-4 h-4" />
17-
<span>
18-
{{ t('chat.input.tab_selector.all_tabs') }}
19-
</span>
20-
<span>
21-
({{ allTabs.length }})
22-
</span>
23-
</div>
24-
</div>
25-
<ScrollContainer
26-
itemContainerClass="h-max"
27-
containerClass="max-h-[max(calc(50vh-120px),250px)]"
28-
>
29-
<div class="flex flex-col h-max gap-px">
16+
<div class="translate-y-[calc(-100%-1rem)] bg-bg-component rounded-lg shadow-01 p-1 w-full">
17+
<div class="flex flex-col">
18+
<div class="w-full mb-px">
3019
<div
31-
v-for="tab in allTabs"
32-
:key="tab.tabId"
33-
class="flex flex-col px-1 py-2 cursor-pointer rounded-sm"
34-
:class="[isTabSelected(tab) ? 'bg-[#DFE1E5]' : 'hover:bg-[#EAECEF]']"
35-
@click="toggleSelect(tab)"
20+
class="w-full flex items-center gap-1 px-1 py-2 cursor-pointer rounded-sm"
21+
:class="[isAllTabSelected ? 'bg-[#DFE1E5]' : 'hover:bg-[#EAECEF]']"
22+
@click="selectAllTabs"
3623
>
24+
<IconTab class="w-4 h-4" />
25+
<span>
26+
{{ t('chat.input.tab_selector.all_tabs') }}
27+
</span>
28+
<span>
29+
({{ allTabs.length }})
30+
</span>
31+
</div>
32+
</div>
33+
<ScrollContainer
34+
itemContainerClass="h-max"
35+
containerClass="max-h-[max(calc(50vh-120px),250px)]"
36+
>
37+
<div class="flex flex-col h-max gap-px">
3738
<div
38-
class="flex gap-2 items-center"
39+
v-for="tab in allTabs"
40+
:key="tab.tabId"
41+
class="flex flex-col px-1 py-2 cursor-pointer rounded-sm"
42+
:class="[isTabSelected(tab) ? 'bg-[#DFE1E5]' : 'hover:bg-[#EAECEF]']"
43+
@click="toggleSelect(tab)"
3944
>
40-
<ExternalImage
41-
v-if="tab.faviconUrl"
42-
:src="tab.faviconUrl"
43-
alt=""
44-
class="w-4 h-4 rounded-full grow-0 shrink-0 bg-gray-300"
45-
>
46-
<template #fallback>
47-
<div class="w-4 h-4 rounded-full grow-0 shrink-0 bg-gray-300" />
48-
</template>
49-
</ExternalImage>
5045
<div
51-
:for="`tab-${tab.tabId}`"
52-
class="wrap-anywhere"
46+
class="flex gap-2 items-center"
5347
>
54-
{{ tab.title }}
48+
<ExternalImage
49+
v-if="tab.faviconUrl"
50+
:src="tab.faviconUrl"
51+
alt=""
52+
class="w-4 h-4 rounded-full grow-0 shrink-0 bg-gray-300"
53+
>
54+
<template #fallback>
55+
<div class="w-4 h-4 rounded-full grow-0 shrink-0 bg-gray-300" />
56+
</template>
57+
</ExternalImage>
58+
<div
59+
:for="`tab-${tab.tabId}`"
60+
class="wrap-anywhere"
61+
>
62+
{{ tab.title }}
63+
</div>
5564
</div>
5665
</div>
5766
</div>
58-
</div>
59-
</ScrollContainer>
67+
</ScrollContainer>
68+
</div>
6069
</div>
6170
</div>
6271
</Transition>
@@ -241,17 +250,3 @@ onBeforeUnmount(() => {
241250
cleanUpTabRemovedListener()
242251
})
243252
</script>
244-
245-
<style lang="scss">
246-
.selector-enter-active,
247-
.selector-leave-active {
248-
transition: all 0.3s var(--ease-cubic-1);
249-
transform: translateY(0%);
250-
}
251-
252-
.selector-enter-from,
253-
.selector-leave-to {
254-
opacity: 0;
255-
transform: translateY(50%);
256-
}
257-
</style>

entrypoints/content/components/WritingTools/EditableEntry.vue

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,9 @@ import Button from '@/components/ui/Button.vue'
8787
import Text from '@/components/ui/Text.vue'
8888
import { useDeferredValue } from '@/composables/useDeferredValue'
8989
import { useRefSnapshot } from '@/composables/useRefSnapshot'
90+
import { MIN_SELECTION_LENGTH_TO_SHOW_WRITING_TOOLS } from '@/utils/constants'
9091
import { useI18n } from '@/utils/i18n'
91-
import { getCommonAncestorElement, getEditableElementSelectedText, getSelectionBoundingRect, isContentEditableElement, isEditorFrameworkElement, isInputOrTextArea, replaceContentInRange } from '@/utils/selection'
92+
import { getCommonAncestorElement, getEditableElementSelectedText, getSelectionBoundingRectWithinElement, isContentEditableElement, isEditorFrameworkElement, isInputOrTextArea, replaceContentInRange } from '@/utils/selection'
9293
import { extendedComputed } from '@/utils/vue/utils'
9394
9495
import SuggestionCard from './SuggestionCard.vue'
@@ -110,7 +111,7 @@ const writingToolSelectedText = ref<string>('')
110111
const editableElementText = ref('')
111112
const regenerateSymbol = ref(0)
112113
const isShowToolBar = computed(() => {
113-
return isEditableFocus.value && !!writingToolSelectedText.value.trim()
114+
return isEditableFocus.value && writingToolSelectedText.value.trim().length >= MIN_SELECTION_LENGTH_TO_SHOW_WRITING_TOOLS
114115
})
115116
const isShowToolBarDeferred = useDeferredValue(isShowToolBar, 200, (v) => !v)
116117
@@ -140,7 +141,7 @@ const selectedBounding = computed(() => {
140141
const _ = editableElementBounding.top.value // reactive to top changes
141142
const _1 = editableElementBounding.left.value // reactive to left changesleft: 0 }
142143
const _2 = writingToolSelectedText.value // reactive to selected text changes
143-
const rect = getSelectionBoundingRect(props.editableElement, window.getSelection())
144+
const rect = getSelectionBoundingRectWithinElement(props.editableElement, window.getSelection())
144145
return rect
145146
})
146147

entrypoints/content/components/WritingTools/SuggestionCard.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ const start = async () => {
153153
output.value = ''
154154
const prompt = await prompts[props.type](props.selectedText)
155155
const iter = streamTextInBackground({
156-
prompt: prompt.user,
156+
prompt: prompt.user.extractText(),
157157
system: prompt.system,
158158
abortSignal: abortController.signal,
159159
})

0 commit comments

Comments
 (0)