Commit dd609683 authored by Domi's avatar Domi

feat: remote config

parent 62715bb0
......@@ -8,6 +8,7 @@
"name": "picture-in-picture",
"version": "0.0.0",
"dependencies": {
"@firebase/remote-config": "^0.4.5",
"@types/turndown": "^5.0.4",
"@vueuse/core": "^10.7.1",
"@vueuse/gesture": "^2.0.0-beta.1",
......@@ -1254,14 +1255,14 @@
"integrity": "sha512-kYrbr8e/CYr1KLrLYZZt2noNnf+pRwDq2KK9Au9jHrBMnb0/C9X9yWSXmZkFt4UIdsQknBq8uBB7fsybZdOBTA=="
},
"node_modules/@firebase/remote-config": {
"version": "0.4.4",
"resolved": "https://registry.npmjs.org/@firebase/remote-config/-/remote-config-0.4.4.tgz",
"integrity": "sha512-x1ioTHGX8ZwDSTOVp8PBLv2/wfwKzb4pxi0gFezS5GCJwbLlloUH4YYZHHS83IPxnua8b6l0IXUaWd0RgbWwzQ==",
"version": "0.4.5",
"resolved": "https://registry.npmjs.org/@firebase/remote-config/-/remote-config-0.4.5.tgz",
"integrity": "sha512-rGLqc/4OmxrS39RA9kgwa6JmgWytQuMo+B8pFhmGp3d++x2Hf9j+MLQfhOLyyUo64fNw20J19mLXhrXvKHsjZQ==",
"dependencies": {
"@firebase/component": "0.6.4",
"@firebase/installations": "0.6.4",
"@firebase/component": "0.6.5",
"@firebase/installations": "0.6.5",
"@firebase/logger": "0.4.0",
"@firebase/util": "1.9.3",
"@firebase/util": "1.9.4",
"tslib": "^2.1.0"
},
"peerDependencies": {
......@@ -1284,11 +1285,57 @@
"@firebase/app-compat": "0.x"
}
},
"node_modules/@firebase/remote-config-compat/node_modules/@firebase/remote-config": {
"version": "0.4.4",
"resolved": "https://registry.npmjs.org/@firebase/remote-config/-/remote-config-0.4.4.tgz",
"integrity": "sha512-x1ioTHGX8ZwDSTOVp8PBLv2/wfwKzb4pxi0gFezS5GCJwbLlloUH4YYZHHS83IPxnua8b6l0IXUaWd0RgbWwzQ==",
"dependencies": {
"@firebase/component": "0.6.4",
"@firebase/installations": "0.6.4",
"@firebase/logger": "0.4.0",
"@firebase/util": "1.9.3",
"tslib": "^2.1.0"
},
"peerDependencies": {
"@firebase/app": "0.x"
}
},
"node_modules/@firebase/remote-config-types": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@firebase/remote-config-types/-/remote-config-types-0.3.0.tgz",
"integrity": "sha512-RtEH4vdcbXZuZWRZbIRmQVBNsE7VDQpet2qFvq6vwKLBIQRQR5Kh58M4ok3A3US8Sr3rubYnaGqZSurCwI8uMA=="
},
"node_modules/@firebase/remote-config/node_modules/@firebase/component": {
"version": "0.6.5",
"resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.6.5.tgz",
"integrity": "sha512-2tVDk1ixi12sbDmmfITK8lxSjmcb73BMF6Qwc3U44hN/J1Fi1QY/Hnnb6klFlbB9/G16a3J3d4nXykye2EADTw==",
"dependencies": {
"@firebase/util": "1.9.4",
"tslib": "^2.1.0"
}
},
"node_modules/@firebase/remote-config/node_modules/@firebase/installations": {
"version": "0.6.5",
"resolved": "https://registry.npmjs.org/@firebase/installations/-/installations-0.6.5.tgz",
"integrity": "sha512-0xxnQWw8rSRzu0ZOCkZaO+MJ0LkDAfwwTB2Z1SxRK6FAz5xkxD1ZUwM0WbCRni49PKubCrZYOJ6yg7tSjU7AKA==",
"dependencies": {
"@firebase/component": "0.6.5",
"@firebase/util": "1.9.4",
"idb": "7.1.1",
"tslib": "^2.1.0"
},
"peerDependencies": {
"@firebase/app": "0.x"
}
},
"node_modules/@firebase/remote-config/node_modules/@firebase/util": {
"version": "1.9.4",
"resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.9.4.tgz",
"integrity": "sha512-WLonYmS1FGHT97TsUmRN3qnTh5TeeoJp1Gg5fithzuAgdZOUtsYECfy7/noQ3llaguios8r5BuXSEiK82+UrxQ==",
"dependencies": {
"tslib": "^2.1.0"
}
},
"node_modules/@firebase/storage": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/@firebase/storage/-/storage-0.12.0.tgz",
......@@ -3591,6 +3638,21 @@
"@firebase/util": "1.9.3"
}
},
"node_modules/firebase/node_modules/@firebase/remote-config": {
"version": "0.4.4",
"resolved": "https://registry.npmjs.org/@firebase/remote-config/-/remote-config-0.4.4.tgz",
"integrity": "sha512-x1ioTHGX8ZwDSTOVp8PBLv2/wfwKzb4pxi0gFezS5GCJwbLlloUH4YYZHHS83IPxnua8b6l0IXUaWd0RgbWwzQ==",
"dependencies": {
"@firebase/component": "0.6.4",
"@firebase/installations": "0.6.4",
"@firebase/logger": "0.4.0",
"@firebase/util": "1.9.3",
"tslib": "^2.1.0"
},
"peerDependencies": {
"@firebase/app": "0.x"
}
},
"node_modules/fraction.js": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
......
......@@ -3,12 +3,12 @@
"version": "0.0.0",
"private": true,
"scripts": {
"dev": "run-p dev:page dev:content dev:js",
"dev:page": "cross-env NODE_ENV=development vite build --watch",
"dev:content": "vite build --watch -c vite.content.config.ts",
"dev:js": "node esbuild.mjs --watch",
"dev": "run-p dev:page dev:content dev:js",
"start": "vite -c vite.config.ts --port 3000",
"build": "run-p type-check build:js build:content \"build-only {@}\" --",
"preview": "vite preview",
"build-only": "vite build",
"build:content": "vite build -c vite.content.config.ts",
"build:js": "node esbuild.mjs",
......@@ -18,6 +18,7 @@
"test": "vitest"
},
"dependencies": {
"@firebase/remote-config": "^0.4.5",
"@types/turndown": "^5.0.4",
"@vueuse/core": "^10.7.1",
"@vueuse/gesture": "^2.0.0-beta.1",
......
{
"data": {
"configVersion": 20240213,
"chatDocSites": [
{
"host": "huggingface.co",
"path": "^/chat",
"maxInputLength": 8000,
"selector": {
"input": "form[tabindex] textarea",
"send": "form[tabindex] button[type=\"submit\"]",
"wait": "form[tabindex] button[type=\"submit\"]:not([disabled=true])"
}
},
{
"host": "chat.openai.com",
"path": ".",
"maxInputLength": 8000,
"maxInputToken": 4096,
"selector": {
"input": "form textarea#prompt-textarea",
"send": "form textarea ~ button",
"wait": "form textarea ~ button"
}
},
{
"host": "bard.google.com",
"path": "chat",
"maxInputLength": 4096,
"selector": {
"input": "input-area rich-textarea div",
"send": "input-area div[class*=send] button[class*=send]",
"wait": "input-area div[class*=send] button[class*=send]"
}
},
{
"host": "gemini.google.com",
"path": "app",
"maxInputLength": 32000,
"selector": {
"input": "input-area-v2 rich-textarea div",
"send": "input-area-v2 div[class*=send] button[class*=send]",
"wait": [
{
"target": ".conversation-container:last-of-type bard-avatar .avatar_primary_model img[src*='processing']",
"match": ""
},
{
"target": "input-area-v2 div[class*=send] button[class*=send] mat-icon",
"match": "*"
}
]
}
},
{
"host": "copilot.microsoft.com",
"path": ".",
"maxInputLength": 2048,
"selector": {
"input": "cib-serp /deep/ cib-action-bar /deep/ cib-text-input /deep/ textarea",
"send": "cib-serp /deep/ cib-action-bar /deep/ .bottom-right-controls button",
"wait": "cib-serp /deep/ cib-action-bar /deep/ cib-typing-indicator /deep/ button[disabled]"
}
},
{
"host": "yiyan.baidu.com",
"path": ".",
"maxInputLength": 2000,
"selector": {
"input": "textarea:not(h1 ~ textarea)",
"send": "div > span:has(svg[width=\"240\"])",
"wait": "div > span:has(svg[width=\"240\"]):not([style*=\"display: none\"])"
}
}
]
}
}
import { MessageType, ServiceFunc, type ParseDocOptions } from "@/types"
import { waitMessage, tabUpdated } from "@/utils/ext"
import { waitMessage, tabUpdated, getLocal } from "@/utils/ext"
import { offscreen } from "./offscreen"
import config from '@/assets/config.json'
async function openPipBackground(url: string) {
const tab = await chrome.tabs.create({
......@@ -153,3 +154,40 @@ function handleCommand(command: string) {
}
chrome.commands.onCommand.addListener(handleCommand)
async function updateConfig() {
const url = 'https://config.ziziyi.com/anything-copilot'
const now = Date.now()
let { timestamp, configVersion, chatDocSites } = await getLocal({
timestamp: {} as Record<string, number>,
configVersion: config.data.configVersion,
chatDocSites: config.data.chatDocSites,
})
if (timestamp.configUpdatedAt > 0 && now - timestamp.configUpdatedAt < 1000 * 60 * 60 * 24) {
return
}
const res = await fetch(url)
const data = await res.json()
if (data.configVersion < configVersion) {
return
}
if (Array.isArray(data.chatDocSites) && data.chatDocSites.every((i: any) => i.host && i.selector)) {
chatDocSites = data.chatDocSites
}
timestamp.configUpdatedAt = now
await chrome.storage.local.set({
chatDocSites,
timestamp,
})
}
chrome.runtime.onStartup.addListener(() => {
updateConfig()
})
\ No newline at end of file
......@@ -6,7 +6,10 @@ import { chatDocsPanel, docsAddon } from "@/store"
import ChatDocsPanel from "@/components/chatdocs/ChatDocsPanel.vue"
import { watchEffect } from "vue"
import { useI18n } from "@/utils/i18n"
import { getDocItem, sitesConfig } from "./helper"
import { getDocItem, devConfig, type SiteConfig } from "./helper"
import config from "@/assets/config.json"
import { getLocal } from "@/utils/ext"
import { chatDocPrompt } from "@/utils/prompt"
const { t } = useI18n()
const logoUrl = chrome.runtime.getURL("/logo.svg")
......@@ -19,10 +22,12 @@ const position = reactive({
ty: 0,
})
const supported = computed(() => {
return sitesConfig.some(
(s) => s.host == location.host && s.path.test(location.pathname)
)
const siteConfig: SiteConfig = reactive({
prompt: chatDocPrompt,
maxInput: 3000,
maxInputType: "token",
maxRuns: 10,
selector: null,
})
let timer = 0
......@@ -38,7 +43,7 @@ watchEffect(() => {
})
function onDragOver(e: DragEvent) {
if (!supported.value) return
if (!siteConfig.selector) return
docsAddon.visible = true
clearTimeout(timer)
timer = window.setTimeout(() => (docsAddon.visible = false), 180)
......@@ -111,6 +116,25 @@ onMounted(() => {
const doc = div.value?.ownerDocument || document
doc.addEventListener("dragover", onDragOver, true)
doc.defaultView?.addEventListener("resize", adjustPosition)
const defaultChatDocSites = config.data.chatDocSites
getLocal({
chatDocSites: defaultChatDocSites,
}).then(({ chatDocSites }) => {
chatDocSites.push(devConfig)
const { host, pathname } = doc.location
const matchConfig = chatDocSites.find(
(s) => s.host == host && (new RegExp(s.path)).test(pathname)
)
if (matchConfig) {
siteConfig.maxInput = matchConfig.maxInputToken || matchConfig.maxInputLength
siteConfig.maxInputType = matchConfig.maxInputToken ? "token" : "char"
siteConfig.selector = matchConfig.selector
}
console.log("chatdoc config: ", matchConfig, chatDocSites)
})
})
onUnmounted(() => {
......@@ -123,21 +147,15 @@ onUnmounted(() => {
<template>
<div ref="div" class="hidden"></div>
<div
v-if="docsAddon.visible"
:class="[
<div v-if="docsAddon.visible" :class="[
'fixed mt-10 top-0 p-6 border-2 rounded-lg bg-background z-[9999999]',
'shadow-lg left-1/2 -translate-x-1/2 w-max transition-all',
{
'border-primary/50': !docsAddon.active && docsAddon.visible,
'border-primary scale-105': docsAddon.active,
},
]"
@dragenter="docsAddon.active = true"
@dragleave="docsAddon.active = false"
@dragover="(e) => e.preventDefault()"
@drop="onDrop"
>
]" @dragenter="docsAddon.active = true" @dragleave="docsAddon.active = false" @dragover="(e) => e.preventDefault()"
@drop="onDrop">
<div class="pointer-events-none">
<div class="flex items-center gap-2">
<IconNoteStackAdd class="w-8 h-8" />
......@@ -150,37 +168,23 @@ onUnmounted(() => {
</div>
</div>
<div
v-if="chatDocsPanel.visible"
ref="chatDocsDiv"
:class="[
<div v-if="chatDocsPanel.visible" ref="chatDocsDiv" :class="[
'fixed flex flex-col w-96 max-w-full h-[600px] max-h-full border rounded-lg',
'z-[9999] border-foreground/10 bg-background shadow-lg dark:border-2',
{
'left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2': !position.valid,
},
]"
>
<div
class="flex items-center px-4 pt-4 pb-1 select-none"
@pointerdown="
(e) => e.buttons == 1 && (e.target as Element)?.setPointerCapture(e.pointerId)
"
@pointermove="handlePointerMove"
@pointerup="adjustPosition"
@pointercancel="adjustPosition"
>
]">
<div class="flex items-center px-4 pt-4 pb-1 select-none" @pointerdown="(e) => e.buttons == 1 && (e.target as Element)?.setPointerCapture(e.pointerId)
" @pointermove="handlePointerMove" @pointerup="adjustPosition" @pointercancel="adjustPosition">
<img :src="logoUrl" class="w-6 h-6" />
<span class="mx-2 text-xl font-bold">{{ t("chatDocsAddon") }}</span>
<button
aria-label="close"
class="ml-auto p-1 top-0 right-0 rounded-full hover:bg-rose-400/10"
@click="chatDocsPanel.visible = false"
>
<button aria-label="close" class="ml-auto p-1 top-0 right-0 rounded-full hover:bg-rose-400/10"
@click="chatDocsPanel.visible = false">
<IconClose class="w-5 h-5" />
</button>
</div>
<ChatDocsPanel @close="chatDocsPanel.visible = false" />
<ChatDocsPanel @close="chatDocsPanel.visible = false" :siteConfig="siteConfig" />
</div>
</template>
......
......@@ -11,7 +11,8 @@ import DocInput from "./DocInput.vue"
import IconPlayCircle from "../icons/IconPlayCircle.vue"
import IconPause from "../icons/IconPause.vue"
import IconProgressActivity from "../icons/IconProgressActivity.vue"
import { sitesConfig } from "./helper"
import { devConfig, type SiteConfig } from "./helper"
import config from "@/assets/config.json"
import {
query,
dispatchInput,
......@@ -19,6 +20,7 @@ import {
waitFor,
getInputValue,
} from "@/utils/dom"
import { getLocal } from "@/utils/ext"
import { chatDocPrompt } from "@/utils/prompt"
import { useI18n } from "@/utils/i18n"
......@@ -33,13 +35,8 @@ type SnippetItem = {
length: number
}
type Selector = {
input: string
send: string
wait: string
}
defineEmits(["close"])
const { siteConfig } = defineProps<{ siteConfig: SiteConfig }>()
const { t } = useI18n()
const logoUrl = chrome.runtime.getURL("/logo.svg")
......@@ -52,14 +49,6 @@ const sendTask = reactive({
error: "",
})
const config = reactive({
prompt: chatDocPrompt,
maxInput: 3000,
maxInputType: "token" as "char" | "token",
maxRuns: 10,
selector: null as Selector | null,
})
const lenRate = reactive({
tokenRate: 4,
})
......@@ -72,7 +61,7 @@ const docs = computed(() => {
})
const currentMessage = computed(() => {
const { prompt, maxInput, maxInputType } = config
const { prompt, maxInput, maxInputType } = siteConfig
const snippets: SnippetItem[] = []
const rate = maxInputType == "token" ? lenRate.tokenRate : 1
......@@ -129,7 +118,7 @@ const currentMessage = computed(() => {
})
watch(
[config, () => currentMessage.value],
[siteConfig, () => currentMessage.value],
async ([config, currentMessage]) => {
const { message } = currentMessage
const { maxInput, maxInputType } = config
......@@ -243,22 +232,6 @@ const progress = computed(() => {
}
})
onMounted(() => {
const doc = div.value?.ownerDocument || document
const { host, pathname } = doc.location
const matchConfig = sitesConfig.find(
(c) => c.host == host && c.path.test(pathname)
)
if (matchConfig) {
config.maxInput = matchConfig.maxInputToken || matchConfig.maxInputLength
config.maxInputType = matchConfig.maxInputToken ? "token" : "char"
config.selector = matchConfig.selector
}
console.log("site config: ", matchConfig)
})
const openDocSelect = (key: string) => {
sheet.value = "docSelect"
currentDoc.value = key
......@@ -293,7 +266,7 @@ const autoSend = async () => {
return
}
if (!config.selector) {
if (!siteConfig.selector) {
return
}
......@@ -301,7 +274,7 @@ const autoSend = async () => {
sendTask.key = key
sendTask.status = "running"
const selector = config.selector
const selector = siteConfig.selector
const isWorking = () =>
!currentMessage.value.done &&
......@@ -331,11 +304,26 @@ const autoSend = async () => {
}
return sented
},
{ interval: 1000 * 2 }
{ interval: 1000 * 1 }
)
await new Promise((r) => setTimeout(r, 200))
await waitFor(() => query(selector.wait) != null, {})
const waitList =
typeof selector.wait == "string"
? [{ target: selector.wait, match: "*" }]
: selector.wait
for (let item of waitList) {
await waitFor(() => {
const t = query(item.target)
if (item.match == "") {
return t == null
}
return t != null && t.matches(item.match)
}, {})
}
await new Promise((r) => setTimeout(r, 200))
await nextMessage()
......@@ -411,11 +399,11 @@ const resetSent = () => {
<div class="flex items-center justify-between my-1">
<span
>{{ t("chatDocs.maxLength") }} ({{
config.maxInputType == "token" ? "token" : "char"
siteConfig.maxInputType == "token" ? "token" : "char"
}})</span
>
<input
v-model="config.maxInput"
v-model="siteConfig.maxInput"
class="border rounded px-2 py-1 w-20"
type="number"
min="1000"
......@@ -425,7 +413,7 @@ const resetSent = () => {
<div class="flex items-center justify-between my-1">
<span>{{ t("chatDocs.maxSendings") }}</span>
<input
v-model="config.maxRuns"
v-model="siteConfig.maxRuns"
class="border rounded px-2 py-1 w-20"
type="number"
min="1"
......@@ -518,7 +506,7 @@ const resetSent = () => {
{{ sendTask.error }}
</p>
<p
v-if="!config.selector"
v-if="!siteConfig.selector"
class="px-3 py-1 border rounded border-amber-400/60 mb-4"
>
{{ t("chatDocs.notSupported") }}
......@@ -610,7 +598,7 @@ const resetSent = () => {
'scrollbar border border-foreground/20 w-full h-36 p-2 bg-background-soft',
'outline-none rounded',
]"
v-model="config.prompt"
v-model="siteConfig.prompt"
></textarea>
<div class="flex gap-2 justify-end my-2">
<button class="px-2 py-1 bg-foreground/10" @click="sheet = ''">
......@@ -647,6 +635,7 @@ input:hover {
from {
transform: translate(-24px, 0);
}
to {
transform: translate(100%, 0);
}
......
import type { chatDocsPanel } from "@/store"
export const sitesConfig = [
{
host: "huggingface.co",
path: /^\/chat/,
maxInputLength: 8000, // 2048 token
selector: {
input: "form[tabindex] textarea",
send: 'form[tabindex] button[type="submit"]',
wait: 'form[tabindex] button[type="submit"]:not([disabled=true])',
},
},
{
host: "chat.openai.com",
path: /./,
maxInputLength: 8000,
maxInputToken: 4096,
selector: {
input: "form textarea#prompt-textarea",
send: "form textarea ~ button",
wait: "form textarea ~ button",
},
},
{
host: "bard.google.com",
path: /chat/,
maxInputLength: 4096,
selector: {
input: "input-area rich-textarea div",
send: "input-area div[class*=send] button[class*=send]",
wait: "input-area div[class*=send] button[class*=send]",
},
},
{
host: "copilot.microsoft.com",
path: /./,
maxInputLength: 2048,
selector: {
input:
"cib-serp /deep/ cib-action-bar /deep/ cib-text-input /deep/ textarea",
send: "cib-serp /deep/ cib-action-bar /deep/ .bottom-right-controls button",
wait: "cib-serp /deep/ cib-action-bar /deep/ cib-typing-indicator /deep/ button[disabled]",
},
},
{
host: "yiyan.baidu.com",
path: /./,
maxInputLength: 2000,
selector: {
input: "textarea:not(h1 ~ textarea)",
send: 'div > span:has(svg[width="240"])',
wait: 'div > span:has(svg[width="240"]):not([style*="display: none"])',
},
},
{
type Selector = {
input: string
send: string
wait: string | { target: string; match: string }[]
}
export interface SiteConfig {
prompt: string
maxInput: number
maxInputType: "char" | "token"
maxRuns: number
selector: Selector | null
}
export const devConfig = {
host: chrome.runtime.id + "-",
path: /^\/dev.html/,
path: "^/dev.html",
maxInputLength: 8000,
maxInputToken: 4096,
selector: {
......@@ -63,8 +24,7 @@ export const sitesConfig = [
send: "form textarea ~ button",
wait: "form textarea ~ button",
},
},
]
}
export async function getDocItem(itemList: DataTransferItemList | FileList) {
const items: typeof chatDocsPanel.inputs = []
......
import "@/content/index"
// import "@/pages/popup"
import "@/pages/popup"
import { testFirebase } from "@/utils/firebase"
......
......@@ -9,12 +9,12 @@ import cl100k_base from "tiktoken/encoders/cl100k_base.json"
pdfjs.GlobalWorkerOptions.workerSrc = "/js/pdf.worker.js"
const service = new TurndownService({ headingStyle: "atx" })
async function parsePdf(file: File) {
const buffer = await file.arrayBuffer()
const task = pdfjs.getDocument(buffer)
const pdf = await task.promise
const service = new TurndownService({ headingStyle: "atx" })
const contents = []
......@@ -38,6 +38,7 @@ async function parseDocx(file: File) {
)
console.log("result: ", result)
const service = new TurndownService({ headingStyle: "atx" })
const markdown = service.turndown(result.value)
return [markdown]
}
......
......@@ -98,7 +98,8 @@ export async function checkContent(tabId: number) {
await resMsgPromise;
}
} catch (err) {
console.error(err);
console.warn(err);
console.log("content is not available")
}
console.log("checkContent alive: ", alive);
......@@ -172,3 +173,11 @@ export function getStoreUrl(options: StoreUrlOptions) {
id: "lbeehbkcmjaopnlccpjcdgamcabhnanl",
})
}
export function getLocal<T extends Record<string, any>>(key: string | T,) {
return chrome.storage.local.get(key) as Promise<T>
}
export function getSession<T extends Record<string, any>>(key: string | T) {
return chrome.storage.session.get(key) as Promise<T>
}
......@@ -11,6 +11,7 @@
"compilerOptions": {
"composite": true,
"baseUrl": ".",
"resolveJsonModule": true,
"paths": {
"@/*": ["./src/*"]
},
......
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