Commit 1ee1ea77 authored by Domi's avatar Domi

feat: sidebar multitab

parent f1552320
...@@ -18,8 +18,10 @@ ...@@ -18,8 +18,10 @@
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"mammoth": "^1.6.0", "mammoth": "^1.6.0",
"pdfjs-dist": "^4.0.269", "pdfjs-dist": "^4.0.269",
"punycode.js": "^2.3.1",
"tiktoken": "^1.0.11", "tiktoken": "^1.0.11",
"turndown": "^7.1.2", "turndown": "^7.1.2",
"ua-parser-js": "^1.0.37",
"vite-plugin-top-level-await": "^1.4.1", "vite-plugin-top-level-await": "^1.4.1",
"vite-plugin-wasm": "^3.3.0", "vite-plugin-wasm": "^3.3.0",
"vue": "^3.3.4", "vue": "^3.3.4",
...@@ -8752,6 +8754,14 @@ ...@@ -8752,6 +8754,14 @@
"pump": "^2.0.0" "pump": "^2.0.0"
} }
}, },
"node_modules/punycode.js": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz",
"integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==",
"engines": {
"node": ">=6"
}
},
"node_modules/q": { "node_modules/q": {
"version": "1.5.1", "version": "1.5.1",
"resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz",
...@@ -10273,6 +10283,28 @@ ...@@ -10273,6 +10283,28 @@
"node": ">=14.17" "node": ">=14.17"
} }
}, },
"node_modules/ua-parser-js": {
"version": "1.0.37",
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.37.tgz",
"integrity": "sha512-bhTyI94tZofjo+Dn8SN6Zv8nBDvyXTymAdM3LDI/0IboIUwTu1rEhW7v2TfiVsoYWgkQ4kOVqnI8APUFbIQIFQ==",
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/ua-parser-js"
},
{
"type": "paypal",
"url": "https://paypal.me/faisalman"
},
{
"type": "github",
"url": "https://github.com/sponsors/faisalman"
}
],
"engines": {
"node": "*"
}
},
"node_modules/uberproto": { "node_modules/uberproto": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/uberproto/-/uberproto-1.2.0.tgz", "resolved": "https://registry.npmjs.org/uberproto/-/uberproto-1.2.0.tgz",
......
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q134 0 227-93t93-227q0-7-.5-14.5T799-507q-5 29-27 48t-52 19h-80q-33 0-56.5-23.5T560-520v-40H400v-80q0-33 23.5-56.5T480-720h40q0-23 12.5-40.5T563-789q-20-5-40.5-8t-42.5-3q-134 0-227 93t-93 227h200q66 0 113 47t47 113v40H400v110q20 5 39.5 7.5T480-160Z"/></svg> <svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24" fill="#888"><path d="M480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q134 0 227-93t93-227q0-7-.5-14.5T799-507q-5 29-27 48t-52 19h-80q-33 0-56.5-23.5T560-520v-40H400v-80q0-33 23.5-56.5T480-720h40q0-23 12.5-40.5T563-789q-20-5-40.5-8t-42.5-3q-134 0-227 93t-93 227h200q66 0 113 47t47 113v40H400v110q20 5 39.5 7.5T480-160Z"/></svg>
\ No newline at end of file \ No newline at end of file
{ {
"data": { "data": {
"configVersion": 20240310, "configVersion": 20240330,
"chatDocSites": [ "chatDocSites": [
{ {
"host": "huggingface.co", "host": "huggingface.co",
...@@ -135,6 +135,13 @@ ...@@ -135,6 +135,13 @@
], ],
"search": { "search": {
"defaultEngine": "google" "defaultEngine": "google"
},
"embedView": {
"defaultUA": "",
"hostUA": {
"www.google.com": 1,
"bing.com": 1
}
} }
} }
} }
...@@ -6,13 +6,13 @@ ...@@ -6,13 +6,13 @@
font-weight: normal; font-weight: normal;
} }
a, /* a,
.green { .green {
text-decoration: none; text-decoration: none;
/* color: hsla(160, 100%, 37%, 1); */ color: hsla(160, 100%, 37%, 1);
color: #38bdf8; color: #38bdf8;
transition: 0.4s; transition: 0.4s;
} } */
@media (hover: hover) { @media (hover: hover) {
a:hover { a:hover {
......
...@@ -164,7 +164,7 @@ function handleMessage(message: any, sender: chrome.runtime.MessageSender) { ...@@ -164,7 +164,7 @@ function handleMessage(message: any, sender: chrome.runtime.MessageSender) {
case MessageType.setupOffscreenDocument: case MessageType.setupOffscreenDocument:
return offscreen.setup() return offscreen.setup()
case MessageType.fromOffscreen: case MessageType.fromOffscreen:
return offscreen.handleMessage(message) return offscreen.handleResMsg(message)
case MessageType.invokeRequest: case MessageType.invokeRequest:
handleInvokeRequest(message, sender) handleInvokeRequest(message, sender)
break break
......
import { MessageType, ServiceFunc, type ParseDocOptions } from "@/types" import { MessageType, ServiceFunc, type ParseDocOptions } from "@/types"
import Invoke from "@/utils/Invoke" import { Invoke } from "@/utils/invoke"
let creating: Promise<void> | null // A global promise to avoid concurrency issues let creating: Promise<void> | null // A global promise to avoid concurrency issues
...@@ -60,7 +60,7 @@ class Offscreen extends Invoke { ...@@ -60,7 +60,7 @@ class Offscreen extends Invoke {
return { key, response } return { key, response }
} }
public handleMessage(message: any): void { public handleResMsg(message: any): void {
const { type, key, payload, success } = message const { type, key, payload, success } = message
if (type === MessageType.fromOffscreen) { if (type === MessageType.fromOffscreen) {
this.setReturnValue(key, success, payload) this.setReturnValue(key, success, payload)
......
...@@ -19,7 +19,7 @@ export async function handleContentMounted(tabId: number) { ...@@ -19,7 +19,7 @@ export async function handleContentMounted(tabId: number) {
const sidebar = contentSidebarMap.get(tabId) const sidebar = contentSidebarMap.get(tabId)
if (sidebar) { if (sidebar) {
await chrome.storage.session.set({ await chrome.storage.session.set({
sidebarUrls: { content: sidebar.url }, sidebarInitUrl: { content: sidebar.url },
}) })
chrome.tabs.sendMessage(tabId, { chrome.tabs.sendMessage(tabId, {
type: MessageType.openContentSidebar, type: MessageType.openContentSidebar,
......
...@@ -32,6 +32,7 @@ const engine = ref(Engine.google) ...@@ -32,6 +32,7 @@ const engine = ref(Engine.google)
const engineListVisible = ref(false) const engineListVisible = ref(false)
let timerId = -1 let timerId = -1
let timerId2 = -1
watch(focus, (value) => { watch(focus, (value) => {
if (value) { if (value) {
...@@ -103,20 +104,37 @@ function switchEngine(value: Engine) { ...@@ -103,20 +104,37 @@ function switchEngine(value: Engine) {
engine.value = value engine.value = value
chrome.storage.local.set({ searchEngine: value }) chrome.storage.local.set({ searchEngine: value })
} }
function handlePointerLeave() {
timerId2 = window.setTimeout(() => {
engineListVisible.value = false
}, 800)
}
function handlePointerEnter() {
clearTimeout(timerId2)
}
</script> </script>
<template> <template>
<div class="relative h-10 w-full"> <div class="relative h-10 w-full">
<div <div
class="absolute border border-foreground/20 rounded-[20px] w-full shadow bg-background z-10 overflow-hidden" :class="[
'absolute border border-foreground/20 rounded-[20px] w-full shadow bg-background z-10 overflow-hidden',
'focus-within:shadow-md',
]"
@pointerleave="handlePointerLeave"
@pointerenter="handlePointerEnter"
> >
<div aria-label="search box" class="flex px-2 items-center"> <div aria-label="search box" class="flex px-2 items-center">
<button <button
class="size-8 flex items-center justify-center shrink-0 rounded-full hover:bg-background-soft cursor-pointer" :class="[
'group size-8 flex items-center justify-center shrink-0 rounded-full hover:bg-background-soft cursor-pointer',
]"
@click="engineListVisible = !engineListVisible" @click="engineListVisible = !engineListVisible"
> >
<img <img
class="size-5" class="size-5 group-active:scale-90 transition-transform"
:src="engine == Engine.bing ? bingImg : googleImg" :src="engine == Engine.bing ? bingImg : googleImg"
/> />
</button> </button>
......
...@@ -11,29 +11,32 @@ defineProps<{ ...@@ -11,29 +11,32 @@ defineProps<{
title: string title: string
badge?: "popup" | "sidebar" | "remove" badge?: "popup" | "sidebar" | "remove"
small?: boolean small?: boolean
url?: string
}>() }>()
defineEmits(["click", "remove"]) defineEmits(["click", "remove"])
</script> </script>
<template> <template>
<button <a
:class="[ :class="[
'group shrink-0 relative flex flex-col items-center justify-self-center rounded-lg py-1 px-0.5 hover:bg-background-soft', 'group shrink-0 relative flex flex-col items-center justify-self-center rounded-lg ',
'py-1 px-0.5 hover:bg-background-soft text-inherit cursor-pointer',
small ? 'w-[58px]' : 'w-16', small ? 'w-[58px]' : 'w-16',
]" ]"
@click="$emit('click')" @click="(e) => (e.preventDefault(), $emit('click'))"
:href="url"
> >
<div class="p-2.5 rounded-full bg-background-soft"> <div class="p-2.5 rounded-full bg-background-soft">
<img <img
class="size-6 rounded" class="size-6 rounded pointer-events-none"
:src="icon" :src="icon"
:data-fallback="globeImg" :data-fallback="globeImg"
loading="lazy" loading="lazy"
@error="handleImgError" @error="handleImgError"
/> />
</div> </div>
<div class="flex flex-col justify-center h-8 w-full"> <div class="flex flex-col justify-center h-8 w-full text-center">
<div class="text-xs max-w-full break-words leading-tight line-clamp-2"> <div class="text-xs max-w-full break-words leading-tight line-clamp-2">
{{ title }} {{ title }}
</div> </div>
...@@ -64,7 +67,7 @@ defineEmits(["click", "remove"]) ...@@ -64,7 +67,7 @@ defineEmits(["click", "remove"])
> >
<IconClose class="size-3" /> <IconClose class="size-3" />
</div> </div>
</button> </a>
</template> </template>
<style scoped></style> <style scoped></style>
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, watch, onMounted, computed, onUnmounted } from "vue" import { ref, reactive, watch, onMounted, computed, onUnmounted } from "vue"
import config from "@/assets/config.json" import { updateFrameNetRules } from "@/utils/ext"
import { getLocal, updateFrameNetRules } from "@/utils/ext" import {
import { ContentScriptId, FrameMessageType } from "@/types" ContentScriptId,
FrameMessageType,
WindowName,
WebviewFunc,
} from "@/types"
import { findFrameLoadUrl } from "@/utils/utils" import { findFrameLoadUrl } from "@/utils/utils"
import { fetchDoc } from "@/content/pip" import { fetchDoc } from "@/content/pip"
import { WebviewInvoke } from "@/utils/invoke"
export type PageInfo = {
url: string
title: string
icon: string
}
const props = defineProps<{ const props = defineProps<{
url: string url: string
ua?: string ua?: string
preloadUrl?: string
preloadCandidates?: string[]
}>() }>()
const emit = defineEmits<{ const emit = defineEmits<{
pageInfo: [pageInfo: { url: string; title: string; icon: string }] load: [PageInfo]
}>() }>()
defineExpose({ defineExpose({
...@@ -24,59 +37,42 @@ defineExpose({ ...@@ -24,59 +37,42 @@ defineExpose({
const onceCallback = new Map<string, (e: MessageEvent) => boolean>() const onceCallback = new Map<string, (e: MessageEvent) => boolean>()
const frame = ref<HTMLIFrameElement>() const frame = ref<HTMLIFrameElement>()
const patchs = reactive(config.data.webviewPatchs)
const loadUrls = reactive(config.data.loadCandidates)
const inited = ref(false)
const frameUrl = ref("") const frameUrl = ref("")
const pageInfo = reactive({ url: "", title: "", icon: "" }) const pageInfo = reactive({ url: "", title: "", icon: "" })
const webviewInvoke = ref<WebviewInvoke>()
watch(
() => props.ua,
async (ua) => {
const tab = await chrome.tabs.getCurrent()
await updateFrameNetRules({
ua: ua,
tabIds: [tab?.id || -1],
initiatorDomains: [chrome.runtime.id],
})
}
)
onMounted(() => { onMounted(() => {
getLocal({ webviewInvoke.value = new WebviewInvoke("webview", frame.value!)
webviewPatchs: config.data.webviewPatchs,
loadCandidates: config.data.loadCandidates,
}).then(({ webviewPatchs, loadCandidates }) => {
if (webviewPatchs) {
patchs.splice(0, patchs.length, ...webviewPatchs)
}
if (loadCandidates) {
loadUrls.splice(0, loadUrls.length, ...loadCandidates)
}
inited.value = true
})
window.addEventListener("message", handleFrameMessage) window.addEventListener("message", handleFrameMessage)
console.log("loadFrame", props.url, props.ua)
loadFrame(props.url, props.ua)
}) })
onUnmounted(() => { onUnmounted(() => {
window.removeEventListener("message", handleFrameMessage) window.removeEventListener("message", handleFrameMessage)
}) })
const patch = computed(() => { async function loadFrame(url: string, ua?: string) {
const url = inited.value ? props.url : ""
const patch = patchs.find((p) => {
try {
return new RegExp(p.re).test(url)
} catch (e) {
console.error(e)
}
return false
})
return {
url,
ua: patch?.ua || props.ua || "",
l: patch?.l || "",
}
})
watch(patch, async (patch) => {
const iframe = frame.value const iframe = frame.value
if (!patch.url || !iframe) return if (!url || !iframe) return
const tab = await chrome.tabs.getCurrent() const tab = await chrome.tabs.getCurrent()
await updateFrameNetRules({ await updateFrameNetRules({
ua: patch.ua, ua: ua,
tabIds: [tab?.id || -1], tabIds: [tab?.id || -1],
initiatorDomains: [chrome.runtime.id],
}) })
await chrome.scripting.updateContentScripts([ await chrome.scripting.updateContentScripts([
...@@ -90,7 +86,7 @@ watch(patch, async (patch) => { ...@@ -90,7 +86,7 @@ watch(patch, async (patch) => {
}, },
]) ])
frameUrl.value = patch.url frameUrl.value = url
pageInfo.url = "" pageInfo.url = ""
pageInfo.title = "" pageInfo.title = ""
pageInfo.icon = "" pageInfo.icon = ""
...@@ -108,12 +104,19 @@ watch(patch, async (patch) => { ...@@ -108,12 +104,19 @@ watch(patch, async (patch) => {
}) })
} catch (e) { } catch (e) {
console.warn(e) console.warn(e)
const url = new URL(props.url) let loadUrl = ""
let loadUrl = url.origin + patch.l if (props.preloadUrl) {
if (!loadUrl) { const u = new URL(props.preloadUrl || "", props.url)
const u = await findFrameLoadUrl(loadUrls) loadUrl = u.href
}
if (!loadUrl && props.preloadCandidates) {
const u = await findFrameLoadUrl(props.preloadCandidates, props.url)
u && (loadUrl = u) u && (loadUrl = u)
} }
if (!loadUrl) {
console.warn("No preload url found")
return
}
try { try {
frameUrl.value = loadUrl frameUrl.value = loadUrl
...@@ -148,7 +151,7 @@ watch(patch, async (patch) => { ...@@ -148,7 +151,7 @@ watch(patch, async (patch) => {
}, },
"*" "*"
) )
}) }
function handleFrameMessage(e: MessageEvent) { function handleFrameMessage(e: MessageEvent) {
console.log("frame message: ", e, e.source !== frame.value?.contentWindow) console.log("frame message: ", e, e.source !== frame.value?.contentWindow)
...@@ -164,47 +167,56 @@ function handleFrameMessage(e: MessageEvent) { ...@@ -164,47 +167,56 @@ function handleFrameMessage(e: MessageEvent) {
pageInfo.title = e.data.title pageInfo.title = e.data.title
pageInfo.icon = e.data.icon pageInfo.icon = e.data.icon
emit("pageInfo", pageInfo) emit("load", pageInfo)
} }
break break
case FrameMessageType.invokeResponse:
webviewInvoke.value?.handleResMsg(e.data)
break
} }
} }
function reload() { function reload() {
console.log("reload") console.log("reload", pageInfo.url)
frame.value?.contentWindow?.postMessage( webviewInvoke.value
{ ?.invoke({
type: FrameMessageType.reload, func: WebviewFunc.reload,
}, args: [],
"*" timeout: 1000,
) })
.catch((e) => {
console.warn("reload failed", e)
if (frame.value) {
frame.value.src = pageInfo.url || props.url
}
})
} }
function goBack() { function goBack() {
console.log("goBack") console.log("goBack")
webviewInvoke.value?.invoke({
frame.value?.contentWindow?.postMessage( func: WebviewFunc.goBack,
{ args: [],
type: FrameMessageType.goBack, })
},
"*"
)
} }
function goForward() { function goForward() {
console.log("goForward") console.log("goForward")
webviewInvoke.value?.invoke({
frame.value?.contentWindow?.postMessage( func: WebviewFunc.goForward,
{ args: [],
type: FrameMessageType.goForward, })
},
"*"
)
} }
</script> </script>
<template> <template>
<iframe class="w-full h-full bg-white" ref="frame" :src="frameUrl"></iframe> <iframe
class="w-full h-full bg-white"
ref="frame"
sandbox="allow-same-origin allow-scripts allow-popups allow-forms"
:src="frameUrl"
:name="WindowName.webview"
></iframe>
</template> </template>
<style scoped></style> <style scoped></style>
...@@ -9,7 +9,7 @@ import { useI18n } from "@/utils/i18n" ...@@ -9,7 +9,7 @@ import { useI18n } from "@/utils/i18n"
import { getDocItem, devConfig, type SiteConfig } from "./helper" import { getDocItem, devConfig, type SiteConfig } from "./helper"
import config from "@/assets/config.json" import config from "@/assets/config.json"
import { getLocal } from "@/utils/ext" import { getLocal } from "@/utils/ext"
import { chatDocPrompt } from "@/utils/prompt" import { chatDocPrompt } from "@/utils/const"
import { autoPointerCapture } from "@/utils/dom" import { autoPointerCapture } from "@/utils/dom"
const { t } = useI18n() const { t } = useI18n()
...@@ -211,4 +211,4 @@ onUnmounted(() => { ...@@ -211,4 +211,4 @@ onUnmounted(() => {
</div> </div>
</template> </template>
<style scoped></style> <style scoped></style>@/utils/const
\ No newline at end of file \ No newline at end of file
<script setup lang="ts"> <script setup lang="ts">
import { watch, computed, reactive, ref, onMounted, watchEffect } from "vue" import { watch, computed, reactive, ref, onMounted, watchEffect } from "vue"
import { chatDocsPanel } from "@/store/content" import { chatDocsPanel } from "@/store/content"
import { contentService } from "@/utils/service" import { contentInvoke } from "@/utils/invoke"
import { convertBlobToBase64, semanticClip } from "@/utils/utils" import { convertBlobToBase64, semanticClip } from "@/utils/utils"
import ScrollView from "@/components/ScrollView.vue" import ScrollView from "@/components/ScrollView.vue"
import IconArrowBack from "@/components/icons/IconArrowBack.vue" import IconArrowBack from "@/components/icons/IconArrowBack.vue"
...@@ -21,7 +21,7 @@ import { ...@@ -21,7 +21,7 @@ import {
getInputValue, getInputValue,
} from "@/utils/dom" } from "@/utils/dom"
import { getLocal } from "@/utils/ext" import { getLocal } from "@/utils/ext"
import { chatDocPrompt } from "@/utils/prompt" import { chatDocPrompt } from "@/utils/const"
import { useI18n } from "@/utils/i18n" import { useI18n } from "@/utils/i18n"
type Sheet = "" | "docSelect" | "promptTemplate" type Sheet = "" | "docSelect" | "promptTemplate"
...@@ -126,7 +126,7 @@ watch( ...@@ -126,7 +126,7 @@ watch(
if (maxInputType !== "token") return if (maxInputType !== "token") return
if (!message) return if (!message) return
const tokenLength = await contentService.calcTokens(message) const tokenLength = await contentInvoke.calcTokens(message)
const rate = (message.length / tokenLength) * 0.95 const rate = (message.length / tokenLength) * 0.95
const exceedMaxInput = tokenLength > maxInput const exceedMaxInput = tokenLength > maxInput
if (exceedMaxInput) { if (exceedMaxInput) {
...@@ -172,7 +172,7 @@ watch( ...@@ -172,7 +172,7 @@ watch(
if (item.kind == "file" && typeof item.data != "string") { if (item.kind == "file" && typeof item.data != "string") {
const url = await convertBlobToBase64(item.data) const url = await convertBlobToBase64(item.data)
const results = await contentService.parseDoc({ const results = await contentInvoke.parseDoc({
key: item.key, key: item.key,
type: item.type, type: item.type,
size: item.data.size, size: item.data.size,
...@@ -640,4 +640,4 @@ input:hover { ...@@ -640,4 +640,4 @@ input:hover {
transform: translate(100%, 0); transform: translate(100%, 0);
} }
} }
</style> </style>@/utils/const@/utils/invoke/service@/utils/invokeb
\ No newline at end of file \ No newline at end of file
<template>
<svg
fill="currentColor"
stroke-width="0"
viewBox="0 0 24 24"
aria-hidden="true"
height="200px"
width="200px"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M10.5 18.75a.75.75 0 000 1.5h3a.75.75 0 000-1.5h-3z"></path>
<path
fill-rule="evenodd"
d="M8.625.75A3.375 3.375 0 005.25 4.125v15.75a3.375 3.375 0 003.375 3.375h6.75a3.375 3.375 0 003.375-3.375V4.125A3.375 3.375 0 0015.375.75h-6.75zM7.5 4.125C7.5 3.504 8.004 3 8.625 3H9.75v.375c0 .621.504 1.125 1.125 1.125h2.25c.621 0 1.125-.504 1.125-1.125V3h1.125c.621 0 1.125.504 1.125 1.125v15.75c0 .621-.504 1.125-1.125 1.125h-6.75A1.125 1.125 0 017.5 19.875V4.125z"
clip-rule="evenodd"
></path>
</svg>
</template>
...@@ -5,7 +5,7 @@ import { getLocal } from "@/utils/ext" ...@@ -5,7 +5,7 @@ import { getLocal } from "@/utils/ext"
import IconClose from "@/components/icons/IconClose.vue" import IconClose from "@/components/icons/IconClose.vue"
import IconSplitscreenRight from "@/components/icons/IconSplitscreenRight.vue" import IconSplitscreenRight from "@/components/icons/IconSplitscreenRight.vue"
import PageScrollbar from "./PageScrollbar.vue" import PageScrollbar from "./PageScrollbar.vue"
import { MessageType } from "@/types" import { FrameMessageType, MessageType } from "@/types"
import { autoPointerCapture } from "@/utils/dom" import { autoPointerCapture } from "@/utils/dom"
const props = defineProps<{ const props = defineProps<{
...@@ -16,6 +16,7 @@ const emit = defineEmits(["close", "hide"]) ...@@ -16,6 +16,7 @@ const emit = defineEmits(["close", "hide"])
const sidebarHtml = chrome.runtime.getURL("sidebar.html") const sidebarHtml = chrome.runtime.getURL("sidebar.html")
const sidebarUrl = sidebarHtml + "?mode=content" const sidebarUrl = sidebarHtml + "?mode=content"
const logoUrl = chrome.runtime.getURL("/logo.svg") const logoUrl = chrome.runtime.getURL("/logo.svg")
const extRootUrl = chrome.runtime.getURL("/")
let sheet: CSSStyleSheet | null = null let sheet: CSSStyleSheet | null = null
let invisibleTimer = 0 let invisibleTimer = 0
...@@ -62,6 +63,21 @@ function handleKeyUp(e: KeyboardEvent) { ...@@ -62,6 +63,21 @@ function handleKeyUp(e: KeyboardEvent) {
} }
} }
function handleMessage(e: MessageEvent) {
if (!extRootUrl.startsWith(e.origin)) {
return
}
// console.log(e)
switch (e.data?.type) {
case FrameMessageType.collapseSidebar:
emit("hide")
break
case FrameMessageType.closeSidebar:
emit("close")
break
}
}
watch( watch(
width, width,
debounce((value) => { debounce((value) => {
...@@ -90,6 +106,7 @@ onMounted(() => { ...@@ -90,6 +106,7 @@ onMounted(() => {
updatePageStyle(width.value) updatePageStyle(width.value)
window.addEventListener("keydown", handleKeydown) window.addEventListener("keydown", handleKeydown)
window.addEventListener("keyup", handleKeyUp) window.addEventListener("keyup", handleKeyUp)
window.addEventListener("message", handleMessage)
chrome.runtime.sendMessage({ chrome.runtime.sendMessage({
type: MessageType.registerContentSidebar, type: MessageType.registerContentSidebar,
}) })
...@@ -99,6 +116,7 @@ onUnmounted(() => { ...@@ -99,6 +116,7 @@ onUnmounted(() => {
disablePageStyle() disablePageStyle()
window.removeEventListener("keydown", handleKeydown) window.removeEventListener("keydown", handleKeydown)
window.removeEventListener("keyup", handleKeyUp) window.removeEventListener("keyup", handleKeyUp)
window.removeEventListener("message", handleMessage)
chrome.runtime.sendMessage({ chrome.runtime.sendMessage({
type: MessageType.unregisterContentSidebar, type: MessageType.unregisterContentSidebar,
}) })
...@@ -121,7 +139,10 @@ onUnmounted(() => { ...@@ -121,7 +139,10 @@ onUnmounted(() => {
@pointerdown="autoPointerCapture" @pointerdown="autoPointerCapture"
@pointermove="pointermove" @pointermove="pointermove"
></div> ></div>
<div class="flex gap-2 items-center justify-between h-8 px-2"> <div
v-if="false"
class="flex gap-2 items-center justify-between h-8 px-2"
>
<button <button
@click="" @click=""
class="size-7 flex items-center justify-center mr-auto" class="size-7 flex items-center justify-center mr-auto"
......
...@@ -31,9 +31,10 @@ const emit = defineEmits({ ...@@ -31,9 +31,10 @@ const emit = defineEmits({
<div class="flex flex-wrap gap-x-4 gap-y-4 justify-center"> <div class="flex flex-wrap gap-x-4 gap-y-4 justify-center">
<SiteButton <SiteButton
v-for="item of recentItems" v-for="item of recentItems.slice(0, 12)"
:icon="item.icon" :icon="item.icon"
:title="item.title" :title="item.title"
:url="item.url"
badge="remove" badge="remove"
@click="emit('go', item.url)" @click="emit('go', item.url)"
@remove="emit('removeRecentItem', item.url)" @remove="emit('removeRecentItem', item.url)"
......
...@@ -6,15 +6,20 @@ import { ...@@ -6,15 +6,20 @@ import {
pipWindow, pipWindow,
sidebarAddon, sidebarAddon,
} from "@/store/content" } from "@/store/content"
import { ContentEventType, FrameMessageType, MessageType } from "@/types" import {
ContentEventType,
FrameMessageType,
MessageType,
WebviewFunc,
WindowName,
} from "@/types"
import Copilot from "./Copilot.vue" import Copilot from "./Copilot.vue"
import { waitMessage } from "@/utils/ext"
import { import {
dispatchContentEvent, dispatchContentEvent,
addContentEventListener, addContentEventListener,
removeContentEventListener, removeContentEventListener,
} from "@/content/event" } from "@/content/event"
import { contentService } from "@/utils/service" import { contentInvoke } from "@/utils/invoke"
import { getPageIcon } from "@/utils/dom" import { getPageIcon } from "@/utils/dom"
// import { PipEventName } from "@/types/pip" // import { PipEventName } from "@/types/pip"
...@@ -47,7 +52,7 @@ function handleMessage( ...@@ -47,7 +52,7 @@ function handleMessage(
}) })
break break
case MessageType.invokeResponse: case MessageType.invokeResponse:
contentService.handleMessage(message) contentInvoke.handleResMsg(message)
break break
case MessageType.showChatDocs: case MessageType.showChatDocs:
chatDocsPanel.visible = true chatDocsPanel.visible = true
...@@ -150,18 +155,50 @@ function handleFrameMessage(e: MessageEvent) { ...@@ -150,18 +155,50 @@ function handleFrameMessage(e: MessageEvent) {
type: ContentEventType.escapeLoad, type: ContentEventType.escapeLoad,
detail: { url: e.data.url }, detail: { url: e.data.url },
}) })
case FrameMessageType.reload: case FrameMessageType.invokeRequest:
return location.reload() handleInvokeRequest(e.data, e.source as Window)
case FrameMessageType.goBack: break
return history.back() }
case FrameMessageType.goForward: }
return history.forward()
async function handleInvokeRequest(message: any, source: Window) {
const { key, func, args } = message
let result = null
let error = null
try {
switch (func) {
case WebviewFunc.reload:
location.reload()
break
case WebviewFunc.goBack:
history.back()
break
case WebviewFunc.goForward:
history.forward()
break
}
} catch (err) {
console.error("invoke error: ", err)
error = err
} }
console.log("invoke response: ", result, error)
source.postMessage(
{
type: FrameMessageType.invokeResponse,
key,
success: !error,
payload: !error ? result : error,
},
chrome.runtime.getURL("")
)
} }
if (window.self == window.parent) { if (window.top === window) {
run() run()
} else { }
// webview
if (window.top !== window && window.name.startsWith(WindowName.webview)) {
window.addEventListener("message", handleFrameMessage) window.addEventListener("message", handleFrameMessage)
window.parent?.postMessage( window.parent?.postMessage(
{ {
......
import { ContentEventType } from "@/types" import { ContentEventType, WindowName } from "@/types"
import { addContentEventListener } from "./event" import { addContentEventListener } from "./event"
import { copilotNavigateTo, pip, fetchDoc, writeHtml } from "./pip" import { copilotNavigateTo, pip, fetchDoc, writeHtml } from "./pip"
...@@ -27,3 +27,8 @@ async function handleEscapeLoadEvent(event: CustomEvent | Event) { ...@@ -27,3 +27,8 @@ async function handleEscapeLoadEvent(event: CustomEvent | Event) {
addContentEventListener(ContentEventType.pip, handlePipEvent) addContentEventListener(ContentEventType.pip, handlePipEvent)
addContentEventListener(ContentEventType.pipLoad, handlePipLoadDocEvent) addContentEventListener(ContentEventType.pipLoad, handlePipLoadDocEvent)
addContentEventListener(ContentEventType.escapeLoad, handleEscapeLoadEvent) addContentEventListener(ContentEventType.escapeLoad, handleEscapeLoadEvent)
if (window.name.startsWith(WindowName.webview)) {
window.parent = window
window.top = window
}
...@@ -21,6 +21,7 @@ import IconSplitscreenRight from "@/components/icons/IconSplitscreenRight.vue" ...@@ -21,6 +21,7 @@ import IconSplitscreenRight from "@/components/icons/IconSplitscreenRight.vue"
import SiteButton from "@/components/SiteButton.vue" import SiteButton from "@/components/SiteButton.vue"
import { getIsEdge } from "@/utils/ext" import { getIsEdge } from "@/utils/ext"
import { handleImgError } from "@/utils/dom" import { handleImgError } from "@/utils/dom"
import { homeUrl, feedbackUrl } from "@/utils/const"
const globeImg = chrome.runtime.getURL("img/globe.svg") const globeImg = chrome.runtime.getURL("img/globe.svg")
...@@ -151,7 +152,7 @@ async function openSidebar(url = "") { ...@@ -151,7 +152,7 @@ async function openSidebar(url = "") {
}) })
const win = await chrome.windows.getCurrent() const win = await chrome.windows.getCurrent()
await chrome.storage.session.set({ await chrome.storage.session.set({
sidebarUrls: { sidepanel: url }, sidebarInitUrl: { sidepanel: url },
}) })
await chrome.sidePanel.open({ windowId: win.id! }) await chrome.sidePanel.open({ windowId: win.id! })
...@@ -160,7 +161,7 @@ async function openSidebar(url = "") { ...@@ -160,7 +161,7 @@ async function openSidebar(url = "") {
async function openContentSidebar(url = "") { async function openContentSidebar(url = "") {
await chrome.storage.session.set({ await chrome.storage.session.set({
sidebarUrls: { content: url }, sidebarInitUrl: { content: url },
}) })
await chrome.tabs.sendMessage(activeTab.value.id!, { await chrome.tabs.sendMessage(activeTab.value.id!, {
type: MessageType.openContentSidebar, type: MessageType.openContentSidebar,
...@@ -180,7 +181,7 @@ async function handleClickLaunch(url: string) { ...@@ -180,7 +181,7 @@ async function handleClickLaunch(url: string) {
} }
function feedback() { function feedback() {
open("https://tawk.to/anythingcopilot", "_blank") open(feedbackUrl, "_blank")
} }
function fivestar() { function fivestar() {
open( open(
...@@ -194,7 +195,7 @@ function fivestar() { ...@@ -194,7 +195,7 @@ function fivestar() {
} }
function goHome() { function goHome() {
open("https://ziziyi.com/copilot", "_blank") open(homeUrl, "_blank")
} }
function showChatDocs() { function showChatDocs() {
...@@ -347,6 +348,7 @@ function showChatDocs() { ...@@ -347,6 +348,7 @@ function showChatDocs() {
:icon="item.icon" :icon="item.icon"
:title="item.title" :title="item.title"
:badge="keyboard.shift || (isEdge && !avaiable) ? 'popup' : 'sidebar'" :badge="keyboard.shift || (isEdge && !avaiable) ? 'popup' : 'sidebar'"
:url="item.url"
small small
@click=" @click="
() => { () => {
......
This diff is collapsed.
...@@ -54,7 +54,18 @@ export enum FrameMessageType { ...@@ -54,7 +54,18 @@ export enum FrameMessageType {
webviewRun = "anything-copilot_webview-run", webviewRun = "anything-copilot_webview-run",
escapeLoad = "anything-copilot_escape-load", escapeLoad = "anything-copilot_escape-load",
pageInfo = "anything-copilot_page-info", pageInfo = "anything-copilot_page-info",
reload = "anything-copilot_reload", collapseSidebar = "anything-copilot_collapse-sidebar",
goBack = "anything-copilot_go-back", closeSidebar = "anything-copilot_close-sidebar",
goForward = "anything-copilot_go-forward", invokeRequest = "anything-copilot_invoke-request",
invokeResponse = "anything-copilot_invoke-response",
}
export enum WindowName {
webview = "anything-copilot_webview",
}
export enum WebviewFunc {
reload = "reload",
goBack = "goBack",
goForward = "goForward",
} }
export const chatDocPrompt = `Here is some relevant reference information. Please refrain from summarizing or responding to specific content until I pose targeted questions. If you comprehend this instruction, kindly reply with "Okay, please continue."` export const chatDocPrompt = `Here is some relevant reference information. Please refrain from summarizing or responding to specific content until I pose targeted questions. If you comprehend this instruction, kindly reply with "Okay, please continue."`
export const homeUrl = 'https://ziziyi.com/copilot'
export const feedbackUrl = 'https://tawk.to/anythingcopilot'
...@@ -217,7 +217,7 @@ export async function waitFor( ...@@ -217,7 +217,7 @@ export async function waitFor(
} }
export function getPageIcon() { export function getPageIcon() {
const icons = document.querySelectorAll<HTMLLinkElement>('link[rel="icon"]') const icons = document.querySelectorAll<HTMLLinkElement>('link[rel~="icon"]')
for (let item of icons) { for (let item of icons) {
if (item.getAttribute("type") == "image/svg+xml") { if (item.getAttribute("type") == "image/svg+xml") {
return item.href return item.href
...@@ -231,6 +231,13 @@ export function getPageIcon() { ...@@ -231,6 +231,13 @@ export function getPageIcon() {
return icons[0].href return icons[0].href
} }
const touchIcon = document.querySelector<HTMLLinkElement>(
'link[rel="apple-touch-icon"]'
)
if (touchIcon) {
return touchIcon.href
}
return location.origin + "/favicon.ico" return location.origin + "/favicon.ico"
} }
......
...@@ -185,56 +185,116 @@ export function getSession<T extends Record<string, any>>(key: string | T) { ...@@ -185,56 +185,116 @@ export function getSession<T extends Record<string, any>>(key: string | T) {
} }
type NetRulesOptions = { type NetRulesOptions = {
id?: number
ua?: string ua?: string
tabIds?: number[] tabIds?: number[]
requestDomains?: string[]
initiatorDomains?: string[] initiatorDomains?: string[]
enabled?: boolean
} }
export async function updateFrameNetRules( export async function updateFrameNetRules({
{ ua, tabIds, initiatorDomains }: NetRulesOptions = { id,
tabIds: [-1], ua,
initiatorDomains: [chrome.runtime.id], tabIds,
} initiatorDomains,
) { requestDomains,
enabled,
}: NetRulesOptions) {
ua = ua || navigator.userAgent ua = ua || navigator.userAgent
// const noneTab = tabIds?.length == 1 && tabIds[0] == -1
// if (!initiatorDomains && !noneTab) {
// initiatorDomains = [chrome.runtime.id]
// }
await chrome.declarativeNetRequest.updateSessionRules({ await chrome.declarativeNetRequest.updateSessionRules({
removeRuleIds: [1], removeRuleIds: [id || 1],
addRules: [ addRules:
{ enabled != false
id: 1, ? [
priority: 1,
action: {
type: chrome.declarativeNetRequest.RuleActionType.MODIFY_HEADERS,
responseHeaders: [
{
header: "X-Frame-Options",
operation: chrome.declarativeNetRequest.HeaderOperation.REMOVE,
},
{ {
header: "Content-Security-Policy", id: id || 1,
operation: chrome.declarativeNetRequest.HeaderOperation.REMOVE, priority: 1,
}, action: {
], type: chrome.declarativeNetRequest.RuleActionType
requestHeaders: [ .MODIFY_HEADERS,
{ responseHeaders: [
header: "User-Agent", {
value: ua, header: "X-Frame-Options",
operation: chrome.declarativeNetRequest.HeaderOperation.SET, operation:
chrome.declarativeNetRequest.HeaderOperation.REMOVE,
},
{
header: "Content-Security-Policy",
operation:
chrome.declarativeNetRequest.HeaderOperation.REMOVE,
},
],
requestHeaders: [
{
header: "User-Agent",
value: ua,
operation: chrome.declarativeNetRequest.HeaderOperation.SET,
},
{
header: "Sec-Fetch-Dest",
value: "document",
operation: chrome.declarativeNetRequest.HeaderOperation.SET,
},
],
},
condition: {
// resourceTypes: [chrome.declarativeNetRequest.ResourceType.SUB_FRAME],
initiatorDomains,
requestDomains,
tabIds,
},
}, },
]
: [],
})
}
export async function updateUANetRules({
id,
ua,
requestDomains,
initiatorDomains,
tabIds,
enabled,
}: NetRulesOptions & { id: number }) {
// const noneTab = tabIds?.length == 1 && tabIds[0] == -1
// if (!initiatorDomains && !noneTab) {
// initiatorDomains = [chrome.runtime.id]
// }
await chrome.declarativeNetRequest.updateSessionRules({
removeRuleIds: [id],
addRules:
enabled != false
? [
{ {
header: "Sec-Fetch-Dest", id,
value: "document", priority: 2,
operation: chrome.declarativeNetRequest.HeaderOperation.SET, action: {
type: chrome.declarativeNetRequest.RuleActionType
.MODIFY_HEADERS,
requestHeaders: [
{
header: "User-Agent",
value: ua,
operation: chrome.declarativeNetRequest.HeaderOperation.SET,
},
],
},
condition: {
// resourceTypes: [chrome.declarativeNetRequest.ResourceType.SUB_FRAME],
initiatorDomains,
requestDomains,
tabIds,
},
}, },
], ]
}, : [],
condition: {
resourceTypes: [chrome.declarativeNetRequest.ResourceType.SUB_FRAME],
initiatorDomains,
tabIds,
},
},
],
}) })
} }
...@@ -250,12 +310,13 @@ export function isProtectedUrl(url: string) { ...@@ -250,12 +310,13 @@ export function isProtectedUrl(url: string) {
} }
const isEdge = getIsEdge() const isEdge = getIsEdge()
if (isEdge && u.hostname == "microsoftedge.microsoft.com") { if (isEdge && u.hostname == "microsoftedge.microsoft.com") {
return true return true
} }
if (u.hostname == "chrome.google.com") { if (
["chromewebstore.google.com", "chrome.google.com"].includes(u.hostname)
) {
return true return true
} }
......
import { MessageType, ServiceFunc, type ParseDocOptions } from "@/types" import { MessageType, ServiceFunc, type ParseDocOptions } from "@/types"
import Invoke from "./Invoke" import Invoke from "./Invoke"
class Service extends Invoke { class ContentInvoke extends Invoke {
public async send(req: any) { public async send(req: any) {
const key = this.key const key = this.key
const response = await chrome.runtime.sendMessage({ const response = await chrome.runtime.sendMessage({
...@@ -13,7 +13,7 @@ class Service extends Invoke { ...@@ -13,7 +13,7 @@ class Service extends Invoke {
return { key, response } return { key, response }
} }
public handleMessage(message: any) { public handleResMsg(message: any) {
if (message?.type === MessageType.invokeResponse) { if (message?.type === MessageType.invokeResponse) {
const { key, success, payload } = message const { key, success, payload } = message
this.setReturnValue(key, success, payload) this.setReturnValue(key, success, payload)
...@@ -42,6 +42,6 @@ class Service extends Invoke { ...@@ -42,6 +42,6 @@ class Service extends Invoke {
} }
} }
const contentService = new Service("content") const contentInvoke = new ContentInvoke("content")
export { Service, contentService } export { ContentInvoke, contentInvoke }
...@@ -6,6 +6,13 @@ type CallbackItem = { ...@@ -6,6 +6,13 @@ type CallbackItem = {
interface InvokeReq { interface InvokeReq {
func: string func: string
args: any[] args: any[]
timeout?: number
}
interface InvokeRes {
key: string
success: boolean
value: any
} }
abstract class Invoke { abstract class Invoke {
...@@ -25,14 +32,14 @@ abstract class Invoke { ...@@ -25,14 +32,14 @@ abstract class Invoke {
return `${this.name}-${this.uniqueId}-${this.count++}` return `${this.name}-${this.uniqueId}-${this.count++}`
} }
protected getReturnValue(key: string, req: InvokeReq, timeout?: number) { protected getReturnValue(key: string, req: InvokeReq) {
let timer: any let timer: any
const { func, timeout } = req
const promise = new Promise<any>((resolve, reject) => { const promise = new Promise<any>((resolve, reject) => {
this.pendingCallback[key] = { resolve, reject } this.pendingCallback[key] = { resolve, reject }
if (timeout) { if (timeout) {
timer = setTimeout( timer = setTimeout(
() => () => reject(`"${this.name}" invoke timeout: ${func} key: ${key}`),
reject(`"${this.name}" invoke timeout: ${req.func} key: ${key}`),
timeout timeout
) )
} }
...@@ -46,11 +53,11 @@ abstract class Invoke { ...@@ -46,11 +53,11 @@ abstract class Invoke {
return promise return promise
} }
protected setReturnValue(key: string, success: boolean, payload: any) { protected setReturnValue(key: string, success: boolean, value: any) {
const callback = this.pendingCallback[key] const callback = this.pendingCallback[key]
if (callback) { if (callback) {
const fn = success != false ? callback.resolve : callback.reject const fn = success != false ? callback.resolve : callback.reject
fn(payload) fn(value)
} else { } else {
console.error(`unknown invoke callback message: ${key}`) console.error(`unknown invoke callback message: ${key}`)
console.log(this.pendingCallback) console.log(this.pendingCallback)
...@@ -58,11 +65,12 @@ abstract class Invoke { ...@@ -58,11 +65,12 @@ abstract class Invoke {
} }
abstract send(req: InvokeReq): Promise<{ key: string; response: any }> abstract send(req: InvokeReq): Promise<{ key: string; response: any }>
abstract handleMessage(message: InvokeReq): void abstract handleResMsg(message: InvokeRes): void
public async invoke<T = any>(req: InvokeReq, timeout = 20000): Promise<T> { public async invoke<T = any>(req: InvokeReq): Promise<T> {
const { key } = await this.send(req) const { key } = await this.send(req)
return this.getReturnValue(key, req, timeout) req.timeout = req.timeout || 20000
return this.getReturnValue(key, req)
} }
} }
......
import { FrameMessageType } from "@/types"
import Invoke from "./Invoke"
class WebviewInvoke extends Invoke {
private frame: HTMLIFrameElement | null = null
constructor(name: string, frame: HTMLIFrameElement) {
super(name)
this.frame = frame
}
public async send(req: any) {
const key = this.key
const win = this.frame?.contentWindow
if (!win) {
console.warn("WebviewInvoke: frame not ready", this.frame)
}
win?.postMessage({ type: FrameMessageType.invokeRequest, key, ...req }, "*")
return { key, response: null }
}
public handleResMsg(message: any) {
if (message?.type === FrameMessageType.invokeResponse) {
const { key, success, payload } = message
this.setReturnValue(key, success, payload)
}
}
}
export { WebviewInvoke }
export { Invoke } from "./Invoke"
export { ContentInvoke, contentInvoke } from "./ContentInvoke"
export { WebviewInvoke } from "./WebviewInvoke"
...@@ -52,7 +52,10 @@ export const semanticClip = (text: string, maxLength: number) => { ...@@ -52,7 +52,10 @@ export const semanticClip = (text: string, maxLength: number) => {
} }
/** Find URL from the same origin that can be embedded */ /** Find URL from the same origin that can be embedded */
export async function findFrameLoadUrl(urls: string[]): Promise<string> { export async function findFrameLoadUrl(
urls: string[],
base?: string
): Promise<string> {
const abortController = new AbortController() const abortController = new AbortController()
let resolve: null | ((url: string) => void) = null let resolve: null | ((url: string) => void) = null
...@@ -65,8 +68,9 @@ export async function findFrameLoadUrl(urls: string[]): Promise<string> { ...@@ -65,8 +68,9 @@ export async function findFrameLoadUrl(urls: string[]): Promise<string> {
return !csp.includes("frame-ancestors") return !csp.includes("frame-ancestors")
} }
const promises = urls.map((url) => const promises = urls.map((url) => {
fetch(url, { const u = new URL(url, base)
return fetch(u.href, {
signal: abortController.signal, signal: abortController.signal,
}) })
.then((res) => { .then((res) => {
...@@ -74,12 +78,12 @@ export async function findFrameLoadUrl(urls: string[]): Promise<string> { ...@@ -74,12 +78,12 @@ export async function findFrameLoadUrl(urls: string[]): Promise<string> {
const xFrameOptions = h.get("X-Frame-Options") const xFrameOptions = h.get("X-Frame-Options")
const csp = h.get("Content-Security-Policy") const csp = h.get("Content-Security-Policy")
if (!xFrameOptions && checkCSP(csp)) { if (!xFrameOptions && checkCSP(csp)) {
resolve && resolve(url) resolve && resolve(u.href)
abortController.abort() abortController.abort()
} }
}) })
.catch(() => {}) .catch(() => {})
) })
Promise.all(promises).then(() => { Promise.all(promises).then(() => {
resolve && resolve("") resolve && resolve("")
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment