Skip to content

Commit

Permalink
feat: Chrome 插件划词解释功能
Browse files Browse the repository at this point in the history
  • Loading branch information
KwokKwok committed Sep 16, 2024
1 parent 6d6b2bf commit 95632c0
Show file tree
Hide file tree
Showing 18 changed files with 409 additions and 24 deletions.
26 changes: 26 additions & 0 deletions entrypoints/background.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,32 @@
import { browser } from "wxt/browser";

export default defineBackground(() => {
// browser.contextMenus.create({
// id: "explain",
// title: "问问AI",
// contexts: ["selection"]
// });

// browser.contextMenus.create({
// id: "summarize",
// title: "总结全文",
// contexts: ["page"]
// });

// browser.contextMenus.onClicked.addListener(async (info, tab) => {
// browser.scripting.executeScript({
// target: { tabId: tab.id },
// files: ['inject.js'],
// // args: [info]
// });
// if (info.menuItemId === "explain") {
// // 处理 AI 讲解逻辑

// } else if (info.menuItemId === "summarize") {
// // 处理 总结全文 逻辑
// console.log("总结全文:", tab.url);
// }
// });
browser.action.onClicked.addListener(function () {
browser.tabs.create({ url: browser.runtime.getURL("ext.html") });
});
Expand Down
74 changes: 74 additions & 0 deletions entrypoints/content/App.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { useState, useEffect } from 'react';
import Modal from './components/Modal';
import Logo from '@/public/logo.svg';

const getPageContext = () => {
return {
content: document.body.innerText,
title: document.title,
keywords: document.querySelector('meta[name="keywords"]')?.content,
description: document.querySelector('meta[name="description"]')?.content,
};
};

export default ({ ctx }) => {
const [showPage, setShowPage] = useState(false);
const [showButton, setShowButton] = useState(false);
const [buttonPosition, setButtonPosition] = useState({ x: 0, y: 0 });
const [selection, setSelection] = useState('');
const [payload, setPayload] = useState({});

useEffect(() => {
const handleSelection = e => {
const selection = window.getSelection();
if (selection.toString().length > 0) {
setSelection(selection.toString());
setShowButton(true);
setButtonPosition({ x: e.clientX + 6, y: e.clientY + 8 });
} else {
setShowButton(false);
}
};

document.addEventListener('mouseup', handleSelection);
return () => document.removeEventListener('mouseup', handleSelection);
}, []);

useEffect(() => {
if (showPage) {
document.querySelector('html').style.overflow = 'hidden';
} else {
document.querySelector('html').style.overflow = 'auto';
}
}, [showPage]);

const handleButtonClick = () => {
setPayload({ selection, type: 'query', context: getPageContext() });
setShowPage(true);
setShowButton(false);
};

return (
<div className="fixed right-0 my-auto top-0 bottom-0 flex items-center">
<Modal
visible={showPage}
close={() => setShowPage(false)}
payload={payload}
/>
<div
style={{
left: `${buttonPosition.x}px`,
top: `${buttonPosition.y}px`,
boxShadow: '0 2px 10px 0 rgba(0,0,0,.1)',
}}
className={
'fixed z-[99998] p-[4px] cursor-pointer rounded-[4px] bg-white transition-all duration-300 transform hover:scale-105 ' +
(showButton ? ' opacity-100' : 'opacity-0 hidden')
}
onClick={handleButtonClick}
>
<img src={Logo} className="!h-[16px] !w-[16px] " />
</div>
</div>
);
};
42 changes: 42 additions & 0 deletions entrypoints/content/components/Modal.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { useEffect } from 'react';
import { useState } from 'react';

export default function ({ close, payload, visible }) {
const url = browser.runtime.getURL('ext.html') + '#web-copilot';
const [loaded, setLoaded] = useState(false);
const iframeRef = useRef(null);

useEffect(() => {
if (!visible) {
iframeRef.current.contentWindow.postMessage(
JSON.stringify({ type: 'clear' }),
'*'
);
return;
}
if (payload.type) {
iframeRef.current.contentWindow.postMessage(JSON.stringify(payload), '*');
}
}, [payload, loaded, visible]);
return (
<div
className={
'fixed inset-0 z-[99999] transform bg-black bg-opacity-70 filter backdrop-blur flex justify-center items-center h-full ' +
(visible ? '' : 'translate-x-full translate-y-full')
}
onClick={close}
>
<iframe
ref={iframeRef}
onLoad={() => {
setLoaded(true);
}}
className={
'border-none transition-opacity duration-300 outline-none rounded-[16px] shadow-xl bg-black overflow-hidden w-[512px] h-[80dvh] ' +
(loaded ? 'opacity-100' : 'opacity-0')
}
src={url}
></iframe>
</div>
);
}
36 changes: 36 additions & 0 deletions entrypoints/content/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import './style.css';
import ReactDOM from 'react-dom/client';
import App from './App';

export default defineContentScript({
matches: ['*://*/*'],
cssInjectionMode: 'ui',

async main(ctx) {
console.log('content script injecting', ctx);
const ui = await createShadowRootUi(ctx, {
name: 'silo-ui',
position: 'overlay',
alignment: 'bottom-right',
zIndex: 99999,
anchor: 'body',
append: 'first',
onMount: container => {
// Don't mount react app directly on <body>
const wrapper = document.createElement('div');
wrapper.className = '';
container.append(wrapper);

const root = ReactDOM.createRoot(wrapper);
root.render(<App />);
return { root, wrapper };
},
onRemove: elements => {
elements?.root.unmount();
elements?.wrapper.remove();
},
});

ui.mount();
},
});
23 changes: 23 additions & 0 deletions entrypoints/content/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

* {
padding: 0;
margin: 0;
}

body {
padding: 16px;
}

.selected-search-box {
z-index: 300;
position: absolute;
cursor: pointer;
border: 0;
background: #fff;
box-shadow: 0 2px 10px 0 rgba(0, 0, 0, 0.1);
border-radius: 6px;
padding: 10px 15px 9px 16px;
}
11 changes: 11 additions & 0 deletions jsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@src/*": [
"./src/*"
]
},
"jsx": "react"
}
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "silo",
"private": true,
"version": "1.3.1",
"version": "1.4.0",
"type": "module",
"scripts": {
"dev": "vite",
Expand Down
22 changes: 14 additions & 8 deletions src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,29 @@ import {
Routes,
Route,
Navigate,
BrowserRouter as Router,
HashRouter as Router,
} from 'react-router-dom';
import ImageGenerate from './pages/image-generate';
import { useIsImageMode } from './store/storage';
import WebCopilot from './pages/web-copilot';

function App() {
const [isImageMode] = useIsImageMode();
return (
<div className="h-dvh w-full flex flex-col selection:bg-primary selection:text-white pb-2 text-sm">
<Router>
<HeaderAndPopup />
{isImageMode ? <ImageGenerate /> : <Chat />}
{/* <Routes>
<Route path="/" element={<Navigate to="/chat" replace />} />
<Route path="/chat" element={<Chat />} />
<Route path="/image" element={<ImageGenerate />} />
</Routes> */}
<Routes>
<Route
path="/"
element={
<>
<HeaderAndPopup />
{isImageMode ? <ImageGenerate /> : <Chat />}
</>
}
/>
<Route path="/web-copilot" element={<WebCopilot />} />
</Routes>
</Router>
</div>
);
Expand Down
24 changes: 16 additions & 8 deletions src/components/CustomModelDrawer/CustomModelForm.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { useState } from 'react';
import { Button } from 'tdesign-react';
import { CUSTOM_PRESET_PREFIX } from '../../utils/types';
import { useMemo } from 'react';
import { isBrowserExtension } from '@src/utils/utils';
const { FormItem } = Form;

const ID_REGEX = /^[a-zA-Z0-9_\-@\.]+\/[a-zA-Z0-9_\-@\.\/]+$/;
Expand Down Expand Up @@ -92,14 +93,14 @@ export default forwardRef((props, ref) => {
const isValid = ids.every(item => ID_REGEX.test(item));
return isValid
? Promise.resolve({
result: true,
})
result: true,
})
: Promise.resolve({
result: false,
message:
'ID格式错误,请检查是否符合: {manufacturer}/{model-name},多个需用英文逗号隔开',
type: 'warning',
});
result: false,
message:
'ID格式错误,请检查是否符合: {manufacturer}/{model-name},多个需用英文逗号隔开',
type: 'warning',
});
},
},
],
Expand Down Expand Up @@ -208,8 +209,15 @@ export default forwardRef((props, ref) => {
</FormItem>
</>
) : (
<FormItem label="解析函数" name="resolveFn">
<FormItem
label="解析函数"
help={
isBrowserExtension ? '浏览器扩展暂不支持自定义解析函数' : ''
}
name="resolveFn"
>
<Textarea
disabled={isBrowserExtension}
rows={10}
placeholder="建议调试好后复制过来,这里就不再做编辑器了"
/>
Expand Down
8 changes: 3 additions & 5 deletions src/components/InputControl.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from 'react';
import { useIsMobile } from '../utils/use';
import { Popconfirm } from 'tdesign-react';

export default function ({ onStop, onSubmit, loading }) {
export default function ({ onStop, onSubmit, loading, enter, placeholder }) {
const [input, setInput] = useState('');
const inputRef = useRef();
const isMobile = useIsMobile();
Expand Down Expand Up @@ -44,7 +44,7 @@ export default function ({ onStop, onSubmit, loading }) {
};

const onKeyDown = e => {
if (isMobile) return; // 移动端只允许点击发送
if (!enter && isMobile) return; // 移动端只允许点击发送
if (e.key === 'Enter') {
// 允许回车键发送
if (!e.shiftKey) {
Expand All @@ -67,9 +67,6 @@ export default function ({ onStop, onSubmit, loading }) {
(input.includes('\n') ? 'rounded-2xl' : 'rounded-3xl')
}
>
{/* <Popconfirm content={'确认删除订单吗'} cancelBtn={null}>
<Button theme="primary">删除订单</Button>
</Popconfirm> */}
<i
className={
(loading ? 'translate-x-0' : '-translate-x-10') +
Expand Down Expand Up @@ -99,6 +96,7 @@ export default function ({ onStop, onSubmit, loading }) {
style={{ height: '1.5rem' }}
onInput={onInput}
onKeyDown={onKeyDown}
placeholder={loading ? '正在思考中...' : placeholder}
ref={inputRef}
disabled={loading}
className=" outline-none overflow-y-auto flex-1 bg-transparent resize-none px-10 text-base leading-6"
Expand Down
Loading

0 comments on commit 95632c0

Please sign in to comment.