├── .env.local.example ├── .eslintrc.json ├── .github └── workflows │ └── release.yaml ├── .gitignore ├── .vscode └── settings.json ├── README.md ├── components ├── check-button.tsx ├── pop-card.tsx └── settings-page.tsx ├── extension ├── grammify.png ├── icon128.png ├── icon16.png ├── icon48.png └── manifest.json ├── next.config.js ├── package.json ├── pages ├── _app.tsx ├── _document.tsx └── index.tsx ├── pnpm-lock.yaml ├── public └── favicon.ico ├── sanitize.js ├── tsconfig.json ├── types └── types.ts └── utils ├── open-ai-stream.ts └── polish.ts /.env.local.example: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_OPEN_AI_TOKEN= -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Publish release 2 | 3 | on: 4 | push: 5 | tags: [v\d+\.\d+\.\d+] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-22.04 10 | 11 | steps: 12 | - uses: actions/checkout@v3 13 | - uses: actions/setup-node@v3 14 | with: 15 | node-version: 18 16 | 17 | - name: Get version 18 | id: get_version 19 | uses: battila7/get-version-action@v2 20 | 21 | - name: Install and build 22 | run: | 23 | corepack enable 24 | pnpm i 25 | pnpm build 26 | 27 | - name: Change version 28 | run: | 29 | sed -i -e "s/\"version\": \".*\"/\"version\": \"${{ steps.get_version.outputs.version-without-v }}\"/" extension/manifest.json 30 | 31 | - name: Package plugin 32 | run: | 33 | mkdir release 34 | mv extension release/grammify-chrome-extension-${{ steps.get_version.outputs.version-without-v }} 35 | cd release 36 | zip -r grammify-chrome-extension-${{ steps.get_version.outputs.version-without-v }}.zip ./grammify-chrome-extension-${{ steps.get_version.outputs.version-without-v }}/* 37 | 38 | - name: Upload plugin to release 39 | uses: svenstaro/upload-release-action@v2 40 | with: 41 | release_name: ${{ steps.get_version.outputs.version }} 42 | repo_token: ${{ secrets.GITHUB_TOKEN }} 43 | file: release/grammify-chrome-extension-${{ steps.get_version.outputs.version-without-v }}.zip 44 | asset_name: grammify-chrome-extension-${{ steps.get_version.outputs.version-without-v }}.zip 45 | tag: ${{ github.ref }} 46 | overwrite: true 47 | body: ${{ steps.tag.outputs.message }} 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | .pnp 6 | .pnp.js 7 | 8 | # testing 9 | coverage 10 | 11 | # next.js 12 | .next/ 13 | next/ 14 | out/ 15 | 16 | # production 17 | build 18 | extension/index.html 19 | 20 | # misc 21 | .DS_Store 22 | *.pem 23 | 24 | # debug 25 | npm-debug.log* 26 | yarn-debug.log* 27 | yarn-error.log* 28 | .pnpm-debug.log* 29 | 30 | # local env files 31 | .env*.local 32 | 33 | # vercel 34 | .vercel 35 | 36 | # typescript 37 | *.tsbuildinfo 38 | next-env.d.ts 39 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true 3 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Grammify - Chrome Extension 2 | 3 | 灵感来自于 [bob-plugin-openai-polisher](https://github.com/yetone/bob-plugin-openai-polisher),使用 ChatGPT 的 API 实现了用来给语言润色和语法纠错的 Chrome 插件,尝试替代 Grammarly! 4 | 5 | ## 使用截图 6 | 7 | ![Settings](https://user-images.githubusercontent.com/38807139/222947018-f269a39b-1758-4116-b07d-fb05722516ba.png) 8 | ![Checks](https://user-images.githubusercontent.com/38807139/222947013-793570e4-b34e-47e7-a909-ff0160ba491f.png) 9 | 10 | ## 安装方法 11 | 12 | Chrome Web Store 审核中,目前需要手动下载和安装,敬请谅解 13 | 14 | 1. 去 Release 页面下载 [grammify-chrome-extension-\*.zip](https://github.com/liby/grammify/releases) 文件 15 | 2. 解压缩下载后的 grammify-chrome-extension-\*.zip 文件 16 | 3. 打开 Chrome 的 [Extensions](chrome://extensions) 页面 17 | 4. 在 Extensions 页面右上角打开 Developer mode 18 | 5. 点击 Extensions 页面左上角的 Load unpacked 按钮,选择刚刚解压缩的目录,随后完成安装 19 | ![Load unpacked](https://wd.imgix.net/image/BhuKGJaIeLNPW9ehns59NfwqKxF2/BzVElZpUtNE4dueVPSp3.png?auto=format&w=800) 20 | -------------------------------------------------------------------------------- /components/check-button.tsx: -------------------------------------------------------------------------------- 1 | import { Divider, Button } from "@mantine/core"; 2 | 3 | interface CheckButtonProps { 4 | inputValue: string; 5 | isLoading: boolean; 6 | handleCheck: (inputValue: string) => Promise; 7 | } 8 | 9 | export function CheckButton({ 10 | inputValue, 11 | isLoading, 12 | handleCheck, 13 | }: CheckButtonProps) { 14 | return ( 15 | handleCheck(inputValue)} 24 | compact 25 | variant="gradient" 26 | gradient={{ from: "#ed6ea0", to: "#ec8c69", deg: 35 }} 27 | > 28 | Check 29 | 30 | } 31 | /> 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /components/pop-card.tsx: -------------------------------------------------------------------------------- 1 | import { Card, Text, Textarea, Skeleton, Alert, Tabs } from "@mantine/core"; 2 | import { TypographyStylesProvider } from "@mantine/core"; 3 | import showdown from "showdown"; 4 | import { 5 | Children, 6 | PropsWithChildren, 7 | useCallback, 8 | useEffect, 9 | useState, 10 | } from "react"; 11 | import { useInputState } from "@mantine/hooks"; 12 | import { IconAlertCircle, IconChecks, IconSettings } from "@tabler/icons-react"; 13 | import { isError } from "#/types/types"; 14 | import { CheckButton } from "./check-button"; 15 | import { models, SettingsPage } from "./settings-page"; 16 | import { polish } from "#/utils/polish"; 17 | 18 | export function PopCard({ children }: PropsWithChildren) { 19 | const [inputValue, setInputValue] = useInputState(""); 20 | const [currentModel, setCurrentModelModel] = useState(models[0].value); 21 | const [tokens, setTokens] = useState(""); 22 | const [output, setOutput] = useState(""); 23 | const [stream, setStream] = useState(true); 24 | const [errorMessage, setErrorMessage] = useState(""); 25 | const [isLoading, setIsLoading] = useState(false); 26 | const converter = new showdown.Converter(); 27 | 28 | const handleCheck = useCallback( 29 | async (inputValue: string) => { 30 | setIsLoading(true); 31 | setOutput(""); 32 | const params = { 33 | prompt: inputValue, 34 | model: currentModel, 35 | apiKey: tokens, 36 | }; 37 | try { 38 | if (stream) { 39 | await polish({ 40 | ...params, 41 | stream: true, 42 | onMessage: (message) => { 43 | if (message.role) { 44 | return; 45 | } 46 | setOutput((output) => { 47 | return output + message.content; 48 | }); 49 | }, 50 | onFinish: (reason) => { 51 | if (reason !== "stop") { 52 | setErrorMessage(`Polishing failed:${reason}`); 53 | } 54 | setOutput((content) => { 55 | if (content.endsWith('"') || content.endsWith("」")) { 56 | return content.slice(0, -1); 57 | } 58 | return content; 59 | }); 60 | }, 61 | }); 62 | } else { 63 | await polish({ 64 | ...params, 65 | stream: false, 66 | onMessage: (message) => { 67 | setOutput(message.content); 68 | }, 69 | }); 70 | } 71 | } catch (error) { 72 | if (isError(error)) { 73 | setErrorMessage(`${error.message}`); 74 | } 75 | setErrorMessage("Unknown Error"); 76 | } finally { 77 | setIsLoading(false); 78 | } 79 | }, 80 | [currentModel, stream, tokens] 81 | ); 82 | 83 | useEffect(() => { 84 | chrome.storage?.sync.get( 85 | { 86 | apiKeys: "", 87 | currentModel: models[0].value, 88 | }, 89 | (result) => { 90 | if (result.apiKeys) { 91 | setTokens(result.apiKeys); 92 | } 93 | if (result.currentModel) { 94 | setCurrentModelModel(result.currentModel); 95 | } 96 | } 97 | ); 98 | }, []); 99 | 100 | return ( 101 | 102 | 103 | {children} 104 | 105 | {tokens || process.env.NEXT_PUBLIC_OPEN_AI_TOKEN ? ( 106 | <> 107 |