├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .github └── FUNDING.yml ├── .gitignore ├── .npmrc ├── .prettierrc ├── .vscode ├── extensions.json └── settings.json ├── CHANGELOG.md ├── CONTRIBUTIONS.md ├── README.md ├── _docs ├── _todo.md ├── api-documentation.md ├── api.md ├── store-listing │ ├── README.md │ ├── de.txt │ ├── en.txt │ ├── es.txt │ ├── fr.txt │ ├── hi.txt │ ├── ja.txt │ ├── pt_PT.txt │ ├── ru.txt │ ├── tr.txt │ └── zh_CN.txt └── translations.md ├── index.html ├── package.json ├── postcss.config.js ├── profiles ├── README.md └── default-profiles.json ├── public ├── _readme.md ├── avatar.png ├── donations │ ├── bitcoin.png │ ├── ethereum.png │ ├── gumroad.png │ └── paypal.png ├── favicon.ico ├── icons │ ├── favicon-128x128.png │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ └── favicon-96x96.png ├── logo.png ├── logo.svg ├── robot.png └── socials │ ├── github.svg │ ├── linkedin.svg │ └── twitter.svg ├── quasar.config.js ├── sites-detection ├── README.md └── sites-detection.ts ├── src-bex ├── _locales │ ├── de │ │ └── messages.json │ ├── en │ │ └── messages.json │ ├── es │ │ └── messages.json │ ├── fr │ │ └── messages.json │ ├── hi │ │ └── messages.json │ ├── ja │ │ └── messages.json │ ├── pt_AO │ │ └── messages.json │ ├── pt_BR │ │ └── messages.json │ ├── pt_CV │ │ └── messages.json │ ├── pt_GW │ │ └── messages.json │ ├── pt_MZ │ │ └── messages.json │ ├── pt_PT │ │ └── messages.json │ ├── pt_ST │ │ └── messages.json │ ├── pt_TL │ │ └── messages.json │ ├── ru │ │ └── messages.json │ ├── tr │ │ └── messages.json │ ├── zh_CN │ │ └── messages.json │ └── zh_SG │ │ └── messages.json ├── assets │ └── content.css ├── background.ts ├── bex-flag.d.ts ├── content.ts ├── dom.ts ├── icons │ ├── icon-128x128.png │ ├── icon-16x16.png │ ├── icon-48x48.png │ └── search.svg ├── libs │ └── sites-detection.ts ├── manifest.json └── modules │ ├── contextMenuComponent.ts │ └── iframeComponent.ts ├── src ├── App.vue ├── assets │ └── _readme.md ├── boot │ ├── .gitkeep │ └── i18next.ts ├── components │ ├── OpenAISettingsComponent.vue │ ├── SearchSettingsComponent.vue │ └── leftmenu │ │ ├── ConversationsComponent.vue │ │ ├── MenuLeftComponent.vue │ │ ├── ProfilesComponent.vue │ │ └── SettingsComponent.vue ├── css │ ├── app.scss │ └── quasar.variables.scss ├── env.d.ts ├── i18n │ ├── de │ │ └── index.ts │ ├── en │ │ └── index.ts │ ├── es │ │ └── index.ts │ ├── fr │ │ └── index.ts │ ├── hi │ │ └── index.ts │ ├── ja │ │ └── index.ts │ ├── pt │ │ └── index.ts │ ├── ru │ │ └── index.ts │ ├── tr │ │ └── index.ts │ ├── types.ts │ └── zh │ │ └── index.ts ├── js │ ├── api │ │ └── openai.api.ts │ ├── controllers │ │ ├── dbController.ts │ │ └── speechController.js │ ├── data │ │ ├── default-profiles.json │ │ ├── default-tags.json │ │ ├── gpt-models.ts │ │ └── triggerMode.ts │ └── libs │ │ ├── fetch-sse.ts │ │ ├── helpers.ts │ │ └── types │ │ ├── IDBConversation.ts │ │ ├── IDBProfile.ts │ │ ├── IGPTResponse.ts │ │ ├── IPTChatRequest.ts │ │ └── IUserSettings.ts ├── layouts │ ├── CompanionLayout.vue │ └── MainLayout.vue ├── pages │ ├── CompanionPage.vue │ ├── ConversationsPage.vue │ ├── ErrorNotFound.vue │ └── ProfilesPage.vue ├── quasar.d.ts ├── router │ ├── index.ts │ └── routes.ts └── shims-vue.d.ts ├── tsconfig.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /dist 2 | /src-capacitor 3 | /src-cordova 4 | /.quasar 5 | /node_modules 6 | .eslintrc.js 7 | /src-ssr 8 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // https://eslint.org/docs/user-guide/configuring#configuration-cascading-and-hierarchy 3 | // This option interrupts the configuration hierarchy at this file 4 | // Remove this if you have an higher level ESLint config file (it usually happens into a monorepos) 5 | root: true, 6 | 7 | // https://eslint.vuejs.org/user-guide/#how-to-use-a-custom-parser 8 | // Must use parserOptions instead of "parser" to allow vue-eslint-parser to keep working 9 | // `parser: 'vue-eslint-parser'` is already included with any 'plugin:vue/**' config and should be omitted 10 | parserOptions: { 11 | parser: require.resolve('@typescript-eslint/parser'), 12 | extraFileExtensions: [ '.vue' ] 13 | }, 14 | 15 | env: { 16 | browser: true, 17 | es2021: true, 18 | node: true, 19 | 'vue/setup-compiler-macros': true 20 | }, 21 | 22 | // Rules order is important, please avoid shuffling them 23 | extends: [ 24 | // Base ESLint recommended rules 25 | // 'eslint:recommended', 26 | 27 | // https://github.com/typescript-eslint/typescript-eslint/tree/master/packages/eslint-plugin#usage 28 | // ESLint typescript rules 29 | 'plugin:@typescript-eslint/recommended', 30 | 31 | // Uncomment any of the lines below to choose desired strictness, 32 | // but leave only one uncommented! 33 | // See https://eslint.vuejs.org/rules/#available-rules 34 | 'plugin:vue/vue3-essential', // Priority A: Essential (Error Prevention) 35 | // 'plugin:vue/vue3-strongly-recommended', // Priority B: Strongly Recommended (Improving Readability) 36 | // 'plugin:vue/vue3-recommended', // Priority C: Recommended (Minimizing Arbitrary Choices and Cognitive Overhead) 37 | 38 | // https://github.com/prettier/eslint-config-prettier#installation 39 | // usage with Prettier, provided by 'eslint-config-prettier'. 40 | 'prettier' 41 | ], 42 | 43 | plugins: [ 44 | // required to apply rules which need type information 45 | '@typescript-eslint', 46 | 47 | // https://eslint.vuejs.org/user-guide/#why-doesn-t-it-work-on-vue-files 48 | // required to lint *.vue files 49 | 'vue' 50 | 51 | // https://github.com/typescript-eslint/typescript-eslint/issues/389#issuecomment-509292674 52 | // Prettier has not been included as plugin to avoid performance impact 53 | // add it as an extension for your IDE 54 | 55 | ], 56 | 57 | globals: { 58 | ga: 'readonly', // Google Analytics 59 | cordova: 'readonly', 60 | __statics: 'readonly', 61 | __QUASAR_SSR__: 'readonly', 62 | __QUASAR_SSR_SERVER__: 'readonly', 63 | __QUASAR_SSR_CLIENT__: 'readonly', 64 | __QUASAR_SSR_PWA__: 'readonly', 65 | process: 'readonly', 66 | Capacitor: 'readonly', 67 | chrome: 'readonly' 68 | }, 69 | 70 | // add your custom rules here 71 | rules: { 72 | 73 | 'prefer-promise-reject-errors': 'off', 74 | 75 | quotes: ['warn', 'single', { avoidEscape: true }], 76 | 77 | // this rule, if on, would require explicit return type on the `render` function 78 | '@typescript-eslint/explicit-function-return-type': 'off', 79 | 80 | // in plain CommonJS modules, you can't use `import foo = require('foo')` to pass this rule, so it has to be disabled 81 | '@typescript-eslint/no-var-requires': 'off', 82 | 83 | // The core 'no-unused-vars' rules (in the eslint:recommended ruleset) 84 | // does not work with type definitions 85 | 'no-unused-vars': 'off', 86 | 87 | // allow debugger during development only 88 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off' 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [robert-hoffmann] 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .thumbs.db 3 | node_modules 4 | 5 | # Quasar core related directories 6 | .quasar 7 | /dist/bex 8 | 9 | # Cordova related directories and files 10 | /src-cordova/node_modules 11 | /src-cordova/platforms 12 | /src-cordova/plugins 13 | /src-cordova/www 14 | 15 | # Capacitor related directories and files 16 | /src-capacitor/www 17 | /src-capacitor/node_modules 18 | 19 | # BEX related directories and files 20 | /src-bex/www 21 | /src-bex/js/core 22 | 23 | # Log files 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # Editor directories and files 29 | .idea 30 | *.suo 31 | *.ntvs* 32 | *.njsproj 33 | *.sln 34 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | # pnpm-related options 2 | shamefully-hoist=true 3 | strict-peer-dependencies=false 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "semi": true 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "esbenp.prettier-vscode", 5 | "editorconfig.editorconfig", 6 | "vue.volar", 7 | "bladnman.auto-align", 8 | "gruntfuggly.todo-tree", 9 | "britesnow.vscode-toggle-quotes", 10 | "wix.vscode-import-cost" 11 | ], 12 | "unwantedRecommendations": [ 13 | "octref.vetur", 14 | "hookyqr.beautify", 15 | "dbaeumer.jshint", 16 | "ms-vscode.vscode-typescript-tslint-plugin" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.bracketPairColorization.enabled": true, 3 | "editor.guides.bracketPairs": true, 4 | "editor.formatOnSave": false, 5 | "editor.defaultFormatter": "esbenp.prettier-vscode", 6 | "editor.codeActionsOnSave": ["source.fixAll.eslint"], 7 | "eslint.validate": ["javascript", "javascriptreact", "typescript", "vue"], 8 | "typescript.tsdk": "node_modules/typescript/lib", 9 | "[vue]": { 10 | "editor.defaultFormatter": "Vue.volar" 11 | }, 12 | "[svg]": { 13 | "editor.defaultFormatter": "jock.svg" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Version: 0.2.5 2 | * Possibility to display Search Companion on other sites as a sidebar (new) 3 | * Added word count to messages in Conversation Manager (new) 4 | * Added copy conversation to clipboard in conversation list (new) 5 | * ALT+C now sends any selected text to the conversation manager (new) 6 | * Now translated into 10 languages (new) 7 | * Added search engine support for: Yahoo, Baidu, Yandex, Naver, SearX (new) 8 | * You can also just activate the sidebar for any unsupported site 9 | * Lots of UI tweaks and code refactorings (enhancement) 10 | 11 | ## Version: 0.2.2 12 | * Fixed a problem where the search companion would not be properly displayed (fix) 13 | * Move notifications to bottom right (enhancement) 14 | * Scroll to bottom after adding user input (fix) 15 | 16 | ## Version: 0.2.1 17 | * Click to edit. You can now change the title of a saved conversation (new) 18 | * Added speech recognition, you can use ctrl+spacebar to toggle it (new) 19 | * Added keyboard shortcut to open conversation manager with alt+c (new) 20 | 21 | ## Version: 0.2.0 22 | * Added support for multiple profiles: prompt engineering (new) 23 | * Fixed scrolling in conversation panel (fix) 24 | * Fixed side menu from disappearing (fix) 25 | * Many small UI enhancements in the conversation manager (new) 26 | * Google changed how user input is set, so using a different detection method (fix) 27 | 28 | ## Version: 0.1.0 29 | * Show correct token pricing after clear (fix) 30 | * Show token length of current input prompt (new) 31 | * Interface language switcher in settings dialog (new) 32 | * Added AbortSignal to fetch request, so that streamed responses can be canceled (new) 33 | * Added full Chat interface with conversation management (new) 34 | ## Version: 0.0.5 35 | * Reinitialize/delete conversation (new) 36 | * Show conversation token length (new) 37 | * Show conversation token pricing (new) 38 | * Translations: EN/FR (new)= 39 | ## Version: 0.0.4 40 | * Toggle API visibility as password (new) 41 | * Support for conversation history in Search Companion (new) 42 | * ScrollToTop on new search (new) 43 | * Remove question mark from search phrase (fix) 44 | ## Version: 0.0.3 45 | * Initial release 46 | * Tested with 47 | * Chrome, Edge, Opera GX 48 | * Google, Bing, DuckDuckGo 49 | -------------------------------------------------------------------------------- /CONTRIBUTIONS.md: -------------------------------------------------------------------------------- 1 | **You can contribute by** 2 | 3 | * [Translating to other languages](_docs/translations.md) 4 | * [Filing bug issues](https://github.com/robert-hoffmann/PowerToys4OpenAI/issues) or [providing feedback in discussions](https://github.com/robert-hoffmann/PowerToys4OpenAI//discussions) 5 | * Sharing on social media, in newsletters, or other 6 | * Making a donation to support further development 7 | 8 | 9 | *I'm availiable via [Twitter](https://twitter.com/itechnologynet) & [LinkedIn](https://www.linkedin.com/in/hoffmannrobert)* 10 | 11 | 12 | --- 13 | **For the curious** 14 | 15 | *This was made with [VueJS](https://vuejs.org/) & [Quasar](https://quasar.dev/introduction-to-quasar) for the UI, [Dexie](https://dexie.org/) for local database handling, [TypeScript](https://www.typescriptlang.org/) for logic handling, and [VueUse](https://vueuse.org/) for certain features* 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PowerToys for OpenAI™ (donationware) 2 | 3 | ### For the power-user in the office, who just wants to get stuff done 4 | #### Lightning fast 5 | * Everything is self hosted/stored on your machine 6 | * No server needed, and no third party plug-ins 7 | * Automatic updates 8 | --- 9 | - **[Installation & Troubleshooting (Chrome, Edge, Opera)](https://github.com/robert-hoffmann/PowerToys4OpenAI/wiki/Docs)** 10 | - **[Issues](https://github.com/robert-hoffmann/PowerToys4OpenAI/issues)** 11 | - **[RoadMap](https://github.com/robert-hoffmann/PowerToys4OpenAI/wiki/RoadMap)** 12 | --- 13 | Get from Edge Store | Get from Chrome Store 14 | --- | --- 15 | [![image](https://user-images.githubusercontent.com/5472296/225245344-112ae97d-03a6-42a6-ab6a-4504d29df695.png)](https://microsoftedge.microsoft.com/addons/detail/powertoys-for-openai-%E2%84%A2/kjeipegpggpbciapoallgaieajcefolp) | [![image](https://user-images.githubusercontent.com/5472296/225245498-2a0ad50d-8295-41ab-8396-b00646521a87.png)](https://chrome.google.com/webstore/detail/powertoys-for-openai/haijiigmikhgoflpocajpfldmjcfbdpa) 16 | 17 | ### Click screenshot for quick product walkthrough 18 | 19 | 20 | 25 | 30 | 31 | 32 | 37 | 42 | 43 |
21 | 22 | 23 | 24 | 26 | 27 | 28 | 29 |
33 | 34 | 35 | 36 | 38 | 39 | 40 | 41 |
44 | 45 |

46 | 47 | 48 | 49 |

50 | 51 | --- 52 | 53 | #### Features 54 | * Search Companion is integrated with search engine results 55 | * Conversation Manager is a full chat interface with history management 56 | * Profile manager lets you fine tune the model's response type 57 | * Import/Export profiles in JSON 58 | * See token count 59 | * See conversation pricing 60 | * Copy results to clipboard 61 | * Markdown rendering support 62 | * Speech recognition support 63 | * Add a sidebar to any site 64 | * Fully translated into 10 languages 65 | 66 | --- 67 | 68 | * **[RoadMap](https://github.com/robert-hoffmann/PowerToys4OpenAI/wiki/RoadMap)** 69 | * **[Contribute](CONTRIBUTIONS.md)** 70 | 71 | --- 72 | 73 | #### External links 74 | 75 | * **[Twitter](https://twitter.com/itechnologynet)** 76 | * **[YouTube](https://www.youtube.com/@itechnologynet)** 77 | -------------------------------------------------------------------------------- /_docs/_todo.md: -------------------------------------------------------------------------------- 1 | # Todo (later) 2 | * Knowledge graph: type question, generate 3 ansers, click an answer, and generate 3 more ..etc 3 | * https://visjs.github.io/vis-network/ 4 | * https://twitter.com/hturan/status/1641780868640374784 5 | * implement writing interface like youwrite, with customizable templating language 6 | * use whisper to generate transcripts automatically ? 7 | * you can query them, but probably run into limitations, unless linked to pinecone or something 8 | * add these settings directly to search companion, IE 9 | * change language 10 | * change profile 11 | * OpenAI settings should be linked to profile 12 | * Thus profile should be linked to conversation history 13 | * add tts support (listen) 14 | * link profile to OpenAI settings 15 | * save/update (previous) conversation 16 | * add token price to db & settings (should probably be updated after each [DONE] event) 17 | * make translation loadable on demand insted of embedding them in the code 18 | * Add word count to search companion (..or not) 19 | 20 | # Refactoring 21 | * move (when possible) event handlers, to routes with arguments 22 | * in settings add: (always reply in selected language) toggle 23 | * add a little animation to show you can delete via swipe 24 | * see what error 600 is 25 | * redo openAI web service to handle errors and stuff 26 | - https://platform.openai.com/docs/guides/error-codes/api-errors 27 | * move db intialization directly to dbController ? 28 | 29 | # Todo (0.3.0) 30 | * Fixed and error with some status code not being handled correctly (done) 31 | * Added some language codes for PT and ZH, see how this works out on next app store submission 32 | * Make the text selection with ALT+C more useful 33 | * Sometimes the open sidebar button becomes unresponsive, see why (done, but needs improvement) 34 | * some strange binding error that doubles every time the conversation panel is opened 35 | * RXJS has some problems, better to use an event handler when settings change and broadcast the event 36 | * temporarily fixed by using a throttle() 37 | * Add popup for ratings, with setting to forget or remind 38 | * Sync settings with Search Companion 39 | * Clean up the way showing/hiding certain buttons work in the Search Companion 40 | * Sometimes OpenAI response is unresponsive, see why, and add a timout with error if possible 41 | * exploit system./user type prompts 42 | * verify DB 43 | * verify default profiles file 44 | * there is a problem that if a message is in progress and you click new conversation, that some stuff stays in memory 45 | * Verify if getFullyQualifiedLanguage() really needs to return en-US, etc (if so, update all supported languages) 46 | * https://www.andiamo.co.uk/resources/iso-language-codes/ 47 | * Use a timeout when sending a request to detect that something is wrong and do a retry 48 | 49 | # Bugs 50 | * DONE :: input.focus() doesn't work always in ConversationsPage 51 | * Extension does not work in Brave (need to use event system for storage) 52 | -------------------------------------------------------------------------------- /_docs/api-documentation.md: -------------------------------------------------------------------------------- 1 | # Usage nomenclature 2 | 3 | ## To call a function in a library use the following syntax: 4 | 5 | `bridge.send('destination.function', data)` 6 | 7 | ```js 8 | bridge.send('background.displayNotification', { 9 | title : 'Hello', 10 | message: 'World' 11 | }) 12 | .then(response => { 13 | // do something 14 | console.log(response.success); 15 | }) 16 | ``` 17 | 18 | * To listen for a function in a library use the following syntax: 19 | 20 | (this should be in the appropriate library file) 21 | ```js 22 | bridge.on('background.displayNotification', ({ data, respond }) => { 23 | chrome.notifications.create('', { 24 | title : data.title, 25 | message : data.message, 26 | iconUrl : data.icon ? data.icon : 'www/logo.png', 27 | type : 'basic' 28 | }); 29 | 30 | respond({ success: true }); 31 | }); 32 | ``` 33 | 34 | ## To listen to an event use the following syntax: 35 | 36 | `bridge.send('origin::event', data)` 37 | 38 | ```js 39 | bridge.send('companionPage::onMounted', { 40 | title : 'Hello', 41 | message: 'World' 42 | }) 43 | .then(response => { 44 | // do something 45 | console.log(response.success); 46 | }) 47 | ``` 48 | 49 | (this can be anywhere in the project) 50 | ```js 51 | bridge.on('companionPage::onMounted', ({ data, respond }) => { 52 | // do something 53 | 54 | respond({ success: true }); 55 | }); 56 | ``` 57 | -------------------------------------------------------------------------------- /_docs/api.md: -------------------------------------------------------------------------------- 1 | * https://platform.openai.com/docs/api-reference/completions/create 2 | 3 | ```js 4 | // options 5 | prompt: { 6 | model : 'text-davinci-003', 7 | prompt : 'Say this is a test', 8 | suffix : null, 9 | max_tokens : 16, 10 | top_p : 1, 11 | n : 1, 12 | stream : false, 13 | logprobs : null, 14 | echo : false, 15 | stop : null, 16 | presence_penalty : 0, 17 | frequency_penalty: 0, 18 | best_of : 1, 19 | logit_bias : null, 20 | user: : '' 21 | } 22 | ``` 23 | 24 | ```js 25 | // stream: false 26 | response: { 27 | "id" : "cmpl-uqkvlQyYK7bGYrRHQ0eXlWi7", 28 | "object" : "text_completion", 29 | "created": 1589478378, 30 | "model" : "text-davinci-003", 31 | "choices": [ 32 | { 33 | "text" : "\n\nThis is indeed a test", 34 | "index" : 0, 35 | "logprobs" : null, 36 | "finish_reason": "length" 37 | } 38 | ], 39 | "usage": { 40 | "prompt_tokens" : 5, 41 | "completion_tokens": 7, 42 | "total_tokens" : 12 43 | } 44 | } 45 | ``` 46 | 47 | ```js 48 | conversation { 49 | timestamp: 1213456 50 | sent : false, 51 | prompt : 'hi there', 52 | template : 'prompt engineering' 53 | } 54 | ``` 55 | 56 | ```js 57 | userSettings: { 58 | triggerMode: 1, 59 | darkMode : false, 60 | apiKey : 'sk-xxxxxx', 61 | licenseKey : 'sk-xxxxxx' 62 | } 63 | ``` 64 | -------------------------------------------------------------------------------- /_docs/store-listing/README.md: -------------------------------------------------------------------------------- 1 | These are the translations used in the store listing (full description) 2 | 3 | [See here for the translations for the extension (name, title, short description](/_docs_/store-listing) 4 | -------------------------------------------------------------------------------- /_docs/store-listing/de.txt: -------------------------------------------------------------------------------- 1 | PowerToys for OpenAI ™ bietet über die offizielle OpenAI™ API ChatGPT-Funktionalität an 2 | 3 | Es richtet sich an Power-User im Büro. 4 | 5 | * Der Suchbegleiter ist in die Suchmaschinenergebnisse integriert 6 | * Der Conversational Manager ist eine vollständige Chat-Schnittstelle mit Verlaufverwaltung 7 | * Der Profil-Manager ermöglicht es Ihnen, den Antworttyp des Modells feinabzustimmen 8 | * Sehen Sie Tokenanzahl 9 | * Sehen Sie die Konversationspreise 10 | * Kopieren Sie Ergebnisse in die Zwischenablage 11 | * Unterstützung für Markdown-Rendering 12 | * Unterstützung für Spracherkennung 13 | * ... und vieles mehr ;-) 14 | 15 | Es ist für Geschwindigkeit optimiert, indem es lokal in Ihrem Browser ausgeführt wird 16 | (keine Serverkosten) 17 | 18 | Die Verwendung dieser Erweiterung erfordert das Vorhandensein eines eigenen API-Schlüssels, der direkt von OpenAI durch Registrierung und Besuch des Entwickler-Dashboards bezogen werden kann 19 | 20 | Verwalten Sie Ihre API-Schlüssel, indem Sie besuchen: 21 | https://platform.openai.com/account/api-keys 22 | 23 | Der Preis liegt bei etwa 0,002 USD pro 750 Wörter oder 1 USD für den Inhalt eines Buches mit 1000 Seiten 24 | (für das Gpt-3.5-Turbo-Model) 25 | 26 | ----------- 27 | Bleiben Sie dran: 28 | https://twitter.com/itechnologynet 29 | -------------------------------------------------------------------------------- /_docs/store-listing/en.txt: -------------------------------------------------------------------------------- 1 | PowerToys for OpenAI ™ provides ChatGPT functionality through the official OpenAI™ API 2 | 3 | Is intended for power users in the office 4 | 5 | * Search Companion is integrated with search engine results 6 | * Conversation Manager is a full chat interface with history management 7 | * Profile manager lets you fine tune the model's response type 8 | * See token count 9 | * See conversation pricing 10 | * Copy results to clipboard 11 | * Markdown rendering support 12 | * Speech recognition support 13 | * ... and more to come ;-) 14 | 15 | It's optimized for speed, by running locally in your browser 16 | (no server costs) 17 | 18 | The use of this extension will require having your own API key, which can be directly obtained from OpenAI by registering and visiting their developer dashboard 19 | 20 | Manage your API keys by visiting: 21 | https://platform.openai.com/account/api-keys 22 | 23 | Pricing is around $0.002 per 750 words, or $1 for the equivalent content of a book of 1000 pages 24 | (for the gpt-3.5-turbo model) 25 | 26 | ----------- 27 | Stay tuned: 28 | https://twitter.com/itechnologynet 29 | -------------------------------------------------------------------------------- /_docs/store-listing/es.txt: -------------------------------------------------------------------------------- 1 | PowerToys for OpenAI ™ proporciona la funcionalidad ChatGPT a través de la API oficial de OpenAI ™ 2 | 3 | Está destinado a usuarios avanzados en la oficina. 4 | 5 | * El buscador compañero está integrado con los resultados del motor de búsqueda 6 | * El Administrador de conversaciones es una interfaz de chat completa con gestión de historial 7 | * El Administrador de perfiles le permite ajustar el tipo de respuesta del modelo 8 | * Ver conteo de token 9 | * Ver precios de conversación 10 | * Copiar resultados al portapapeles 11 | * Soporte de representación Markdown 12 | * Soporte de reconocimiento de voz 13 | * ... ¡y más por venir! ;-) 14 | 15 | Está optimizado para la velocidad, al ejecutarse localmente en su navegador 16 | (sin costos de servidor) 17 | 18 | El uso de esta extensión requerirá tener su propia clave API, que se puede obtener directamente de OpenAI mediante el registro y la visita del panel de desarrollo del desarrollador 19 | 20 | Administre sus claves API visitando: 21 | https://platform.openai.com/account/api-keys 22 | 23 | El precio es de alrededor de $ 0.002 por 750 palabras, o $ 1 por el contenido equivalente a un libro de 1000 páginas 24 | (para el modelo gpt-3.5-turbo) 25 | 26 | ----------- 27 | Manténgase en sintonía: 28 | https://twitter.com/itechnologynet 29 | -------------------------------------------------------------------------------- /_docs/store-listing/fr.txt: -------------------------------------------------------------------------------- 1 | PowerToys for OpenAI ™ fournit des fonctionnalités de ChatGPT via l'API officielle d'OpenAI™ 2 | 3 | Il est destiné aux utilisateurs avancés au bureau 4 | 5 | * Compagnon de recherche est intégré aux résultats des moteurs de recherche 6 | * Le Gestionnaire de Conversation est une interface de chat complète avec une gestion de l'historique 7 | * Le Gestionnaire de Profil vous permet d'affiner le type de réponse du modèle 8 | * Voir le nombre de jetons 9 | * Voir le coût de la conversation 10 | * Copier les résultats dans le presse-papiers 11 | * Support du rendu Markdown 12 | * Prise en charge de la reconnaissance vocale 13 | * ... et plus à venir ;-) 14 | 15 | Il est optimisé pour la vitesse, en s'exécutant localement dans votre navigateur 16 | (sans coût de serveur) 17 | 18 | Pour utiliser cette extension, vous aurez besoin de votre propre clé API qui peut être obtenue directement auprès d'OpenAI en vous inscrivant et en visitant le tableau de bord du développeur 19 | 20 | Gérez vos clés API en visitant: 21 | https://platform.openai.com/account/api-keys 22 | 23 | Le prix est d'environ $0,002 pour 750 mots, ou $1 pour le contenu équivalent d'un livre de 1000 pages 24 | (pour le modèle gpt-3.5-turbo) 25 | 26 | ----------- 27 | Restez informé: 28 | https://twitter.com/itechnologynet 29 | -------------------------------------------------------------------------------- /_docs/store-listing/hi.txt: -------------------------------------------------------------------------------- 1 | पावरटॉय्स फॉर ओपनएआई ™ आधिकारिक ओपनएआई ™ एपीआई के माध्यम से चैटजीपीटी की फ़ंक्शनैलिटी प्रदान करता है 2 | 3 | कार्यालय में पावर यूज़र्स के लिए उद्देश्यित है| 4 | 5 | * सर्च कंपेनियन खोज इंजन परिणामों के साथ एकीकृत है 6 | * कन्वर्सेशन मैनेजर एक पूर्ण चैट इंटरफ़ेस है जिसमें इतिहास प्रबंधन होता है 7 | * प्रोफ़ाइल मैनेजर आपको मॉडल के जवाब के प्रकार को धीमा करने देता है 8 | * टोकन काउंट देखें 9 | * बातचीत मूल्य देखें 10 | * परिणाम को क्लिपबोर्ड में कॉपी करें 11 | * मार्कडाउन रेंडरिंग समर्थन 12 | * वाक्यांश पहचान समर्थन 13 | * ... और और भी आने वाला है ;-) 14 | 15 | यह तेजी से ऑप्टिमाइज़ किया गया है, आपके ब्राउज़र में स्थानीय रूप से चलता है 16 | (कोई सर्वर लागत नहीं) 17 | 18 | एक्सटेंशन का उपयोग करने के लिए आपके पास अपनी खुद की एपीआई कुंजी होनी चाहिए, जो OpenAI से सीधे प्राप्त की जा सकती है, रजिस्टर करके और उनके डेवलपर डैशबोर्ड पर जाकर। 19 | 20 | अपनी एपीआई कुंजियों को विस्तारित करने के लिए अपने खाते पर जाएं: 21 | https://platform.openai.com/account/api-keys 22 | 23 | कीमत लगभग $0.002 प्रति 750 शब्द है, या एक 1000 पृष्ठ की पुस्तक के समकक्ष सामग्री के लिए $1। 24 | (gpt-3.5-turbo मॉडल के लिए) 25 | 26 | ----------- 27 | जुड़े रहें: 28 | https://twitter.com/itechnologynet 29 | -------------------------------------------------------------------------------- /_docs/store-listing/ja.txt: -------------------------------------------------------------------------------- 1 | PowerToys for OpenAI™ は、公式の OpenAI™ API を介して ChatGPT 機能を提供します 2 | 3 | この拡張機能は、オフィスでのパワーユーザーを対象としています 4 | 5 | * 検索コンパニオンは、検索エンジンの検索結果と統合されています 6 | * 会話マネージャーは、履歴管理が可能な完全なチャットインターフェースです 7 | * プロファイルマネージャーを使用すると、モデルの応答タイプを細かく調整できます 8 | * トークン数を確認できます 9 | * 会話の価格を確認できます 10 | * 結果をクリップボードにコピーできます 11 | * Markdown レンダリングサポートがあります 12 | * 音声認識のサポートがあります 13 | * ... さらに多くの機能が追加されます ;-) 14 | 15 | ブラウザでローカルに実行されるため、速度が最適化されています 16 | (サーバー費用はかかりません) 17 | 18 | この拡張機能の使用には、OpenAI から API キーを取得する必要があります。API キーは、開発者ダッシュボードを登録して訪問することで直接取得できます 19 | 20 | API キーの管理方法については、以下を参照してください: 21 | https://platform.openai.com/account/api-keys 22 | 23 | 料金は、750単語あたり約0.002ドル、もしくは1000ページ分のコンテンツに相当する1ドル(gpt-3.5-turboモデルの場合)です 24 | 25 | ----------- 26 | アップデートをお見逃しなく: 27 | https://twitter.com/itechnologynet 28 | -------------------------------------------------------------------------------- /_docs/store-listing/pt_PT.txt: -------------------------------------------------------------------------------- 1 | PowerToys for OpenAI™ fornece funcionalidade ChatGPT através da API oficial da OpenAI™ 2 | 3 | É destinado para usuários avançados no escritório 4 | 5 | * Search Companion está integrado aos resultados do motor de busca 6 | * Conversation Manager é uma interface completa de chat com gerenciamento de histórico 7 | * Profile Manager permite ajustar o tipo de resposta do modelo 8 | * Veja a contagem de tokens 9 | * Veja a precificação da conversa 10 | * Copie os resultados para a área de transferência 11 | * Suporte à renderização Markdown 12 | * Suporte a reconhecimento de voz 13 | * ... e mais por vir ;-) 14 | 15 | É otimizado para velocidade, ao ser executado localmente em seu navegador 16 | (sem custos de servidor) 17 | 18 | O uso desta extensão requer ter sua própria chave de API, que pode ser obtida diretamente da OpenAI ao se registrar e visitar o painel de desenvolvedor 19 | 20 | Gerencie suas chaves API visitando: 21 | https://platform.openai.com/account/api-keys 22 | 23 | O preço fica em cerca de $0,002 por 750 palavras, ou $1 pelo conteúdo equivalente a um livro de 1000 páginas 24 | (para o modelo gpt-3.5-turbo) 25 | 26 | ----------- 27 | Fique ligado: 28 | https://twitter.com/itechnologynet 29 | -------------------------------------------------------------------------------- /_docs/store-listing/ru.txt: -------------------------------------------------------------------------------- 1 | PowerToys for OpenAI™ предоставляет функциональность ChatGPT через официальный API OpenAI™ 2 | 3 | Предназначен для опытных пользователей 4 | 5 | * Поисковый компаньон интегрирован с результатами поисковых систем 6 | * Менеджер диалогов - это полноценный интерфейс чата с управлением историей 7 | * Менеджер профилей позволяет точно настроить тип ответа модели 8 | * Просмотр количества токенов 9 | * Оценка цены разговора 10 | * Копирование результатов в буфер обмена 11 | * Поддержка рендеринга в формате Markdown 12 | * Поддержка распознавания речи 13 | * ... и многое другое в будущем ;-) 14 | 15 | Оптимизирован для производительности и запускается локально в вашем браузере 16 | (без затрат на сервер) 17 | 18 | Для использования этого расширения необходимо иметь собственный API-ключ, который можно получить непосредственно у OpenAI, зарегистрировавшись и посетив их панель разработчика 19 | 20 | Управление ключами API осуществляется на сайте: 21 | https://platform.openai.com/account/api-keys 22 | 23 | Цена составляет около $0.002 за 750 слов, или $1 за эквивалент содержания книги в 1000 страниц 24 | (для модели gpt-3.5-turbo) 25 | 26 | ----------- 27 | Следите за новостями: 28 | https://twitter.com/itechnologynet 29 | -------------------------------------------------------------------------------- /_docs/store-listing/tr.txt: -------------------------------------------------------------------------------- 1 | PowerToys for OpenAI™, resmi OpenAI™ API'si aracılığıyla ChatGPT işlevselliği sağlar 2 | 3 | Ofiste güç kullanıcıları için tasarlanmıştır: 4 | 5 | * Arama Eşlikçisi arama motoru sonuçlarıyla entegre edilir 6 | * Konuşma Yöneticisi, geçmiş yönetimiyle tam bir sohbet arayüzüdür 7 | * Profiller, modelin yanıt türünü ince ayar yapmanızı sağlar 8 | * Token sayısını görüntüleyin 9 | * Konuşma fiyatlandırmasını görüntüleyin 10 | * Sonuçları panoya kopyalama 11 | * Markdown işaretleme desteği 12 | * Konuşma tanıma desteği 13 | * ve daha fazlası ;-) 14 | 15 | Hız için optimize edilmiştir, tarayıcınızda yerel olarak çalışarak 16 | (sunucu maliyeti yok) 17 | 18 | Bu uzantının kullanımı, OpenAI'den doğrudan API anahtarınızın olması gerektirir, bu anahtar kayıt olmanız ve geliştirici panosunu ziyaret etmenizle doğrudan OpenAI'den elde edilebilir. 19 | 20 | API anahtarlarınızı yönetmek için şurayı ziyaret edin: 21 | https://platform.openai.com/account/api-keys 22 | 23 | Fiyatlandırma, 750 kelime başına yaklaşık 0,002 $ veya 1000 sayfalık bir kitaba eşdeğer olan içerik için 1 $ civarındadır 24 | (gpt-3.5-turbo modeli için) 25 | 26 | ----------- 27 | Güncel kalmaya devam edin: 28 | https://twitter.com/itechnologynet 29 | -------------------------------------------------------------------------------- /_docs/store-listing/zh_CN.txt: -------------------------------------------------------------------------------- 1 | PowerToys for OpenAI ™ 通过官方 OpenAI™ API 提供 ChatGPT 功能 2 | 3 | 旨在为办公室中的高级用户提供服务 4 | 5 | * 搜索助手与搜索引擎结果集成 6 | * 会话管理器为带有历史管理的完整聊天界面 7 | * 配置管理器可让您微调模型的响应类型 8 | * 查看标记计数 9 | * 查看会话定价 10 | * 复制结果到剪贴板 11 | * 支持 Markdown 渲染 12 | * 支持语音识别 13 | * ...等等功能正在增加 ;-) 14 | 15 | 它经过速度优化,在本地浏览器中运行 16 | (无服务器成本) 17 | 18 | 使用此扩展需要拥有自己的 API 密钥,可以通过注册并访问其开发者仪表板从 OpenAI 直接获取 19 | 20 | 通过访问 https://platform.openai.com/account/api-keys 管理您的 API 密钥 21 | 22 | 价格大约为每750个单词0.002美元,或相当于一本1000页书的内容1美元 23 | (针对 gpt-3.5-turbo 模型) 24 | 25 | ----------------- 26 | 敬请关注: 27 | https://twitter.com/itechnologynet 28 | -------------------------------------------------------------------------------- /_docs/translations.md: -------------------------------------------------------------------------------- 1 | ### How to translate (or fix a typo) 2 | 3 | * **i18n/en/index.ts** contains all the english translations 4 | * **i18n/fr/index.ts** contains all the french translations 5 | * **i18n/types.ts** is used in the application to provide **strongly typed** translations, instead of using *magic strings* 6 | * **_docs/store-listing** contains the translations for the app store (full description) 7 | * **src-bex/_locales contains** translations for the for the app store (name, title, short description) 8 | 9 | 10 | #### To add a translation, you must 11 | 12 | * Fork the project 13 | * Duplicate the existing folders or files, for example copy en/* to de/* 14 | * Translate all entries to the new language 15 | * Make a pull request 16 | 17 | _**i18n/types.ts should not need to be touched, unless adding new entries**_ 18 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <%= productName %> 5 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "powerai", 3 | "version": "0.3.0", 4 | "description": "Productivity tools for OpenAI's API", 5 | "productName": "PowerToys for OpenAI ™", 6 | "author": "Robert Hoffmann ", 7 | "private": true, 8 | "baseUrl": "https://api.openai.com", 9 | "scripts": { 10 | "dev": "quasar dev -m bex", 11 | "build": "quasar build -m bex" 12 | }, 13 | "dependencies": { 14 | "@quasar/extras": "^1.16.7", 15 | "@vueuse/core": "^10.4.1", 16 | "dexie": "^3.2.4", 17 | "events": "^3.3.0", 18 | "eventsource-parser": "^1.1.1", 19 | "i18next": "^23.5.1", 20 | "i18next-browser-languagedetector": "^7.1.0", 21 | "i18next-vue": "^3.0.0", 22 | "markdown-it": "^13.0.2", 23 | "quasar": "^2.12.7", 24 | "vue": "^3.3.4", 25 | "vue-router": "^4.2.5" 26 | }, 27 | "devDependencies": { 28 | "@quasar/app-vite": "^1.6.2", 29 | "@types/markdown-it": "^13.0.2", 30 | "@types/node": "^18.16.0", 31 | "@typescript-eslint/eslint-plugin": "^5.59.0", 32 | "@typescript-eslint/parser": "^5.59.0", 33 | "autoprefixer": "^10.4.16", 34 | "eslint": "^8.40.0", 35 | "eslint-config-prettier": "^8.8.0", 36 | "eslint-plugin-vue": "^9.12.0", 37 | "postcss": "^8.4.31", 38 | "prettier": "^3.0.3", 39 | "typescript": "4.9.5" 40 | }, 41 | "engines": { 42 | "node": "^20 || ^18", 43 | "npm": ">= 6.13.4", 44 | "yarn": ">= 1.21.1" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // https://github.com/michael-ciniawsky/postcss-load-config 3 | 4 | module.exports = { 5 | plugins: [ 6 | // https://github.com/postcss/autoprefixer 7 | require('autoprefixer')({ 8 | overrideBrowserslist: [ 9 | 'last 4 Chrome versions', 10 | 'last 4 Firefox versions', 11 | 'last 4 Edge versions', 12 | 'last 4 Safari versions', 13 | 'last 4 Android versions', 14 | 'last 4 ChromeAndroid versions', 15 | 'last 4 FirefoxAndroid versions', 16 | 'last 4 iOS versions' 17 | ] 18 | }) 19 | 20 | // https://github.com/elchininet/postcss-rtlcss 21 | // If you want to support RTL css, then 22 | // 1. yarn/npm install postcss-rtlcss 23 | // 2. optionally set quasar.config.js > framework > lang to an RTL language 24 | // 3. uncomment the following line: 25 | // require('postcss-rtlcss') 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /profiles/README.md: -------------------------------------------------------------------------------- 1 | **These are the default profiles that are loaded when the application is installed** 2 | 3 | There will probaly be a system where profiles can be imported from the web (via JSON) and you can select them directly in the application 4 | Those files will be hosted here 5 | -------------------------------------------------------------------------------- /public/_readme.md: -------------------------------------------------------------------------------- 1 | ## Accessible via: 2 | * Inside Chrome extension 3 | * www/* 4 | * Inside *.vue files 5 | * /* 6 | -------------------------------------------------------------------------------- /public/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robert-hoffmann/PowerToys4OpenAI/e8aa87e64abe4ba9a11b99f984e6dd0429358f90/public/avatar.png -------------------------------------------------------------------------------- /public/donations/bitcoin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robert-hoffmann/PowerToys4OpenAI/e8aa87e64abe4ba9a11b99f984e6dd0429358f90/public/donations/bitcoin.png -------------------------------------------------------------------------------- /public/donations/ethereum.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robert-hoffmann/PowerToys4OpenAI/e8aa87e64abe4ba9a11b99f984e6dd0429358f90/public/donations/ethereum.png -------------------------------------------------------------------------------- /public/donations/gumroad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robert-hoffmann/PowerToys4OpenAI/e8aa87e64abe4ba9a11b99f984e6dd0429358f90/public/donations/gumroad.png -------------------------------------------------------------------------------- /public/donations/paypal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robert-hoffmann/PowerToys4OpenAI/e8aa87e64abe4ba9a11b99f984e6dd0429358f90/public/donations/paypal.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robert-hoffmann/PowerToys4OpenAI/e8aa87e64abe4ba9a11b99f984e6dd0429358f90/public/favicon.ico -------------------------------------------------------------------------------- /public/icons/favicon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robert-hoffmann/PowerToys4OpenAI/e8aa87e64abe4ba9a11b99f984e6dd0429358f90/public/icons/favicon-128x128.png -------------------------------------------------------------------------------- /public/icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robert-hoffmann/PowerToys4OpenAI/e8aa87e64abe4ba9a11b99f984e6dd0429358f90/public/icons/favicon-16x16.png -------------------------------------------------------------------------------- /public/icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robert-hoffmann/PowerToys4OpenAI/e8aa87e64abe4ba9a11b99f984e6dd0429358f90/public/icons/favicon-32x32.png -------------------------------------------------------------------------------- /public/icons/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robert-hoffmann/PowerToys4OpenAI/e8aa87e64abe4ba9a11b99f984e6dd0429358f90/public/icons/favicon-96x96.png -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robert-hoffmann/PowerToys4OpenAI/e8aa87e64abe4ba9a11b99f984e6dd0429358f90/public/logo.png -------------------------------------------------------------------------------- /public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /public/robot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robert-hoffmann/PowerToys4OpenAI/e8aa87e64abe4ba9a11b99f984e6dd0429358f90/public/robot.png -------------------------------------------------------------------------------- /public/socials/github.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /public/socials/linkedin.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /public/socials/twitter.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /quasar.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 3 | /* 4 | * This file runs in a Node context (it's NOT transpiled by Babel), so use only 5 | * the ES6 features that are supported by your Node version. https://node.green/ 6 | */ 7 | 8 | // Configuration for your app 9 | // https://v2.quasar.dev/quasar-cli-vite/quasar-config-js 10 | 11 | const { configure } = require('quasar/wrappers'); 12 | 13 | module.exports = configure(function (/* ctx */) { 14 | return { 15 | eslint: { 16 | // fix: true, 17 | // include = [], 18 | // exclude = [], 19 | // rawOptions = {}, 20 | warnings: true, 21 | errors: true 22 | }, 23 | 24 | // https://v2.quasar.dev/quasar-cli-vite/prefetch-feature 25 | // preFetch: true, 26 | 27 | // app boot file (/src/boot) 28 | // --> boot files are part of "main.js" 29 | // https://v2.quasar.dev/quasar-cli-vite/boot-files 30 | boot: [ 31 | 'i18next', 32 | ], 33 | 34 | // https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#css 35 | css: [ 36 | 'app.scss' 37 | ], 38 | 39 | // https://github.com/quasarframework/quasar/tree/dev/extras 40 | extras: [ 41 | // 'ionicons-v4', 42 | // 'mdi-v5', 43 | // 'fontawesome-v6', 44 | // 'eva-icons', 45 | // 'themify', 46 | // 'line-awesome', 47 | // 'roboto-font-latin-ext', // this or either 'roboto-font', NEVER both! 48 | 49 | 'roboto-font', // optional, you are not bound to it 50 | 'material-icons', // optional, you are not bound to it 51 | 'material-icons-outlined', 52 | ], 53 | 54 | // Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#build 55 | build: { 56 | target: { 57 | browser: [ 'es2019', 'edge98', 'chrome97' ], 58 | node: 'node20' 59 | }, 60 | 61 | vueRouterMode: 'hash', // available values: 'hash', 'history' 62 | // vueRouterBase, 63 | // vueDevtools, 64 | // vueOptionsAPI: false, 65 | 66 | // rebuildCache: true, // rebuilds Vite/linter/etc cache on startup 67 | 68 | // publicPath: '/', 69 | // analyze: true, 70 | // env: {}, 71 | // rawDefine: {} 72 | // ignorePublicFolder: true, 73 | minify: false, 74 | // polyfillModulePreload: true, 75 | // distDir 76 | extendViteConf (viteConf) { 77 | // INFO: https://stackoverflow.com/a/74019163/896849 78 | viteConf.build.rollupOptions = { 79 | output:{ 80 | manualChunks(id) { 81 | if (id.includes('node_modules')) { 82 | return id.toString().split('node_modules/')[1].split('/')[0].toString(); 83 | } 84 | } 85 | } 86 | }; 87 | }, 88 | // viteVuePluginOptions: {}, 89 | // vitePlugins: [] 90 | }, 91 | 92 | // Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#devServer 93 | devServer: { 94 | // https: true 95 | open: true // opens browser window automatically 96 | }, 97 | 98 | // https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#framework 99 | framework: { 100 | config: {}, 101 | 102 | // iconSet: 'material-icons', // Quasar icon set 103 | // lang: 'en-US', // Quasar language pack 104 | 105 | // For special cases outside of where the auto-import strategy can have an impact 106 | // (like functional components as one of the examples), 107 | // you can manually specify Quasar components/directives to be available everywhere: 108 | // 109 | // components: [], 110 | // directives: [], 111 | 112 | // Quasar plugins 113 | plugins: ['Notify'] 114 | }, 115 | 116 | // animations: 'all', // --- includes all animations 117 | // https://v2.quasar.dev/options/animations 118 | animations: [], 119 | 120 | // https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#sourcefiles 121 | // sourceFiles: { 122 | // rootComponent: 'src/App.vue', 123 | // router: 'src/router/index', 124 | // store: 'src/store/index', 125 | // registerServiceWorker: 'src-pwa/register-service-worker', 126 | // serviceWorker: 'src-pwa/custom-service-worker', 127 | // pwaManifestFile: 'src-pwa/manifest.json', 128 | // electronMain: 'src-electron/electron-main', 129 | // electronPreload: 'src-electron/electron-preload' 130 | // }, 131 | 132 | // https://v2.quasar.dev/quasar-cli-vite/developing-ssr/configuring-ssr 133 | ssr: { 134 | // ssrPwaHtmlFilename: 'offline.html', // do NOT use index.html as name! 135 | // will mess up SSR 136 | 137 | // extendSSRWebserverConf (esbuildConf) {}, 138 | // extendPackageJson (json) {}, 139 | 140 | pwa: false, 141 | 142 | // manualStoreHydration: true, 143 | // manualPostHydrationTrigger: true, 144 | 145 | prodPort: 3000, // The default port that the production server should use 146 | // (gets superseded if process.env.PORT is specified at runtime) 147 | 148 | middlewares: [ 149 | 'render' // keep this as last one 150 | ] 151 | }, 152 | 153 | // https://v2.quasar.dev/quasar-cli-vite/developing-pwa/configuring-pwa 154 | pwa: { 155 | workboxMode: 'generateSW', // or 'injectManifest' 156 | injectPwaMetaTags: true, 157 | swFilename: 'sw.js', 158 | manifestFilename: 'manifest.json', 159 | useCredentialsForManifestTag: false, 160 | // useFilenameHashes: true, 161 | // extendGenerateSWOptions (cfg) {} 162 | // extendInjectManifestOptions (cfg) {}, 163 | // extendManifestJson (json) {} 164 | // extendPWACustomSWConf (esbuildConf) {} 165 | }, 166 | 167 | // Full list of options: https://v2.quasar.dev/quasar-cli-vite/developing-cordova-apps/configuring-cordova 168 | cordova: { 169 | // noIosLegacyBuildFlag: true, // uncomment only if you know what you are doing 170 | }, 171 | 172 | // Full list of options: https://v2.quasar.dev/quasar-cli-vite/developing-capacitor-apps/configuring-capacitor 173 | capacitor: { 174 | hideSplashscreen: true 175 | }, 176 | 177 | // Full list of options: https://v2.quasar.dev/quasar-cli-vite/developing-electron-apps/configuring-electron 178 | electron: { 179 | // extendElectronMainConf (esbuildConf) 180 | // extendElectronPreloadConf (esbuildConf) 181 | 182 | inspectPort: 5858, 183 | 184 | bundler: 'packager', // 'packager' or 'builder' 185 | 186 | packager: { 187 | // https://github.com/electron-userland/electron-packager/blob/master/docs/api.md#options 188 | 189 | // OS X / Mac App Store 190 | // appBundleId: '', 191 | // appCategoryType: '', 192 | // osxSign: '', 193 | // protocol: 'myapp://path', 194 | 195 | // Windows only 196 | // win32metadata: { ... } 197 | }, 198 | 199 | builder: { 200 | // https://www.electron.build/configuration/configuration 201 | 202 | appId: 'powerai' 203 | } 204 | }, 205 | 206 | // Full list of options: https://v2.quasar.dev/quasar-cli-vite/developing-browser-extensions/configuring-bex 207 | bex: { 208 | contentScripts: [ 209 | 'content' 210 | ], 211 | 212 | // extendBexScriptsConf (esbuildConf) {} 213 | // extendBexManifestJson (json) {} 214 | } 215 | } 216 | }); 217 | -------------------------------------------------------------------------------- /sites-detection/README.md: -------------------------------------------------------------------------------- 1 | ### Configuration file for detecting sites 2 | 3 | This file holds all the information needed to inject the Search Companion into a site and *how* it should be injected 4 | 5 | **Walkthrough:** 6 | 7 | - Detect URL to see if we have a match 8 | - Should it be injected as a sidebar or inline 9 | - If inline, see if we have a match to get the user input query 10 | - Look if we have a container to attach to 11 | - If so, get the configuration info to apply for that container 12 | - append / prepend 13 | - any classes to add 14 | - any styles to apply 15 | - this is how we make sure padding, etc is ok 16 | 17 | *Maybe in a future version we can add this directly to the interface* 18 | -------------------------------------------------------------------------------- /sites-detection/sites-detection.ts: -------------------------------------------------------------------------------- 1 | export interface SearchEngines { 2 | /** Website name */ 3 | name: string, 4 | 5 | /** If URL matches, then module will be loaded */ 6 | url: RegExp, 7 | 8 | /** Possible target containers */ 9 | targets: Configuration[], 10 | 11 | /** If site supports sidebar injection **/ 12 | addSidebar: boolean 13 | }[] 14 | 15 | export interface Configuration { 16 | /** 17 | * From where to extract the users search phrase 18 | * @type {query selector} 19 | * */ 20 | userInputs: string[], 21 | 22 | /** Array of possible targets of where the module will be attached to in the DOM */ 23 | containers: string[], 24 | 25 | /** How to add the module to the selector */ 26 | method: 'append' | 'prepend', 27 | 28 | /** Classes to add to the component */ 29 | classes?: string[], 30 | 31 | /** Styles to apply to the component */ 32 | styles?: { 33 | [key: string]: string 34 | }[] 35 | } 36 | 37 | export const config: SearchEngines[] = [ 38 | { 39 | name : 'Google Search', 40 | url : /https?:\/\/.*(google)\.[a-z]{2,3}\/search\?.*(q=.*)/, 41 | addSidebar : false, 42 | targets: [ 43 | { 44 | userInputs : [ 45 | "input[name='q']", 46 | "textarea[name='q']" 47 | ], 48 | containers: [ 49 | '#rhs' 50 | ], 51 | method : 'prepend', 52 | classes : [ 53 | 'ptoia-companion-prepend' 54 | ] 55 | }, 56 | { 57 | userInputs : [ 58 | "input[name='q']", 59 | "textarea[name='q']" 60 | ], 61 | containers: [ 62 | '#rcnt' 63 | ], 64 | method : 'append', 65 | classes : [ 66 | 'ptoia-companion-sidecontent', 67 | 'append' 68 | ], 69 | styles: [ 70 | { marginLeft: '30px' }, 71 | { width : '367px' } 72 | ] 73 | } 74 | ], 75 | }, 76 | { 77 | name : 'Bing Search', 78 | url : /https?:\/\/.*(bing)\.[a-z]{2,3}\/search\?.*(q=.*)/, 79 | addSidebar : false, 80 | targets: [ 81 | { 82 | userInputs : [ 83 | "[name='q']" 84 | ], 85 | containers: [ 86 | '#b_context' 87 | ], 88 | method : 'prepend', 89 | classes : [ 90 | 'ptoia-companion-prepend' 91 | ], 92 | styles: [ 93 | { margin: '0px 0px 20px -20px' }, 94 | { width : '108%' } 95 | ] 96 | } 97 | ], 98 | }, 99 | { 100 | name : 'Yahoo Search', 101 | url : /https?:\/\/.*(search\.yahoo)\.[a-z]{2,3}\/search\?.*(p=.*)/, 102 | addSidebar : false, 103 | targets: [ 104 | { 105 | userInputs : [ 106 | "[name='p']" 107 | ], 108 | containers: [ 109 | '#right', 110 | '.Contents__inner.Contents__inner--sub' 111 | ], 112 | method : 'prepend', 113 | classes : [ 114 | 'ptoia-companion-prepend' 115 | ], 116 | styles: [ 117 | { margin: '0px 0px 20px 0px' }, 118 | { width : '370px' } 119 | ] 120 | }, 121 | { 122 | userInputs : [ 123 | "[name='p']" 124 | ], 125 | containers: [ 126 | '#cols', 127 | '#contents__wrap' 128 | ], 129 | method : 'append', 130 | classes : [ 131 | 'ptoia-companion-sidecontent', 132 | 'append' 133 | ], 134 | styles: [ 135 | { margin: '0px 0px 20px 0px' }, 136 | { width : '370px' } 137 | ] 138 | } 139 | ], 140 | }, 141 | { 142 | name : 'DuckDuckGo Search', 143 | url : /https?:\/\/.*(duckduckgo)\.[a-z]{2,3}\/\?.*(q=.*)/, 144 | addSidebar : false, 145 | targets: [ 146 | { 147 | userInputs : [ 148 | "input[name='q']" 149 | ], 150 | containers: [ 151 | '.results--sidebar.js-results-sidebar' 152 | ], 153 | method : 'prepend', 154 | classes : [ 155 | 'ptoia-companion-prepend' 156 | ], 157 | styles: [ 158 | { margin: '0 0 10px 0' }, 159 | { width : '100%' } 160 | ] 161 | }, 162 | { 163 | userInputs : [ 164 | "input[name='q']" 165 | ], 166 | containers: [ 167 | '#links_wrapper' 168 | ], 169 | method : 'append', 170 | classes : [ 171 | 'ptoia-companion-sidecontent', 172 | 'append' 173 | ], 174 | styles: [ 175 | { margin: '0 0 10px 0' }, 176 | { width : '100%' } 177 | ] 178 | } 179 | ], 180 | }, 181 | { 182 | name : 'Baidu Search', 183 | url : /https?:\/\/.*(baidu)\.[a-z]{2,3}\/s?.*/, 184 | addSidebar : false, 185 | targets: [ 186 | { 187 | userInputs : [ 188 | "input[name='wd']" 189 | ], 190 | containers: [ 191 | '#content_right' 192 | ], 193 | method : 'prepend', 194 | classes : [ 195 | 'ptoia-companion-prepend' 196 | ], 197 | styles: [ 198 | { width : '367px' } 199 | ] 200 | }, 201 | { 202 | userInputs : [ 203 | "input[name='wd']" 204 | ], 205 | containers: [ 206 | '#container' 207 | ], 208 | method : 'append', 209 | classes : [ 210 | 'ptoia-companion-sidecontent', 211 | 'append' 212 | ], 213 | styles: [ 214 | { width : '367px' } 215 | ] 216 | } 217 | ], 218 | }, 219 | { 220 | name : 'Yandex Search', 221 | url : /https?:\/\/.*(yandex)\.[a-z]{2,3}\/search\/\?.*(text=.*)/, 222 | addSidebar : false, 223 | targets: [ 224 | { 225 | userInputs : [ 226 | "input[name='text']" 227 | ], 228 | containers: [ 229 | '#search-result-aside' 230 | ], 231 | method : 'prepend', 232 | classes : [ 233 | 'ptoia-companion-prepend' 234 | ], 235 | styles: [ 236 | { margin: '0 0 10px 0' }, 237 | { width : '100%' } 238 | ] 239 | } 240 | ], 241 | }, 242 | { 243 | name : 'Naver Search', 244 | url : /https?:\/\/.*(search\.naver)\.[a-z]{2,3}\/search.naver\?.*/, 245 | addSidebar : false, 246 | targets: [ 247 | { 248 | userInputs : [ 249 | "input[name='query']" 250 | ], 251 | containers: [ 252 | '#sub_pack' 253 | ], 254 | method : 'prepend', 255 | classes : [ 256 | 'ptoia-companion-prepend' 257 | ], 258 | styles: [ 259 | { margin: '0px 0px 15px 16px' }, 260 | { width : '400px' } 261 | ] 262 | }, 263 | { 264 | userInputs : [ 265 | "input[name='query']" 266 | ], 267 | containers: [ 268 | '#content' 269 | ], 270 | method : 'append', 271 | classes : [ 272 | 'ptoia-companion-sidecontent', 273 | 'append' 274 | ], 275 | styles: [ 276 | { margin: '0px 0px 15px 16px' }, 277 | { width : '400px' } 278 | ] 279 | } 280 | ], 281 | }, 282 | { 283 | name : 'Brave Search', 284 | url : /https?:\/\/.*(search\.brave)\.[a-z]{2,3}\/search\?.*(q=.*)/, 285 | addSidebar : false, 286 | targets: [ 287 | { 288 | userInputs : [ 289 | "[name='q']" 290 | ], 291 | containers: [ 292 | '#side-right'], 293 | method : 'prepend', 294 | styles: [ 295 | { margin: '0px 0px 20px 0px' }, 296 | { width : '370px' } 297 | ] 298 | } 299 | ], 300 | }, 301 | { 302 | name : 'SearX Search', 303 | url : /https?:\/\/.*(searx\.thegpm)\.[a-z]{2,3}\/.*/, 304 | addSidebar : false, 305 | targets: [ 306 | { 307 | userInputs : [ 308 | "input[name='q']" 309 | ], 310 | containers: [ 311 | '#sidebar_results'], 312 | method : 'prepend', 313 | styles: [ 314 | { width : '360px' } 315 | ] 316 | } 317 | ], 318 | }, 319 | { 320 | name : 'Other Sites', 321 | url : /https?:\/\/(.*)\.[a-z]{2,3}/i, 322 | addSidebar : true, 323 | targets: [ 324 | { 325 | userInputs : [], 326 | containers : [ 327 | 'body' 328 | ], 329 | method : 'append', 330 | styles: [ 331 | { margin : '0px 0px 20px 0px' }, 332 | { width : '450px' }, 333 | { position : 'absolute' }, 334 | { top : '150px' }, 335 | { right : '0' }, 336 | { zIndex : '9999' }, 337 | ] 338 | } 339 | ], 340 | }, 341 | ] 342 | -------------------------------------------------------------------------------- /src-bex/_locales/de/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": { 3 | "message" : "PowerToys for OpenAI ™", 4 | "description" : "name" 5 | }, 6 | "shortName": { 7 | "message" : "PowerToys", 8 | "description" : "shortName" 9 | }, 10 | "description": { 11 | "message" : "Produktivitätswerkzeuge für OpenAI-APIs", 12 | "description" : "description" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src-bex/_locales/en/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": { 3 | "message" : "PowerToys for OpenAI ™", 4 | "description" : "name" 5 | }, 6 | "shortName": { 7 | "message" : "PowerToys", 8 | "description" : "shortName" 9 | }, 10 | "description": { 11 | "message" : "Productivity tools for OpenAI API's", 12 | "description" : "description" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src-bex/_locales/es/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": { 3 | "message" : "PowerToys for OpenAI ™", 4 | "description" : "name" 5 | }, 6 | "shortName": { 7 | "message" : "PowerToys", 8 | "description" : "shortName" 9 | }, 10 | "description": { 11 | "message" : "Herramientas de productividad para las API de OpenAI", 12 | "description" : "description" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src-bex/_locales/fr/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": { 3 | "message" : "PowerToys for OpenAI ™", 4 | "description" : "name" 5 | }, 6 | "shortName": { 7 | "message" : "PowerToys", 8 | "description" : "shortName" 9 | }, 10 | "description": { 11 | "message" : "Outils de productivité pour les API's d'OpenAI™", 12 | "description" : "description" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src-bex/_locales/hi/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": { 3 | "message" : "PowerToys for OpenAI ™", 4 | "description" : "name" 5 | }, 6 | "shortName": { 7 | "message" : "PowerToys", 8 | "description" : "shortName" 9 | }, 10 | "description": { 11 | "message" : "ओपनएआई एपीआई के लिए उत्पादकता टूल्स।", 12 | "description" : "description" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src-bex/_locales/ja/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": { 3 | "message" : "PowerToys for OpenAI ™", 4 | "description" : "name" 5 | }, 6 | "shortName": { 7 | "message" : "PowerToys", 8 | "description" : "shortName" 9 | }, 10 | "description": { 11 | "message" : "OpenAI API のための生産性向上ツール", 12 | "description" : "description" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src-bex/_locales/pt_AO/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": { 3 | "message" : "PowerToys for OpenAI ™", 4 | "description" : "name" 5 | }, 6 | "shortName": { 7 | "message" : "PowerToys", 8 | "description" : "shortName" 9 | }, 10 | "description": { 11 | "message" : "Ferramentas de produtividade para APIs do OpenAI", 12 | "description" : "description" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src-bex/_locales/pt_BR/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": { 3 | "message" : "PowerToys for OpenAI ™", 4 | "description" : "name" 5 | }, 6 | "shortName": { 7 | "message" : "PowerToys", 8 | "description" : "shortName" 9 | }, 10 | "description": { 11 | "message" : "Ferramentas de produtividade para APIs do OpenAI", 12 | "description" : "description" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src-bex/_locales/pt_CV/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": { 3 | "message" : "PowerToys for OpenAI ™", 4 | "description" : "name" 5 | }, 6 | "shortName": { 7 | "message" : "PowerToys", 8 | "description" : "shortName" 9 | }, 10 | "description": { 11 | "message" : "Ferramentas de produtividade para APIs do OpenAI", 12 | "description" : "description" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src-bex/_locales/pt_GW/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": { 3 | "message" : "PowerToys for OpenAI ™", 4 | "description" : "name" 5 | }, 6 | "shortName": { 7 | "message" : "PowerToys", 8 | "description" : "shortName" 9 | }, 10 | "description": { 11 | "message" : "Ferramentas de produtividade para APIs do OpenAI", 12 | "description" : "description" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src-bex/_locales/pt_MZ/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": { 3 | "message" : "PowerToys for OpenAI ™", 4 | "description" : "name" 5 | }, 6 | "shortName": { 7 | "message" : "PowerToys", 8 | "description" : "shortName" 9 | }, 10 | "description": { 11 | "message" : "Ferramentas de produtividade para APIs do OpenAI", 12 | "description" : "description" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src-bex/_locales/pt_PT/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": { 3 | "message" : "PowerToys for OpenAI ™", 4 | "description" : "name" 5 | }, 6 | "shortName": { 7 | "message" : "PowerToys", 8 | "description" : "shortName" 9 | }, 10 | "description": { 11 | "message" : "Ferramentas de produtividade para APIs do OpenAI", 12 | "description" : "description" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src-bex/_locales/pt_ST/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": { 3 | "message" : "PowerToys for OpenAI ™", 4 | "description" : "name" 5 | }, 6 | "shortName": { 7 | "message" : "PowerToys", 8 | "description" : "shortName" 9 | }, 10 | "description": { 11 | "message" : "Ferramentas de produtividade para APIs do OpenAI", 12 | "description" : "description" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src-bex/_locales/pt_TL/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": { 3 | "message" : "PowerToys for OpenAI ™", 4 | "description" : "name" 5 | }, 6 | "shortName": { 7 | "message" : "PowerToys", 8 | "description" : "shortName" 9 | }, 10 | "description": { 11 | "message" : "Ferramentas de produtividade para APIs do OpenAI", 12 | "description" : "description" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src-bex/_locales/ru/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": { 3 | "message" : "PowerToys for OpenAI ™", 4 | "description" : "name" 5 | }, 6 | "shortName": { 7 | "message" : "PowerToys", 8 | "description" : "shortName" 9 | }, 10 | "description": { 11 | "message" : "Инструменты для продуктивности с помощью OpenAI API", 12 | "description" : "description" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src-bex/_locales/tr/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": { 3 | "message" : "PowerToys for OpenAI ™", 4 | "description" : "name" 5 | }, 6 | "shortName": { 7 | "message" : "PowerToys", 8 | "description" : "shortName" 9 | }, 10 | "description": { 11 | "message" : "OpenAI API'ları için Üretkenlik Araçları", 12 | "description" : "description" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src-bex/_locales/zh_CN/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": { 3 | "message" : "PowerToys for OpenAI ™", 4 | "description" : "name" 5 | }, 6 | "shortName": { 7 | "message" : "PowerToys", 8 | "description" : "shortName" 9 | }, 10 | "description": { 11 | "message" : "OpenAI API 的生产力工具", 12 | "description" : "description" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src-bex/_locales/zh_SG/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": { 3 | "message" : "PowerToys for OpenAI ™", 4 | "description" : "name" 5 | }, 6 | "shortName": { 7 | "message" : "PowerToys", 8 | "description" : "shortName" 9 | }, 10 | "description": { 11 | "message" : "OpenAI API 的生产力工具", 12 | "description" : "description" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src-bex/assets/content.css: -------------------------------------------------------------------------------- 1 | /* Global CSS used within your BEX. This is not preprocessed so this has to be pure CSS. */ 2 | div#ptoia-companion-container { 3 | border-width : 1px; 4 | border-style : solid; 5 | border-color : initial; 6 | border-image : initial; 7 | border-radius : 8px; 8 | padding : 0; 9 | margin-bottom : 10px; 10 | 11 | color : rgb(0, 0, 0); 12 | background-color : rgb(255, 255, 255); 13 | border-color : rgb(218, 220, 224); 14 | } 15 | div#ptoia-companion-container.ptoia-companion-sidecontent { 16 | margin-left : 30px; 17 | } 18 | 19 | /* #region ---------------- contextMenu ---------------- */ 20 | div#ptoia-context-menu { 21 | display : none; 22 | 23 | position : absolute; 24 | z-index : 9999999; 25 | top : 0; 26 | left : 0; 27 | 28 | border: 1px solid black; 29 | } 30 | div.ptoia-context-menu-item { 31 | font-size : 14px; 32 | font-family: 'Roboto'; 33 | 34 | display : block; 35 | background-color : white; 36 | color : black; 37 | cursor : pointer; 38 | 39 | padding : 5px; 40 | height : 25px; 41 | width : 250px; 42 | } 43 | 44 | div.ptoia-context-menu-item { 45 | border-top: 1px solid black; 46 | } 47 | div.ptoia-context-menu-item:first-child { 48 | border-top: none; 49 | } 50 | 51 | div.ptoia-context-menu-item:hover { 52 | background-color: lightgray; 53 | } 54 | 55 | .ptoia-sidebar-closed { 56 | width: 10px !important; 57 | } 58 | /* #endregion ------------- contextMenu ---------------- */ 59 | -------------------------------------------------------------------------------- /src-bex/background.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * INFO: This is the background script for the extension. 3 | * 4 | * It's loaded once when the extension is installed. 5 | * It has access to the chrome.* APIs. 6 | * 7 | */ 8 | import { bexBackground } from 'quasar/wrappers'; 9 | import { BexBridge } from '@quasar/app-vite/types/bex'; 10 | import { openAi } from 'src/js/api/openai.api'; 11 | 12 | import { db } from 'src/js/controllers/dbController'; 13 | import defaultProfiles from 'src/js/data/default-profiles.json'; 14 | 15 | // Used to cancel the Fetch request if the user closes the popup. 16 | // https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal 17 | let controller: AbortController | null = null; 18 | 19 | chrome.runtime.onInstalled.addListener((details) => { 20 | // open options on first install 21 | if (details.reason === 'install') { 22 | // open settings 23 | chrome.runtime.openOptionsPage(); 24 | 25 | // load defaults 26 | loadDefaults(); 27 | } 28 | 29 | // open when updated 30 | if(details.reason === 'update') { 31 | // show changelog 32 | chrome.tabs.create({url: 'https://github.com/robert-hoffmann/PowerToys4OpenAI/blob/main/CHANGELOG.md'}); 33 | 34 | // load defaults 35 | loadDefaults(); 36 | } 37 | }); 38 | 39 | // Opend feedback URL on extension uninstall 40 | chrome.runtime.setUninstallURL('https://airtable.com/shrqYoLYGNPzqUHMD'); 41 | 42 | /* INFO: this can reload the extension if there is an update 43 | chrome.runtime.onUpdateAvailable.addListener(() => { 44 | chrome.runtime.reload(); 45 | }); 46 | */ 47 | 48 | chrome.action.onClicked.addListener((/* tab */) => { 49 | // Opens the chat interface in a new tab. 50 | chrome.tabs.create( 51 | { 52 | url: chrome.runtime.getURL('www/index.html#/index'), 53 | }, 54 | (/* newTab */) => { 55 | // Tab opened. 56 | } 57 | ); 58 | }); 59 | 60 | /** 61 | * Insert default settings into the database 62 | */ 63 | async function loadDefaults() { 64 | const profiles = await db.getProfiles(); 65 | 66 | if (!profiles.length) { 67 | db.insertProfiles(defaultProfiles); 68 | } 69 | 70 | //console.log('background.loadDefaults()', profiles); 71 | } 72 | 73 | /** 74 | * Send userSettings to the content script 75 | * @param bridge 76 | */ 77 | async function sendSettingsToContent(bridge: BexBridge) { 78 | const settings = await db.getUserSettings(); 79 | //console.log('background.sendSettingsToContent()', settings); 80 | bridge.send('background.ready', { 81 | settings: settings, 82 | }); 83 | } 84 | 85 | declare module '@quasar/app-vite' { 86 | interface BexEventMap { 87 | /* eslint-disable @typescript-eslint/no-explicit-any */ 88 | log : [{ message: string; data?: any[] }, never]; 89 | getTime: [never, number]; 90 | 91 | 'storage.get' : [{ key: string | null }, any]; 92 | 'storage.set' : [{ key: string; value: any }, any]; 93 | 'storage.remove': [{ key: string }, any]; 94 | /* eslint-enable @typescript-eslint/no-explicit-any */ 95 | } 96 | } 97 | 98 | export default bexBackground((bridge: BexBridge /* , allActiveConnections */) => { 99 | //console.log('LOADING:', Date.now(), 'background.ts::bexBackground'); 100 | 101 | //#region Quasar Bridge 102 | bridge.on('log', ({ data, respond }) => { 103 | console.log(`[BEX] ${data.message}`, ...(data.data || [])); 104 | respond(); 105 | }); 106 | 107 | bridge.on('storage.get', ({ data, respond }) => { 108 | const { key } = data; 109 | if (key === null) { 110 | chrome.storage.local.get(null, (items) => { 111 | // Group the values up into an array to take advantage of the bridge's chunk splitting. 112 | respond(Object.values(items)); 113 | }); 114 | } else { 115 | chrome.storage.local.get([key], (items) => { 116 | respond(items[key]); 117 | }); 118 | } 119 | }); 120 | // Usage: 121 | // const { data } = await bridge.send('storage.get', { key: 'someKey' }) 122 | 123 | bridge.on('storage.set', ({ data, respond }) => { 124 | chrome.storage.local.set({ [data.key]: data.value }, () => { 125 | respond(); 126 | }); 127 | }); 128 | // Usage: 129 | // await bridge.send('storage.set', { key: 'someKey', value: 'someValue' }) 130 | 131 | bridge.on('storage.remove', ({ data, respond }) => { 132 | chrome.storage.local.remove(data.key, () => { 133 | respond(); 134 | }); 135 | }); 136 | // Usage: 137 | // await bridge.send('storage.remove', { key: 'someKey' }) 138 | //#endregion 139 | 140 | //#region ----------------------- OpenAI ----------------------- 141 | /** 142 | * @description Get the answer from OpenAI 143 | */ 144 | bridge.on('background.getAnswer', async ({ data /*, respond */ }) => { 145 | console.log('background.getAnswer'); 146 | 147 | const session = await openAi.getSession(); 148 | if (session.error) { 149 | console.log('session error', session); 150 | bridge.send(data.replyTo, { response: session, context: data.context }); 151 | //respond(session); 152 | return; 153 | } 154 | 155 | controller = new AbortController(); 156 | await openAi.getAnswerStreamed( 157 | data.messages, 158 | session.content.accessToken, 159 | controller.signal, 160 | (response) => { 161 | //console.log('background.getAnswer response', response); 162 | //respond(response); 163 | bridge.send(data.replyTo, { response: response, context: data.context }); 164 | } 165 | ); 166 | }); 167 | 168 | bridge.on('background.cancelAnswer', async ({ data, respond }) => { 169 | controller?.abort(); 170 | respond(data); 171 | }); 172 | 173 | bridge.on('background.openTabUrl', ({ data, respond }) => { 174 | chrome.tabs.create( 175 | { 176 | url: chrome.runtime.getURL(`www/index.html#${data.url}`) 177 | }, 178 | (/*newTab*/) => { 179 | // Tab opened 180 | // do something with it (cross tab messaging ?) 181 | //console.log('background.openTabUrl', data); 182 | 183 | // TODO: this works, but we need a better system that takes into account the asynchronous nature of this 184 | if (data.content) { 185 | setTimeout(() => { 186 | bridge.send('background.selectedText', data.content); 187 | }, 1000); 188 | } 189 | } 190 | ); 191 | 192 | respond(data); 193 | }); 194 | 195 | /** 196 | * @description Send a notification to the user via Chrome's built-in notification system 197 | * @param {object} { data.title, data.message, data.icon } 198 | */ 199 | bridge.on('background.displayNotification', ({ data }) => { 200 | chrome.notifications.create('', { 201 | title : data.title, 202 | message : data.message, 203 | iconUrl : data.icon ? data.icon : 'www/logo.png', 204 | silent : true, 205 | type : 'basic' 206 | }); 207 | }); 208 | 209 | // TODO we need a wrapper that can pass trhough background and back to content 210 | // https://quasar.dev/quasar-cli-vite/developing-browser-extensions/bex-communication#communication-rules 211 | /* 212 | bridge.on('background.proxyEvent', ({ data }) => { 213 | bridge.send(data.destination, { 214 | payload: data.payload, 215 | replyTo: data.replyTo 216 | }); 217 | }); 218 | */ 219 | 220 | sendSettingsToContent(bridge); 221 | //#endregion -------------------- OpenAI ----------------------- 222 | }); 223 | -------------------------------------------------------------------------------- /src-bex/bex-flag.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // THIS FEATURE-FLAG FILE IS AUTOGENERATED, 3 | // REMOVAL OR CHANGES WILL CAUSE RELATED TYPES TO STOP WORKING 4 | import "quasar/dist/types/feature-flag"; 5 | 6 | declare module "quasar/dist/types/feature-flag" { 7 | interface QuasarFeatureFlags { 8 | bex: true; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src-bex/content.ts: -------------------------------------------------------------------------------- 1 | // Hooks added here have a bridge allowing communication between the BEX Content Script and the Quasar Application. 2 | // More info: https://quasar.dev/quasar-cli/developing-browser-extensions/content-hooks 3 | import { bexContent } from 'quasar/wrappers'; 4 | import { BexBridge } from '@quasar/app-vite/types/bex'; 5 | 6 | import { IUserSettings } from 'src/js/libs/types/IUserSettings'; 7 | 8 | // Import Modules 9 | import iframe from './modules/iframeComponent'; 10 | //import contextMenu from './modules/contextMenuComponent'; 11 | 12 | /** 13 | * Initialize the content script 14 | */ 15 | function main(bridge: BexBridge, settings: IUserSettings) { 16 | /** 17 | * Initialize Modules (loaded above) 18 | */ 19 | //contextMenu(bridge); 20 | iframe(bridge, settings); 21 | } 22 | 23 | /** 24 | * This is the main entry point for the BEX Content Script. 25 | */ 26 | // BUG: This loads as many times as the page is refreshed !!! 27 | // Warning: This will bind multiple times, fix it, or unbind before binding 28 | export default bexContent(async (bridge: BexBridge) => { 29 | //console.log('LOADING:', Date.now(), 'content.ts::bexContent'); 30 | 31 | // INFO: Fires when the background is ready, and sets userSettings from Database 32 | // Seems to still work, even though using once() instead of on() : mayber should use this elsewhere to prevent those double loads ? 33 | bridge.once('background.ready', async ({ data }) => { 34 | //console.log('background.ready in content', data); 35 | 36 | main(bridge, data.settings); 37 | }); 38 | 39 | }); 40 | -------------------------------------------------------------------------------- /src-bex/dom.ts: -------------------------------------------------------------------------------- 1 | // https://quasar.dev/quasar-cli-vite/developing-browser-extensions/dom-script 2 | // Hooks added here have a bridge allowing communication between the Web Page and the BEX Content Script. 3 | import { bexDom } from 'quasar/wrappers'; 4 | // import { BexBridge } from '@quasar/app-vite/types/bex'; 5 | 6 | // Quasar: internal function 7 | export default bexDom((/* bridge: BexBridge */) => { 8 | //console.log('LOADING: dom.ts', bridge); 9 | }) 10 | -------------------------------------------------------------------------------- /src-bex/icons/icon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robert-hoffmann/PowerToys4OpenAI/e8aa87e64abe4ba9a11b99f984e6dd0429358f90/src-bex/icons/icon-128x128.png -------------------------------------------------------------------------------- /src-bex/icons/icon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robert-hoffmann/PowerToys4OpenAI/e8aa87e64abe4ba9a11b99f984e6dd0429358f90/src-bex/icons/icon-16x16.png -------------------------------------------------------------------------------- /src-bex/icons/icon-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robert-hoffmann/PowerToys4OpenAI/e8aa87e64abe4ba9a11b99f984e6dd0429358f90/src-bex/icons/icon-48x48.png -------------------------------------------------------------------------------- /src-bex/icons/search.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src-bex/libs/sites-detection.ts: -------------------------------------------------------------------------------- 1 | export interface SearchEngines { 2 | /** Website name */ 3 | name: string, 4 | 5 | /** If URL matches, then module will be loaded */ 6 | url: RegExp, 7 | 8 | /** Possible target containers */ 9 | targets: Configuration[], 10 | 11 | /** If site supports sidebar injection **/ 12 | addSidebar: boolean 13 | }[] 14 | 15 | export interface Configuration { 16 | /** 17 | * From where to extract the users search phrase 18 | * @type {query selector} 19 | * */ 20 | userInputs: string[], 21 | 22 | /** Array of possible targets of where the module will be attached to in the DOM */ 23 | containers: string[], 24 | 25 | /** How to add the module to the selector */ 26 | method: 'append' | 'prepend', 27 | 28 | /** Classes to add to the component */ 29 | classes?: string[], 30 | 31 | /** Styles to apply to the component */ 32 | styles?: { 33 | [key: string]: string 34 | }[] 35 | } 36 | 37 | export const config: SearchEngines[] = [ 38 | { 39 | name : 'Google Search', 40 | url : /https?:\/\/.*(google)\.[a-z]{2,3}\/search\?.*(q=.*)/, 41 | addSidebar : false, 42 | targets: [ 43 | { 44 | userInputs : [ 45 | "input[name='q']", 46 | "textarea[name='q']" 47 | ], 48 | containers: [ 49 | '#rhs' 50 | ], 51 | method : 'prepend', 52 | classes : [ 53 | 'ptoia-companion-prepend' 54 | ] 55 | }, 56 | { 57 | userInputs : [ 58 | "input[name='q']", 59 | "textarea[name='q']" 60 | ], 61 | containers: [ 62 | '#rcnt' 63 | ], 64 | method : 'append', 65 | classes : [ 66 | 'ptoia-companion-sidecontent', 67 | 'append' 68 | ], 69 | styles: [ 70 | { marginLeft: '30px' }, 71 | { width : '367px' } 72 | ] 73 | } 74 | ], 75 | }, 76 | { 77 | name : 'Bing Search', 78 | url : /https?:\/\/.*(bing)\.[a-z]{2,3}\/search\?.*(q=.*)/, 79 | addSidebar : false, 80 | targets: [ 81 | { 82 | userInputs : [ 83 | "[name='q']" 84 | ], 85 | containers: [ 86 | '#b_context' 87 | ], 88 | method : 'prepend', 89 | classes : [ 90 | 'ptoia-companion-prepend' 91 | ], 92 | styles: [ 93 | { margin: '0px 0px 20px -20px' }, 94 | { width : '108%' } 95 | ] 96 | } 97 | ], 98 | }, 99 | { 100 | name : 'Yahoo Search', 101 | url : /https?:\/\/.*(search\.yahoo)\.[a-z]{2,3}\/search\?.*(p=.*)/, 102 | addSidebar : false, 103 | targets: [ 104 | { 105 | userInputs : [ 106 | "[name='p']" 107 | ], 108 | containers: [ 109 | '#right', 110 | '.Contents__inner.Contents__inner--sub' 111 | ], 112 | method : 'prepend', 113 | classes : [ 114 | 'ptoia-companion-prepend' 115 | ], 116 | styles: [ 117 | { margin: '0px 0px 20px 0px' }, 118 | { width : '370px' } 119 | ] 120 | }, 121 | { 122 | userInputs : [ 123 | "[name='p']" 124 | ], 125 | containers: [ 126 | '#cols', 127 | '#contents__wrap' 128 | ], 129 | method : 'append', 130 | classes : [ 131 | 'ptoia-companion-sidecontent', 132 | 'append' 133 | ], 134 | styles: [ 135 | { margin: '0px 0px 20px 0px' }, 136 | { width : '370px' } 137 | ] 138 | } 139 | ], 140 | }, 141 | { 142 | name : 'DuckDuckGo Search', 143 | url : /https?:\/\/.*(duckduckgo)\.[a-z]{2,3}\/\?.*(q=.*)/, 144 | addSidebar : false, 145 | targets: [ 146 | { 147 | userInputs : [ 148 | "input[name='q']" 149 | ], 150 | containers: [ 151 | '.results--sidebar.js-results-sidebar' 152 | ], 153 | method : 'prepend', 154 | classes : [ 155 | 'ptoia-companion-prepend' 156 | ], 157 | styles: [ 158 | { margin: '0 0 10px 0' }, 159 | { width : '100%' } 160 | ] 161 | }, 162 | { 163 | userInputs : [ 164 | "input[name='q']" 165 | ], 166 | containers: [ 167 | '#links_wrapper' 168 | ], 169 | method : 'append', 170 | classes : [ 171 | 'ptoia-companion-sidecontent', 172 | 'append' 173 | ], 174 | styles: [ 175 | { margin: '0 0 10px 0' }, 176 | { width : '100%' } 177 | ] 178 | } 179 | ], 180 | }, 181 | { 182 | name : 'Baidu Search', 183 | url : /https?:\/\/.*(baidu)\.[a-z]{2,3}\/s?.*/, 184 | addSidebar : false, 185 | targets: [ 186 | { 187 | userInputs : [ 188 | "input[name='wd']" 189 | ], 190 | containers: [ 191 | '#content_right' 192 | ], 193 | method : 'prepend', 194 | classes : [ 195 | 'ptoia-companion-prepend' 196 | ], 197 | styles: [ 198 | { width : '367px' } 199 | ] 200 | }, 201 | { 202 | userInputs : [ 203 | "input[name='wd']" 204 | ], 205 | containers: [ 206 | '#container' 207 | ], 208 | method : 'append', 209 | classes : [ 210 | 'ptoia-companion-sidecontent', 211 | 'append' 212 | ], 213 | styles: [ 214 | { width : '367px' } 215 | ] 216 | } 217 | ], 218 | }, 219 | { 220 | name : 'Yandex Search', 221 | url : /https?:\/\/.*(yandex)\.[a-z]{2,3}\/search\/\?.*(text=.*)/, 222 | addSidebar : false, 223 | targets: [ 224 | { 225 | userInputs : [ 226 | "input[name='text']" 227 | ], 228 | containers: [ 229 | '#search-result-aside' 230 | ], 231 | method : 'prepend', 232 | classes : [ 233 | 'ptoia-companion-prepend' 234 | ], 235 | styles: [ 236 | { margin: '0 0 10px 0' }, 237 | { width : '100%' } 238 | ] 239 | } 240 | ], 241 | }, 242 | { 243 | name : 'Naver Search', 244 | url : /https?:\/\/.*(search\.naver)\.[a-z]{2,3}\/search.naver\?.*/, 245 | addSidebar : false, 246 | targets: [ 247 | { 248 | userInputs : [ 249 | "input[name='query']" 250 | ], 251 | containers: [ 252 | '#sub_pack' 253 | ], 254 | method : 'prepend', 255 | classes : [ 256 | 'ptoia-companion-prepend' 257 | ], 258 | styles: [ 259 | { margin: '0px 0px 15px 16px' }, 260 | { width : '400px' } 261 | ] 262 | }, 263 | { 264 | userInputs : [ 265 | "input[name='query']" 266 | ], 267 | containers: [ 268 | '#content' 269 | ], 270 | method : 'append', 271 | classes : [ 272 | 'ptoia-companion-sidecontent', 273 | 'append' 274 | ], 275 | styles: [ 276 | { margin: '0px 0px 15px 16px' }, 277 | { width : '400px' } 278 | ] 279 | } 280 | ], 281 | }, 282 | { 283 | name : 'Brave Search', 284 | url : /https?:\/\/.*(search\.brave)\.[a-z]{2,3}\/search\?.*(q=.*)/, 285 | addSidebar : false, 286 | targets: [ 287 | { 288 | userInputs : [ 289 | "[name='q']" 290 | ], 291 | containers: [ 292 | '#side-right'], 293 | method : 'prepend', 294 | styles: [ 295 | { margin: '0px 0px 20px 0px' }, 296 | { width : '370px' } 297 | ] 298 | } 299 | ], 300 | }, 301 | { 302 | name : 'SearX Search', 303 | url : /https?:\/\/.*(searx\.thegpm)\.[a-z]{2,3}\/.*/, 304 | addSidebar : false, 305 | targets: [ 306 | { 307 | userInputs : [ 308 | "input[name='q']" 309 | ], 310 | containers: [ 311 | '#sidebar_results'], 312 | method : 'prepend', 313 | styles: [ 314 | { width : '360px' } 315 | ] 316 | } 317 | ], 318 | }, 319 | { 320 | name : 'Other Sites', 321 | url : /https?:\/\/(.*)\.[a-z]{2,3}/i, 322 | addSidebar : true, 323 | targets: [ 324 | { 325 | userInputs : [], 326 | containers : [ 327 | 'body' 328 | ], 329 | method : 'append', 330 | styles: [ 331 | { margin : '0px 0px 20px 0px' }, 332 | { width : '450px' }, 333 | { position : 'absolute' }, 334 | { top : '150px' }, 335 | { right : '0' }, 336 | { zIndex : '9999' }, 337 | ] 338 | } 339 | ], 340 | }, 341 | ] 342 | -------------------------------------------------------------------------------- /src-bex/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "__MSG_name__", 4 | "short_name": "__MSG_shortName__", 5 | "description": "__MSG_description__", 6 | "default_locale": "en", 7 | "icons": { 8 | "16" : "icons/icon-16x16.png", 9 | "48" : "icons/icon-48x48.png", 10 | "128" : "icons/icon-128x128.png" 11 | }, 12 | 13 | "permissions": [ 14 | "storage", 15 | "notifications" 16 | ], 17 | 18 | "action": { 19 | "default_title": "__MSG_name__" 20 | }, 21 | 22 | "options_ui": { 23 | "page" : "www/index.html#/index/settings/search", 24 | "open_in_tab": true 25 | }, 26 | 27 | "background": { 28 | "service_worker": "background.js" 29 | }, 30 | 31 | "content_scripts": [ 32 | { 33 | "matches": [ "*://*/*" ], 34 | "css" : [ "assets/content.css" ], 35 | "js" : [ 36 | "content.js" 37 | ], 38 | "run_at": "document_end" 39 | } 40 | ], 41 | 42 | "content_security_policy": { 43 | "extension_pages": "script-src 'self'; object-src 'self';" 44 | }, 45 | 46 | "web_accessible_resources": [ 47 | { 48 | "resources": [ "*" ], 49 | "matches" : [ "*://*/*" ] 50 | } 51 | ] 52 | } 53 | -------------------------------------------------------------------------------- /src-bex/modules/contextMenuComponent.ts: -------------------------------------------------------------------------------- 1 | // We create a new folder + file: 2 | // src-bex/dom/detect-quasar.js 3 | // https://quasar.dev/quasar-cli-vite/developing-browser-extensions/dom-script 4 | import { BexBridge } from '@quasar/app-vite/types/bex'; 5 | 6 | //#region --------------------------- Behavioral Modules --------------------------- 7 | function main(bridge: BexBridge) { 8 | 9 | function displayContextMenu(e: MouseEvent) { 10 | //console.log('displayContextMenu', e.target); 11 | 12 | // ctrl is not pressed, do not open context menu 13 | if (!e.ctrlKey) { 14 | return; 15 | } 16 | e.preventDefault(); 17 | 18 | // get existing context menu 19 | let contextMenu = document.getElementById('ptoia-context-menu'); 20 | 21 | // create context menu if it does not exist 22 | if (contextMenu === null) { 23 | contextMenu = document.createElement('div'); 24 | contextMenu.id = 'ptoia-context-menu'; 25 | contextMenu.classList.add('ptoia-context-menu'); 26 | 27 | contextMenu.addEventListener('click', (ev: MouseEvent) => { 28 | //console.log('contextMenu click', ev); 29 | 30 | bridge.send('content::context-action', { clientX: ev.clientX, clientY: ev.clientY }) 31 | .then((response) => { 32 | console.log({ 33 | file : 'contentClick.ts', 34 | method: { 35 | name: 'content::context-action', 36 | type: 'send' 37 | }, 38 | result: response 39 | }); 40 | }); 41 | }); 42 | 43 | contextMenu.addEventListener('click', (ev: MouseEvent) => { 44 | const target = ev.target as HTMLElement; 45 | 46 | target.style.display = 'none'; 47 | //console.log('target', target.dataset.action); 48 | getSelectedText(ev); 49 | }); 50 | 51 | document.body.appendChild(contextMenu); 52 | } 53 | 54 | function populateContextMenu(menu: HTMLElement) { 55 | menu.innerHTML = ''; 56 | 57 | actions.forEach((action) => { 58 | const item = document.createElement('div'); 59 | item.dataset.action = action.action; 60 | 61 | item.innerText = action.name; 62 | item.classList.add('ptoia-context-menu-item'); 63 | 64 | menu.appendChild(item) 65 | }); 66 | } 67 | 68 | // display 69 | populateContextMenu(contextMenu); 70 | contextMenu.style.display = 'block'; 71 | contextMenu.style.top = `${e.pageY}px`; 72 | contextMenu.style.left = `${e.pageX}px`; 73 | 74 | } 75 | 76 | function detectKeyDown(e: KeyboardEvent) { 77 | //console.log('detectKeyDown', e); 78 | 79 | if (e.key === 'Escape') { 80 | const contextMenu = document.getElementById('ptoia-context-menu'); 81 | if (contextMenu !== null) { 82 | contextMenu.style.display = 'none'; 83 | } 84 | } 85 | } 86 | 87 | // may want to use this to manipulmate the DOM 88 | function detectClick(e: MouseEvent) { 89 | const target = e.target as HTMLElement; 90 | 91 | //console.log('detectClick', e.target); 92 | 93 | // dont close if we click on the context menu 94 | if (target.closest('.ptoia-context-menu') !== null) { 95 | return; 96 | } 97 | 98 | const contextMenu = document.getElementById('ptoia-context-menu'); 99 | if (contextMenu !== null) { 100 | 101 | 102 | contextMenu.style.display = 'none'; 103 | } 104 | } 105 | 106 | 107 | function getSelectedText(e: MouseEvent) { 108 | const selectedText = window.getSelection()?.toString(); 109 | 110 | if (!!selectedText) { 111 | //console.log('getSelectedText', selectedText); 112 | } 113 | } 114 | 115 | // bind context 116 | document.addEventListener('contextmenu', (e: MouseEvent) => displayContextMenu(e)); 117 | //document.addEventListener('mouseup' , (e: MouseEvent) => getSelectedText(e)); 118 | document.addEventListener('click' , (e: MouseEvent) => detectClick(e)); 119 | document.addEventListener('keydown', (e: KeyboardEvent) => detectKeyDown(e)); 120 | } 121 | //#endregion ------------------------ Behavioral Modules --------------------------- 122 | 123 | const actions = [ 124 | { 125 | name : 'getTargetText', 126 | action : 'getTargetText' 127 | }, 128 | { 129 | name : 'action2', 130 | action : 'action2' 131 | }, 132 | { 133 | name : 'action3', 134 | action : 'action3' 135 | }, 136 | { 137 | name : 'action4', 138 | action : 'action4' 139 | } 140 | ]; 141 | 142 | 143 | // Export module 144 | export default function contextMenu(bridge: BexBridge) { 145 | main(bridge); 146 | } 147 | -------------------------------------------------------------------------------- /src-bex/modules/iframeComponent.ts: -------------------------------------------------------------------------------- 1 | // We create a new folder + file: 2 | // src-bex/dom/detect-quasar.js 3 | // https://quasar.dev/quasar-cli-vite/developing-browser-extensions/dom-script 4 | import { BexBridge } from '@quasar/app-vite/types/bex'; 5 | import { useDebounceFn } from '@vueuse/core'; 6 | import { config, Configuration } from '../libs/sites-detection'; 7 | import { IUserSettings } from 'src/js/libs/types/IUserSettings'; 8 | 9 | //console.log('iframeComponent.ts'); 10 | 11 | const 12 | component = document.createElement('div'), 13 | iFrame = document.createElement('iframe'), 14 | defaulContainerHeight = '56px', 15 | defaulContainerWidth = '370px'; 16 | 17 | let selectedText = ''; 18 | 19 | /** 20 | * Set the height of our container housing our BEX 21 | * @param height 22 | */ 23 | const setContainerHeight = (height: string) => { 24 | component.style.height = `${height}px`; 25 | } 26 | 27 | /** 28 | * The code below will get everything going. Initialize the iFrame with defaults and add it to the page. 29 | * @type {string} 30 | */ 31 | component.id = 'ptoia-companion-container'; 32 | iFrame.allow = 'clipboard-write'; 33 | 34 | // Default settings for container 35 | Object.assign(component.style, { 36 | overflow: 'hidden', 37 | height : defaulContainerHeight, 38 | width : defaulContainerWidth, 39 | }); 40 | 41 | // Default settings for Iframe 42 | Object.assign(iFrame.style, { 43 | overflow: 'hidden', 44 | height : '100%', 45 | width : '100%', 46 | border : '0' 47 | }); 48 | 49 | /** 50 | * This is a workaround to events triggering multiple times 51 | */ 52 | const toggleCompanion = useDebounceFn(() => { 53 | component.classList.toggle('ptoia-sidebar-closed'); 54 | }, 250); 55 | 56 | /** 57 | * Inject a module into the DOM 58 | */ 59 | function injectContentModule(bridge: BexBridge, configuration: Configuration, asSidebar: boolean) { 60 | //console.log('injectContentModule::content.ts', configuration); 61 | 62 | // INFO: find the element we want to append to 63 | let container: Element | undefined; 64 | configuration.containers.forEach((selector) => { 65 | const element = document.querySelector(selector); 66 | 67 | if (element) { 68 | container = element; 69 | return; 70 | } 71 | }); 72 | 73 | if (container) { 74 | // INFO: Wrap in shadow DOM to prevent CSS conflicts 75 | // Also keeps certain sites from blocking iframes 76 | // https://stackoverflow.com/a/67613697/896849 77 | 78 | // BUG: was having random this error randomly 79 | // Shadow root cannot be created on a host which already hosts a shadow tree 80 | // So for now, just wrapping in a try/catch 81 | try { 82 | const shadowDom = component.attachShadow({ mode:'closed' }); 83 | shadowDom.appendChild(iFrame); 84 | } catch (e) { 85 | console.error('Error attaching shadow DOM', e); 86 | // could not attach to shadow DOM, so just append it to the container... 87 | component.appendChild(iFrame); 88 | } 89 | 90 | // When the page loads, insert our browser extension app. 91 | iFrame.src = chrome.runtime.getURL('www/index.html#/search-companion'); 92 | 93 | // ------------------------------------------------------------------- 94 | 95 | // append or prepend the container to the page 96 | container[configuration.method](component); 97 | 98 | // Add any classes to the container if needed 99 | if (configuration.classes) { 100 | configuration.classes.forEach((className) => { 101 | component.classList.add(className); 102 | }); 103 | } 104 | 105 | // Apply any styles to the container if needed 106 | if (configuration.styles) { 107 | configuration.styles.forEach((style) => { 108 | //console.log('style', style); 109 | 110 | const styleName : string = Object.entries(style)[0][0]; 111 | const styleValue: string = Object.entries(style)[0][1]; 112 | component.style[styleName] = styleValue; 113 | }); 114 | } 115 | 116 | // if we should display as a sidebar (starts in closed position) 117 | if (asSidebar) { 118 | component.classList.add('ptoia-sidebar-closed'); 119 | } 120 | 121 | let prompt = ''; 122 | if (configuration.userInputs) { 123 | // Get the user input 124 | configuration.userInputs.forEach((selector) => { 125 | const input = document.querySelector(selector); 126 | 127 | // if we find a match, assign it and break out of the loop 128 | if (input) { 129 | prompt = input?.value || ''; 130 | return; 131 | } 132 | }); 133 | } 134 | 135 | // INFO: listen for page ready 136 | bridge.on('content.pageReady', () => { 137 | // Set the value of the user input, to the SearchCompanion input field 138 | bridge.send('page.searchCompanion.initialize', { 139 | prompt : prompt || '', 140 | sidebar: asSidebar || false, 141 | }); 142 | }); 143 | } 144 | } 145 | 146 | //#region --------------------------- Behavioral Modules --------------------------- 147 | function main(bridge: BexBridge, settings: IUserSettings) { 148 | // see if the site we are on is in our list of supported sites 149 | const website = config.find((item) => { 150 | const match = window.location.href.match(item.url); 151 | return match && match[1]; 152 | }); 153 | 154 | //console.log('website', website); 155 | 156 | // only insert content on supported websites 157 | if (website) { 158 | //console.log('we found a URL match', website); 159 | 160 | // Attach a listener to open the Conversation Manager when the user presses Alt+C 161 | document.addEventListener('keydown', (event: KeyboardEvent) => { 162 | if (event.altKey && event.code === 'KeyC') { 163 | bridge.send('background.openTabUrl', 164 | { 165 | url : '/index', 166 | content: selectedText 167 | } 168 | ); 169 | } 170 | }); 171 | 172 | // attach an event listener to see if text has been selected 173 | // INFO: ask GPT about: Selection API (and an event listener example) 174 | document.addEventListener('mouseup', (/* event */) => { 175 | selectedText = window.getSelection()?.toString() || ''; 176 | 177 | if (selectedText !== '') { 178 | //console.log('Text has been selected: ' + selectedText, event); 179 | // do something with the selected text 180 | } 181 | }); 182 | 183 | // loop over all posssible targets 184 | const configuration = website.targets.find((target) => { 185 | // loop over all possible containers 186 | const container = target.containers.find((selector) => { 187 | return document.querySelector(selector); 188 | }); 189 | 190 | // if we have a container to attach to for this target, return the target 191 | if (container) { 192 | return target; 193 | } 194 | }); 195 | 196 | if (configuration) { 197 | //console.log('website.addSidebar' , website.addSidebar); 198 | //console.log('settings.showSidebar', settings.showSidebar); 199 | 200 | // Dont add sidebar if user has it disabled 201 | if (website.addSidebar == true && settings.showSidebar == false) { 202 | // just exit and add nothing 203 | return; 204 | } 205 | 206 | // inject module if we found a match 207 | injectContentModule(bridge, configuration, website.addSidebar); 208 | } 209 | } 210 | } 211 | //#endregion ------------------------ Behavioral Modules --------------------------- 212 | // Export module 213 | export default function iframe(bridge: BexBridge, settings: IUserSettings) { 214 | //console.log('LOADING:', Date.now(), 'iframeComponent.export'); 215 | 216 | main(bridge, settings); 217 | 218 | bridge.on('content.onResize', async ({ data }) => { 219 | //console.log('content.onResize', data); 220 | setContainerHeight(data.height); 221 | }); 222 | 223 | // INFO: Event when the sidebar should be shown/hidden 224 | bridge.on('content.toggleCompanion', async () => { 225 | // INFO: This is a workaround to events triggering multiple times 226 | toggleCompanion(); 227 | }); 228 | 229 | bridge.on('content.scrollToTop', () => { 230 | window.scrollTo(0, 0); 231 | }); 232 | 233 | bridge.on('content.copyToClipboard', ({ data }) => { 234 | //console.log('content.copyToClipboard', data); 235 | navigator.clipboard.writeText(data || ''); 236 | }); 237 | } 238 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 53 | -------------------------------------------------------------------------------- /src/assets/_readme.md: -------------------------------------------------------------------------------- 1 | ## Accessible via: 2 | * Inside Chrome extension 3 | * www/assets/* 4 | * Inside *.vue files 5 | * ~assets/* 6 | -------------------------------------------------------------------------------- /src/boot/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robert-hoffmann/PowerToys4OpenAI/e8aa87e64abe4ba9a11b99f984e6dd0429358f90/src/boot/.gitkeep -------------------------------------------------------------------------------- /src/boot/i18next.ts: -------------------------------------------------------------------------------- 1 | import { boot } from 'quasar/wrappers'; 2 | 3 | import i18next from 'i18next'; 4 | import I18NextVue from 'i18next-vue'; 5 | import LanguageDetector from 'i18next-browser-languagedetector'; 6 | 7 | import de from '../i18n/de'; 8 | import en from '../i18n/en'; 9 | import es from '../i18n/es'; 10 | import fr from '../i18n/fr'; 11 | import hi from '../i18n/hi'; 12 | import ja from '../i18n/ja'; 13 | import pt from '../i18n/pt'; 14 | import ru from '../i18n/ru'; 15 | import tr from '../i18n/tr'; 16 | import zh from '../i18n/zh'; 17 | 18 | // https://dev.to/adrai/how-to-properly-internationalize-a-vue-application-using-i18next-1doj 19 | // usage in vue file: https://github.com/i18next/i18next-vue 20 | export default boot(({ app }) => { 21 | i18next 22 | // detect user language 23 | // learn more: https://github.com/i18next/i18next-browser-languageDetector 24 | .use(LanguageDetector) 25 | // init i18next 26 | // for all options read: https://www.i18next.com/overview/configuration-options 27 | .init({ 28 | debug : false, 29 | fallbackLng: 'en', 30 | resources: { 31 | de: de, 32 | en: en, 33 | es: es, 34 | fr: fr, 35 | hi: hi, 36 | ja: ja, 37 | pt: pt, 38 | ru: ru, 39 | tr: tr, 40 | zh: zh 41 | }, 42 | detection: { 43 | order: ['localStorage', 'navigator'], 44 | } 45 | }); 46 | 47 | // Set i18n instance on app 48 | app.use(I18NextVue, { i18next }) 49 | }); 50 | -------------------------------------------------------------------------------- /src/components/SearchSettingsComponent.vue: -------------------------------------------------------------------------------- 1 | 175 | 176 | 185 | 186 | 274 | 275 | 278 | -------------------------------------------------------------------------------- /src/components/leftmenu/ConversationsComponent.vue: -------------------------------------------------------------------------------- 1 | 92 | 93 | 102 | 103 | 217 | 218 | 221 | -------------------------------------------------------------------------------- /src/components/leftmenu/MenuLeftComponent.vue: -------------------------------------------------------------------------------- 1 | 115 | 116 | 125 | 126 | 205 | 206 | 215 | 216 | 217 | -------------------------------------------------------------------------------- /src/components/leftmenu/ProfilesComponent.vue: -------------------------------------------------------------------------------- 1 | 67 | 68 | 77 | 78 | 181 | 182 | 185 | -------------------------------------------------------------------------------- /src/components/leftmenu/SettingsComponent.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 37 | 38 | 73 | 74 | 77 | -------------------------------------------------------------------------------- /src/css/app.scss: -------------------------------------------------------------------------------- 1 | // app global css in SCSS form 2 | /* ===== Scrollbar CSS ===== */ 3 | /* Firefox */ 4 | * { 5 | scrollbar-width: auto; 6 | scrollbar-color: #99141d #ffffff; 7 | } 8 | 9 | /* Chrome, Edge, and Safari */ 10 | *::-webkit-scrollbar { 11 | width: 16px; 12 | } 13 | 14 | *::-webkit-scrollbar-track { 15 | background: #ffffff; 16 | } 17 | 18 | *::-webkit-scrollbar-thumb { 19 | background-color: #99141d; 20 | border-radius: 10px; 21 | border: 2px solid #ffffff; 22 | } 23 | 24 | .text-underline { 25 | text-decoration: underline; 26 | } 27 | -------------------------------------------------------------------------------- /src/css/quasar.variables.scss: -------------------------------------------------------------------------------- 1 | // Quasar SCSS (& Sass) Variables 2 | // -------------------------------------------------- 3 | // To customize the look and feel of this app, you can override 4 | // the Sass/SCSS variables found in Quasar's source Sass/SCSS files. 5 | 6 | // Check documentation for full list of Quasar variables 7 | 8 | // Your own variables (that are declared here) and Quasar's own 9 | // ones will be available out of the box in your .vue/.scss/.sass files 10 | 11 | // It's highly recommended to change the default colors 12 | // to match your app's branding. 13 | // Tip: Use the "Theme Builder" on Quasar's documentation website. 14 | 15 | $primary : #ce1925; 16 | $secondary : #99141d; 17 | $accent : #ffb201; 18 | 19 | $dark : #1D1D1D; 20 | $dark-page : #121212; 21 | 22 | $positive : #21BA45; 23 | $negative : #C10015; 24 | $info : #31CCEC; 25 | $warning : #F2C037; 26 | 27 | 28 | // Alexia 29 | $_primary : #e63440; 30 | $_secondary : #99141d; 31 | $_accent : #ffb201; 32 | $_tertiary : #9a3345; 33 | -------------------------------------------------------------------------------- /src/env.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | declare namespace NodeJS { 4 | interface ProcessEnv { 5 | NODE_ENV: string; 6 | VUE_ROUTER_MODE: 'hash' | 'history' | 'abstract' | undefined; 7 | VUE_ROUTER_BASE: string | undefined; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/i18n/de/index.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | translation: { 3 | global: { 4 | yes : 'Ja', 5 | no : 'Nein', 6 | save : 'Speichern', 7 | close : 'Schließen', 8 | edit : 'Bearbeiten', 9 | copy : 'Kopieren', 10 | play : 'Abspielen', 11 | record : 'Aufnehmen', 12 | import : 'Importieren', 13 | export : 'Exportieren', 14 | upload : 'Hochladen', 15 | cancel : 'Abbrechen', 16 | delete : 'Löschen', 17 | retry : 'Erneut versuchen', 18 | missing : 'fehlend', 19 | settings : 'Einstellungen', 20 | search : 'Eine Frage stellen', 21 | searchHint : 'Oder ein Thema ausführlich behandeln', 22 | authError : 'Fehler: {{statusCode}}\n\nEs tut uns leid, aber beim Authentifizieren ist ein Fehler aufgetreten.\nBitte überprüfen Sie Ihren API-Schlüssel und Ihr Modell.', 23 | comingSoon : 'Kommt bald', 24 | donate : 'Spenden {{name}}', 25 | donateCopy : '{{name}}-Adresse in die Zwischenablage kopiert', 26 | copiedToClipBoard : 'Inhalt in Zwischenablage kopiert', 27 | }, 28 | searchCompanion: { 29 | title : 'Suchbegleiter', 30 | search : 'Suche', 31 | searchHint : 'Vertiefen Sie dieses Thema', 32 | clear : 'Verlauf löschen', 33 | }, 34 | conversations: { 35 | me : 'Ich', 36 | welcome : 'Hallo, wie kann ich Ihnen helfen?', 37 | title : 'Konversationen', 38 | newConversation : 'Neues Gespräch', 39 | sttHint : 'Hören aktivieren mit Strg+Leertaste', 40 | sttOn : 'Spracherkennung: Aktiviert', 41 | sttOff : 'Spracherkennung: Deaktiviert', 42 | inputHint : 'Bestätigen Sie mit Strg+Eingabetaste oder verwenden Sie die Spracherkennung mit Strg+Leertaste', 43 | wordCount : 'Wörter', 44 | }, 45 | profiles: { 46 | title : 'Profile', 47 | newProfile : 'Neues Profil', 48 | selectProfile : 'Profil auswählen', 49 | selected : 'Aktives Profil: {{name}}', 50 | name : 'Titel', 51 | active : 'Aktiv', 52 | tags : 'Tags', 53 | description : 'Beschreibung', 54 | prompt : 'Systemaufforderung', 55 | 56 | }, 57 | settings: { 58 | general : 'Allgemein', 59 | global : 'Globale Einstellungen' 60 | }, 61 | searchSettings: { 62 | interfaceLanguage : 'Schnittstellensprache', 63 | showSidebarOnAllSites : 'Seitenleiste auf allen Seiten anzeigen', 64 | triggerModeTitle : 'Auslösemodus', 65 | triggerModeManual : 'Manuell', 66 | triggerModeManualHint : 'Nur bei manueller Suche auslösen', 67 | triggerModeQuestion : 'Frage', 68 | triggerModeQuestionHint : 'Nur bei Endung mit: ? auslösen', 69 | triggerModeAuto : 'Automatisch', 70 | triggerModeAutoHint : 'Systematisch die Suchmaschine abfragen', 71 | apiKeyTitle : 'API-Schlüssel', 72 | apiKeyHint : 'Ihr OpenAI-API-Schlüssel', 73 | model : 'Modell', 74 | } 75 | } 76 | }; 77 | -------------------------------------------------------------------------------- /src/i18n/en/index.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | translation: { 3 | global: { 4 | yes : 'Yes', 5 | no : 'No', 6 | save : 'Save', 7 | close : 'Close', 8 | edit : 'Edit', 9 | copy : 'Copy', 10 | play : 'Play', 11 | record : 'Record', 12 | import : 'Import', 13 | export : 'Export', 14 | upload : 'Upload', 15 | cancel : 'Cancel', 16 | delete : 'Delete', 17 | retry : 'Retry', 18 | missing : 'missing', 19 | settings : 'Settings', 20 | search : 'Ask a question', 21 | searchHint : 'Or expand on a topic', 22 | authError : 'Error: {{statusCode}}\n\nSorry, something went wrong with authentication, please check your API key & model', 23 | comingSoon : 'coming soon', 24 | donate : 'Donate {{name}}', 25 | donateCopy : '{{name}} address copied to clipboard', 26 | copiedToClipBoard : 'Content copied to clipboard', 27 | }, 28 | searchCompanion: { 29 | title : 'Search Companion', 30 | search : 'Search', 31 | searchHint : 'Expand on this topic', 32 | clear : 'Clear conversation history', 33 | }, 34 | conversations: { 35 | me : 'me', 36 | welcome : 'Hi there, how may i assist you', 37 | title : 'Conversations', 38 | newConversation : 'New conversation', 39 | sttHint : 'Toggle listening with CTRL+SpaceBar', 40 | sttOn : 'Speech Recognition: Activated', 41 | sttOff : 'Speech Recognition: Deactivated', 42 | inputHint : 'You can set focus with CTRL, validate with CTRL+Enter, or use speech recognition with CTRL+SpaceBar', 43 | wordCount : 'Word count', 44 | }, 45 | profiles: { 46 | title : 'Profiles', 47 | newProfile : 'New profile', 48 | selectProfile : 'Select a profile', 49 | selected : 'Active Profile: {{name}}', 50 | name : 'Title', 51 | active : 'Active', 52 | tags : 'Tags', 53 | description : 'Description', 54 | prompt : 'System prompt', 55 | 56 | }, 57 | settings: { 58 | general : 'General', 59 | global : 'Global settings' 60 | }, 61 | searchSettings: { 62 | interfaceLanguage : 'Interface language', 63 | showSidebarOnAllSites : 'Show sidebar on all sites', 64 | triggerModeTitle : 'Trigger Mode', 65 | triggerModeManual : 'Manual', 66 | triggerModeManualHint : 'Only trigger search on manual click', 67 | triggerModeQuestion : 'Question', 68 | triggerModeQuestionHint : 'Only trigger search if ending with: ?', 69 | triggerModeAuto : 'Automatic', 70 | triggerModeAutoHint : 'Systematically query the search engine', 71 | apiKeyTitle : 'API Key', 72 | apiKeyHint : 'Your OpenAI API key', 73 | model : 'Model', 74 | } 75 | } 76 | }; 77 | -------------------------------------------------------------------------------- /src/i18n/es/index.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | translation: { 3 | global: { 4 | yes : 'Sí', 5 | no : 'No', 6 | save : 'Guardar', 7 | close : 'Cerrar', 8 | edit : 'Editar', 9 | copy : 'Copiar', 10 | play : 'Reproducir', 11 | record : 'Grabar', 12 | import : 'Importar', 13 | export : 'Exportar', 14 | upload : 'Cargar', 15 | cancel : 'Cancelar', 16 | delete : 'Eliminar', 17 | retry : 'Reintentar', 18 | missing : 'faltante', 19 | settings : 'Ajustes', 20 | search : 'Hacer una pregunta', 21 | searchHint : 'O expande un tema', 22 | authError : 'Error: {{statusCode}}\noops, algo salió mal con la autenticación, por favor verifica tu clave API y modelo', 23 | comingSoon : 'próximamente disponible', 24 | donate : 'Donar para {{name}}', 25 | donateCopy : 'Dirección {{name}} copiada al portapapeles', 26 | copiedToClipBoard : 'Contenido copiado al portapapeles' 27 | }, 28 | searchCompanion: { 29 | title : 'Compañero de búsqueda', 30 | search : 'Buscar', 31 | searchHint : 'Expandir este tema', 32 | clear : 'Borrar historial de conversaciones' 33 | }, 34 | conversations: { 35 | me : 'yo', 36 | welcome : 'Hola, ¿cómo puedo ayudarte?', 37 | title : 'Conversaciones', 38 | newConversation : 'Nueva conversación', 39 | sttHint : 'Puedes activarlo/desactivarlo con CTRL+Espacio', 40 | sttOn : 'Reconocimiento de voz: Activado', 41 | sttOff : 'Reconocimiento de voz: Desactivado', 42 | inputHint : 'Puedes validar con CTRL+Intro, o utilizar el reconocimiento de voz con CTRL+Espacio', 43 | wordCount : 'Número de palabras' 44 | }, 45 | profiles: { 46 | title : 'Perfiles', 47 | newProfile : 'Nuevo perfil', 48 | selectProfile : 'Seleccionar perfil', 49 | selected : 'Perfil activo: {{name}}', 50 | name : 'Perfiles', 51 | active : 'Activo', 52 | tags : 'Etiquetas', 53 | description : 'Descripción', 54 | prompt : 'Comando de invitación' 55 | }, 56 | settings: { 57 | general : 'General', 58 | global : 'Configuraciones globales' 59 | }, 60 | searchSettings: { 61 | interfaceLanguage : 'Idioma de la interfaz', 62 | showSidebarOnAllSites : 'Mostrar barra lateral en todos los sitios', 63 | triggerModeTitle : 'Modo de activación', 64 | triggerModeManual : 'Manual', 65 | triggerModeManualHint : 'Activación solo en clic manual', 66 | triggerModeQuestion : 'Pregunta', 67 | triggerModeQuestionHint : 'Activación solo si termina en: ?', 68 | triggerModeAuto : 'Automático', 69 | triggerModeAutoHint : 'Interrogar sistemáticamente el motor de búsqueda', 70 | apiKeyTitle : 'Clave API', 71 | apiKeyHint : 'Tu clave API de OpenAI', 72 | model : 'Modelo' 73 | } 74 | } 75 | }; 76 | -------------------------------------------------------------------------------- /src/i18n/fr/index.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | translation: { 3 | global: { 4 | yes : 'Oui', 5 | no : 'Non', 6 | save : 'Enregistrer', 7 | close : 'Fermer', 8 | edit : 'Modifier', 9 | copy : 'Copier', 10 | play : 'Jouer', 11 | record : 'Enregistrer', 12 | import : 'Importer', 13 | export : 'Exporter', 14 | upload : 'Charger', 15 | cancel : 'Annuler', 16 | delete : 'Supprimer', 17 | retry : 'Ressayer', 18 | missing : 'manquant', 19 | settings : 'Settings', 20 | search : 'Poser une question', 21 | searchHint : 'Ou développez un sujet', 22 | authError : 'Erreur: {{statusCode}}\n\nDésolé, quelque chose s\'est mal passé avec l\'authentification, veuillez vérifier votre clé API & modèle', 23 | comingSoon : 'bientôt disponible', 24 | donate : 'Faire un don de {{name}}', 25 | donateCopy : 'Adresse {{name}} copié dans le presse-papier', 26 | copiedToClipBoard : 'Contenu copié dans le presse-papier', 27 | }, 28 | searchCompanion: { 29 | title : 'Compagnon de recherche', 30 | search : 'Chercher', 31 | searchHint : 'Développer ce sujet', 32 | clear : 'Effacer l\'hitorique des conversations', 33 | }, 34 | conversations: { 35 | me : 'moi', 36 | welcome : 'Bonjour, comment puis-je vous aider', 37 | title : 'Conversations', 38 | newConversation : 'Nouvelle conversation', 39 | sttHint : 'Vous pouvez l\'activer/désactiver avec CTRL+Espace', 40 | sttOn : 'Reconnaissance Vocale: Activée', 41 | sttOff : 'Reconnaissance Vocale: Désactivée', 42 | inputHint : 'Vous pouvez valider avec CTRL+Entrée, ou utiliser la reconnaissance vocale avec CTRL+Espace', 43 | wordCount : 'Nombre de mots', 44 | }, 45 | profiles: { 46 | title : 'Profils', 47 | newProfile : 'Nouveau profil', 48 | selectProfile : 'Sélectionner un profil', 49 | selected : 'Profil Actif: {{name}}', 50 | name : 'Profils', 51 | active : 'Actif', 52 | tags : 'Tags', 53 | description : 'Déscription', 54 | prompt : 'Invité de commande' 55 | }, 56 | settings: { 57 | general : 'Général', 58 | global : 'Paramètres globaux' 59 | }, 60 | searchSettings: { 61 | interfaceLanguage : 'Langue de l\'interface', 62 | showSidebarOnAllSites : 'Afficher la barre latérale sur tous les sites', 63 | triggerModeTitle : 'Mode de déclenchement', 64 | triggerModeManual : 'Manuel', 65 | triggerModeManualHint : 'Déclenchement uniquement sur clic manuel', 66 | triggerModeQuestion : 'Question', 67 | triggerModeQuestionHint : 'Déclenchement uniquement si se termine par : ?', 68 | triggerModeAuto : 'Automatique', 69 | triggerModeAutoHint : 'Interroger systématiquement le moteur de recherche', 70 | apiKeyTitle : 'Clé API', 71 | apiKeyHint : 'Votre clé API OpenAI', 72 | model : 'Modèle', 73 | } 74 | } 75 | }; 76 | -------------------------------------------------------------------------------- /src/i18n/hi/index.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | translation: { 3 | global: { 4 | yes : 'हाँ', 5 | no : 'नहीं', 6 | save : 'सहेजें', 7 | close : 'बंद करें', 8 | edit : 'संपादित करें', 9 | copy : 'कॉपी करें', 10 | play : 'खेलें', 11 | record : 'रिकॉर्ड करें', 12 | import : 'आयात करें', 13 | export : 'निर्यात करें', 14 | upload : 'अपलोड करें', 15 | cancel : 'रद्द करें', 16 | delete : 'हटाएँ', 17 | retry : 'पुन: प्रयास करें', 18 | missing : 'खोया हुआ', 19 | settings : 'सेटिंग्स', 20 | search : 'सवाल पूछें', 21 | searchHint : 'या एक विषय पर विस्तार करें', 22 | authError : 'त्रुटि: {{statusCode}}\n\nक्षमा करें, प्रमाणीकरण के साथ कुछ गलत हो गया है, कृपया अपनी एपीआई कुंजी और मॉडल जांचें', 23 | comingSoon : 'जल्द ही आ रहा है', 24 | donate : '{{name}} दान करें', 25 | donateCopy : '{{name}} पता क्लिपबोर्ड पर कॉपी हो गया है', 26 | copiedToClipBoard : 'सामग्री क्लिपबोर्ड पर कॉपी हो गई है' 27 | }, 28 | searchCompanion: { 29 | title : 'खोज सहयोगी', 30 | search : 'खोजें', 31 | searchHint : 'इस विषय पर विस्तार करें', 32 | clear : 'बातचीत इतिहास साफ करें' 33 | }, 34 | conversations: { 35 | me : 'मुझसे', 36 | welcome : 'हाय वहाँ, मैं आपकी केसे सहायता कर सकता हूँ?', 37 | title : 'बातचीतें', 38 | newConversation : 'नई बातचीत', 39 | sttHint : 'CTRL+स्पेसबार के साथ सुनना टॉगल करें', 40 | sttOn : 'वाणी ज्ञान प्राप्ति: सक्रिय', 41 | sttOff : 'वाणी ज्ञान प्राप्ति: निष्क्रिय', 42 | inputHint : 'आप CTRL+Enter के साथ मान्य कर सकते हैं, या CTRL+स्पेसबार के साथ वाणी ज्ञान प्राप्ति का उपयोग कर सकते हैं', 43 | wordCount : 'शब्द गणना' 44 | }, 45 | profiles: { 46 | title : 'प्रोफ़ाइल्स', 47 | newProfile : 'नई प्रोफाइल', 48 | selectProfile : 'एक प्रोफाइल चुनें', 49 | selected : 'सक्रिय प्रोफ़ाइल: {{name}}', 50 | name : 'शीर्षक', 51 | active : 'सक्रिय', 52 | tags : 'टैग', 53 | description : 'विवरण', 54 | prompt : 'सिस्टम प्रॉम्प्ट' 55 | }, 56 | settings: { 57 | general : 'सामान्य', 58 | global : 'वैश्विक सेटिंग्स' 59 | }, 60 | searchSettings: { 61 | interfaceLanguage : 'इंटरफ़ेस भाषा', 62 | showSidebarOnAllSites : 'सभी साइटों पर साइडबार दिखाएं', 63 | triggerModeTitle : 'ट्रिगर मोड', 64 | triggerModeManual : 'मैनुअल', 65 | triggerModeManualHint : 'केवल मैनुअल क्लिक पर खोज ट्रिगर करें', 66 | triggerModeQuestion : 'सवाल', 67 | triggerModeQuestionHint : 'केवल सवाल समाप्त होने पर खोज ट्रिगर करें :?', 68 | triggerModeAuto : 'ऑटोमैटिक', 69 | triggerModeAutoHint : 'सिस्टमैटिकली सवाल इंजन क्वेरी करें', 70 | apiKeyTitle : 'एपीआई कुंजी', 71 | apiKeyHint : 'आपकी ओपनएआई एपीआई कुंजी', 72 | model : 'मॉडल' 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/i18n/ja/index.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | translation: { 3 | global: { 4 | yes : 'はい', 5 | no : 'いいえ', 6 | save : '保存', 7 | close : '閉じる', 8 | edit : '編集', 9 | copy : '複製', 10 | play : '再生', 11 | record : '録音', 12 | import : 'インポート', 13 | export : 'エクスポート', 14 | upload : 'アップロード', 15 | cancel : 'キャンセル', 16 | delete : '削除', 17 | retry : '再試行', 18 | missing : '見つかりません', 19 | settings : '設定', 20 | search : '質問してください', 21 | searchHint : 'またはトピックを拡大してください', 22 | authError : 'エラー:{{statusCode}}\n\n申し訳ありませんが、認証で問題が発生しました。APIキーとモデルを確認してください', 23 | comingSoon : '近日公開', 24 | donate : '{{name}}を寄付する', 25 | donateCopy : '{{name}}のアドレスがクリップボードにコピーされました', 26 | copiedToClipBoard : 'コンテンツがクリップボードにコピーされました', 27 | }, 28 | searchCompanion: { 29 | title : '検索コンパニオン', 30 | search : '検索', 31 | searchHint : 'このトピックを拡大してください', 32 | clear : '会話履歴をクリア', 33 | }, 34 | conversations: { 35 | me : '私', 36 | welcome : 'こんにちは、いつでもどうぞ', 37 | title : '会話', 38 | newConversation : '新しい会話', 39 | sttHint : 'CTRL+SpaceBarで音声入力', 40 | sttOn : '音声認識:オン', 41 | sttOff : '音声認識:オフ', 42 | inputHint : 'CTRL+Enterで検索またはCTRL+SpaceBarで音声認識', 43 | wordCount : '単語数', 44 | }, 45 | profiles: { 46 | title : 'プロフィール', 47 | newProfile : '新しいプロフィール', 48 | selectProfile : 'プロフィールを選択', 49 | selected : 'アクティブプロファイル:{{name}}', 50 | name : 'タイトル', 51 | active : 'アクティブ', 52 | tags : 'タグ', 53 | description : '説明', 54 | prompt : 'システムプロンプト', 55 | 56 | }, 57 | settings: { 58 | general : '一般', 59 | global : 'グローバル設定' 60 | }, 61 | searchSettings: { 62 | interfaceLanguage : 'インターフェース言語', 63 | showSidebarOnAllSites : 'すべてのサイトでサイドバーを表示', 64 | triggerModeTitle : 'トリガーモード', 65 | triggerModeManual : 'マニュアル', 66 | triggerModeManualHint : 'マニュアルクリックでのみ検索をトリガーする', 67 | triggerModeQuestion : '質問', 68 | triggerModeQuestionHint : '質問符 (?) で終わる場合にのみ検索をトリガーする', 69 | triggerModeAuto : '自動', 70 | triggerModeAutoHint : '定期的に検索エンジンにクエリを送信する', 71 | apiKeyTitle : 'APIキー', 72 | apiKeyHint : 'あなたのOpenAI APIキー', 73 | model : 'モデル', 74 | } 75 | } 76 | }; 77 | -------------------------------------------------------------------------------- /src/i18n/pt/index.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | translation: { 3 | global: { 4 | yes : 'Sim', 5 | no : 'Não', 6 | save : 'Salvar', 7 | close : 'Fechar', 8 | edit : 'Editar', 9 | copy : 'Copiar', 10 | play : 'Reproduzir', 11 | record : 'Gravar', 12 | import : 'Importar', 13 | export : 'Exportar', 14 | upload : 'Enviar', 15 | cancel : 'Cancelar', 16 | delete : 'Excluir', 17 | retry : 'Tentar Novamente', 18 | missing : 'Perdido', 19 | settings : 'Configurações', 20 | search : 'Pergunte uma questão', 21 | searchHint : 'Ou expanda um tópico', 22 | authError : 'Erro: {{statusCode}}\n\nDesculpe, ocorreu um erro na autenticação, verifique sua chave API e seu modelo', 23 | comingSoon : 'em breve', 24 | donate : 'Doar {{name}}', 25 | donateCopy : 'Endereço {{name}} copiado para área de transferência', 26 | copiedToClipBoard : 'Conteúdo copiado para área de transferência', 27 | }, 28 | searchCompanion: { 29 | title : 'Companheiro de Busca', 30 | search : 'Procurar', 31 | searchHint : 'Expandir neste tópico', 32 | clear : 'Limpar histórico de conversas', 33 | }, 34 | conversations: { 35 | me : 'eu', 36 | welcome : 'Olá, como posso ajudar?', 37 | title : 'Conversas', 38 | newConversation : 'Nova conversa', 39 | sttHint : 'Alternar escuta com CTRL+Barra de Espaço', 40 | sttOn : 'Reconhecimento de fala: Ativado', 41 | sttOff : 'Reconhecimento de fala: Desativado', 42 | inputHint : 'Você pode validar com CTRL + Enter ou use a fala com CTRL + Barra de Espaço', 43 | wordCount : 'Contagem de Palavras', 44 | }, 45 | profiles: { 46 | title : 'Perfis', 47 | newProfile : 'Novo perfil', 48 | selectProfile : 'Selecione um perfil', 49 | selected : 'Perfil Ativo: {{name}}', 50 | name : 'Título', 51 | active : 'Ativo', 52 | tags : 'Tags', 53 | description : 'Descrição', 54 | prompt : 'Solicitação do sistema', 55 | 56 | }, 57 | settings: { 58 | general : 'Geral', 59 | global : 'Configurações Globais' 60 | }, 61 | searchSettings: { 62 | interfaceLanguage : 'Idioma da interface', 63 | showSidebarOnAllSites : 'Mostrar barra lateral em todos os sites', 64 | triggerModeTitle : 'Modo de Ativação', 65 | triggerModeManual : 'Manual', 66 | triggerModeManualHint : 'Ativar buscas somente ao clicar manualmente', 67 | triggerModeQuestion : 'Pergunta', 68 | triggerModeQuestionHint : 'Ativar buscas somente em perguntas com ponto de interrogação no final', 69 | triggerModeAuto : 'Automático', 70 | triggerModeAutoHint : 'Realizar consulta no motor de busca de maneira sistemática', 71 | apiKeyTitle : 'Chave da API', 72 | apiKeyHint : 'Sua chave de API do OpenAI', 73 | model : 'Modelo', 74 | } 75 | } 76 | }; 77 | -------------------------------------------------------------------------------- /src/i18n/ru/index.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | translation: { 3 | global: { 4 | yes : 'Да', 5 | no : 'Нет', 6 | save : 'Сохранить', 7 | close : 'Закрыть', 8 | edit : 'Править', 9 | play : 'Слушать', 10 | record : 'Запись', 11 | import : 'Импорт', 12 | export : 'Экспорт', 13 | upload : 'Закачка', 14 | cancel : 'Отмена', 15 | delete : 'Удалить', 16 | retry : 'Повторить', 17 | missing : 'отсутствует', 18 | settings : 'Настройки', 19 | search : 'Введи вопрос', 20 | searchHint : 'Или раскрой тему', 21 | authError : 'Ошибка: {{statusCode}}\n\nЧто-то не так с аутентификацией, проверьте ваш API ключ и модел.', 22 | comingSoon : 'скоро', 23 | donate : 'Пожертвовать {{name}}', 24 | donateCopy : '{{name}} адрес скопирован в буфер обмена', 25 | copiedToClipBoard : 'Содержимое скопировано в буфер обмена', 26 | }, 27 | searchCompanion: { 28 | title : 'Поисковый компаньон', 29 | login : 'Логин', 30 | search : 'Поиск', 31 | searchHint : 'Развернуть тему', 32 | clear : 'Очистить историю разговоря', 33 | copy : 'Скопировать результат', 34 | }, 35 | conversations: { 36 | me : 'Я', 37 | welcome : 'Привет, чем могу вам помочь?', 38 | title : 'Диалоги', 39 | newConversation : 'Новый диалог', 40 | sttHint : 'Переключить диктовку по CTRL+SpaceBar', 41 | sttOn : 'Диктовка: активна', 42 | sttOff : 'Диктовка: неактивна', 43 | inputHint : 'Можно отправить по CTRL+Enter, или воспользоваться диктовкой по CTRL+SpaceBar', 44 | wordCount : 'Всего слов', 45 | }, 46 | profiles: { 47 | title : 'Профили', 48 | newProfile : 'Новый профиль', 49 | selectProfile : 'Выбрать профиль', 50 | selected : 'Текущий профиль: {{name}}', 51 | name : 'Подпись', 52 | active : 'Активен', 53 | tags : 'Тэги', 54 | description : 'Описание', 55 | prompt : 'Системная затравка', 56 | 57 | }, 58 | settings: { 59 | general : 'Общие', 60 | global : 'Глобальные настройки' 61 | }, 62 | searchSettings: { 63 | interfaceLanguage : 'Язык интерфейса', 64 | showSidebarOnAllSites : 'Показывать боковую панель на сайтах', 65 | triggerModeTitle : 'Режим срабатывания', 66 | triggerModeManual : 'По требованию', 67 | triggerModeManualHint : 'Только, когда пользователь самостоятельно отправляет запрос', 68 | triggerModeQuestion : 'По знаку вопроса', 69 | triggerModeQuestionHint : 'Отправляет, если предложение оканчивается на "?"', 70 | triggerModeAuto : 'Автоматически', 71 | triggerModeAutoHint : 'Регулярно опрашивать поиск', 72 | apiKeyTitle : 'API Ключ', 73 | apiKeyHint : 'Ваш ключ OpenAI API', 74 | model : 'Модель', 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/i18n/tr/index.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | translation: { 3 | global: { 4 | yes : 'Evet', 5 | no : 'Hayır', 6 | save : 'Kaydet', 7 | close : 'Kapat', 8 | edit : 'Düzenle', 9 | copy : 'Kopyala', 10 | play : 'Oynat', 11 | record : 'Kaydet', 12 | import : 'İçe Aktar', 13 | export : 'Dışa Aktar', 14 | upload : 'Yükle', 15 | cancel : 'İptal', 16 | delete : 'Sil', 17 | retry : 'Yeniden Dene', 18 | missing : 'eksik', 19 | settings : 'Ayarlar', 20 | search : 'Soru Sormak İçin Tıklayın', 21 | searchHint : 'Veya bir konuda genişleyin', 22 | authError : 'Hata: {{statusCode}}\n\nÜzgünüz, kimlik doğrulamada bir sorun oluştu, lütfen API anahtarınızı ve modelinizi kontrol edin', 23 | comingSoon : 'Çok Yakında', 24 | donate : '{{name}} Bağış Yap', 25 | donateCopy : '{{name}} bağış adresi panoya kopyalandı', 26 | copiedToClipBoard : 'İçerik panoya kopyalandı', 27 | }, 28 | searchCompanion: { 29 | title : 'Arama Asistanı', 30 | search : 'Ara', 31 | searchHint : 'Bu konuda genişleyin', 32 | clear : 'Konuşma geçmişini temizle', 33 | }, 34 | conversations: { 35 | me : 'ben', 36 | welcome : 'Merhaba, nasıl yardımcı olabilirim?', 37 | title : 'Konuşmalar', 38 | newConversation : 'Yeni Konuşma', 39 | sttHint : 'CTRL+Boşluk ile dinlemeyi açın/kapatın', 40 | sttOn : 'Konuşma Tanıma: Aktif', 41 | sttOff : 'Konuşma Tanıma: Pasif', 42 | inputHint : 'CTRL+Enter ile doğrulayabilir veya CTRL+Boşluk kullanarak konuşma tanımayı kullanabilirsiniz', 43 | wordCount : 'Kelime Sayısı', 44 | }, 45 | profiles: { 46 | title : 'Profil Ayarları', 47 | newProfile : 'Yeni Profil', 48 | selectProfile : 'Bir Profil Seçin', 49 | selected : 'Aktif Profil: {{name}}', 50 | name : 'Başlık', 51 | active : 'Aktif', 52 | tags : 'Etiketler', 53 | description : 'Açıklama', 54 | prompt : 'Sistem İsteği', 55 | 56 | }, 57 | settings: { 58 | general : 'Genel', 59 | global : 'Global Ayarlar' 60 | }, 61 | searchSettings: { 62 | interfaceLanguage : 'Arayüz Dili', 63 | showSidebarOnAllSites : 'Tüm Sitelerde Kenar Çubuğunu Göster', 64 | triggerModeTitle : 'Tetikleme Modu', 65 | triggerModeManual : 'Manuel', 66 | triggerModeManualHint : 'Sadece manuel tıklamada aramayı tetikler', 67 | triggerModeQuestion : 'Soru', 68 | triggerModeQuestionHint : 'Sadece "?" ile bitenlerde aramayı tetikler', 69 | triggerModeAuto : 'Otomatik', 70 | triggerModeAutoHint : 'Sistemli olarak arama motoruna sorgu gönderir', 71 | apiKeyTitle : 'API Anahtarı', 72 | apiKeyHint : 'OpenAI API anahtarınız', 73 | model : 'Model', 74 | } 75 | } 76 | }; 77 | -------------------------------------------------------------------------------- /src/i18n/types.ts: -------------------------------------------------------------------------------- 1 | export const translation = { 2 | global: { 3 | yes : 'global.yes', 4 | no : 'global.no', 5 | save : 'global.save', 6 | close : 'global.close', 7 | edit : 'global.edit', 8 | copy : 'global.copy', 9 | play : 'global.play', 10 | record : 'global.record', 11 | import : 'global.import', 12 | export : 'global.export', 13 | upload : 'global.upload', 14 | cancel : 'global.cancel', 15 | delete : 'global.delete', 16 | retry : 'global.retry', 17 | missing : 'global.missing', 18 | settings : 'global.settings', 19 | search : 'global.search', 20 | searchHint : 'global.searchHint', 21 | authError : 'global.authError', 22 | comingSoon : 'global.comingSoon', 23 | donate : 'global.donate', 24 | donateCopy : 'global.donateCopy', 25 | copiedToClipBoard : 'global.copiedToClipBoard', 26 | }, 27 | searchCompanion: { 28 | title : 'searchCompanion.title', 29 | search : 'searchCompanion.search', 30 | searchHint : 'searchCompanion.searchHint', 31 | clear : 'searchCompanion.clear', 32 | }, 33 | conversations: { 34 | me : 'conversations.me', 35 | welcome : 'conversations.welcome', 36 | title : 'conversations.title', 37 | newConversation : 'conversations.newConversation', 38 | sttHint : 'conversations.sttHint', 39 | sttOn : 'conversations.sttOn', 40 | sttOff : 'conversations.sttOff', 41 | inputHint : 'conversations.inputHint', 42 | wordCount : 'conversations.wordCount', 43 | }, 44 | profiles: { 45 | title : 'profiles.title', 46 | newProfile : 'profiles.newProfile', 47 | selectProfile : 'profiles.selectProfile', 48 | selected : 'profiles.selected', 49 | name : 'profiles.name', 50 | active : 'profiles.active', 51 | tags : 'profiles.tags', 52 | description : 'profiles.description', 53 | prompt : 'profiles.prompt', 54 | }, 55 | settings: { 56 | general : 'settings.general', 57 | global : 'settings.global' 58 | }, 59 | searchSettings: { 60 | interfaceLanguage : 'searchSettings.interfaceLanguage', 61 | showSidebarOnAllSites : 'searchSettings.showSidebarOnAllSites', 62 | triggerModeTitle : 'searchSettings.triggerModeTitle', 63 | triggerModeManual : 'searchSettings.triggerModeManual', 64 | triggerModeManualHint : 'searchSettings.triggerModeManualHint', 65 | triggerModeQuestion : 'searchSettings.triggerModeQuestion', 66 | triggerModeQuestionHint : 'searchSettings.triggerModeQuestionHint', 67 | triggerModeAuto : 'searchSettings.triggerModeAuto', 68 | triggerModeAutoHint : 'searchSettings.triggerModeAutoHint', 69 | apiKeyTitle : 'searchSettings.apiKeyTitle', 70 | apiKeyHint : 'searchSettings.apiKeyHint', 71 | model : 'searchSettings.model', 72 | } 73 | }; 74 | -------------------------------------------------------------------------------- /src/i18n/zh/index.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | translation: { 3 | global: { 4 | yes : '是', 5 | no : '不', 6 | save : '保存', 7 | close : '关闭', 8 | edit : '编辑', 9 | copy : '复制', 10 | play : '播放', 11 | record : '录制', 12 | import : '导入', 13 | export : '导出', 14 | upload : '上传', 15 | cancel : '取消', 16 | delete : '删除', 17 | retry : '重试', 18 | missing : '丢失', 19 | settings : '设置', 20 | search : '提出问题', 21 | searchHint : '或扩展某个主题', 22 | authError : '错误:{{statusCode}}\n\n对不起,身份验证出现问题,请检查您的 API 密钥和模型', 23 | comingSoon : '即将上线', 24 | donate : '捐赠 {{name}}', 25 | donateCopy : '{{name}} 地址已复制到剪贴板', 26 | copiedToClipBoard : '内容已复制到剪贴板', 27 | }, 28 | searchCompanion: { 29 | title : '搜索陪伴', 30 | search : '搜索', 31 | searchHint : '扩展此主题', 32 | clear : '清除对话历史', 33 | }, 34 | conversations: { 35 | me : '我', 36 | welcome : '你好,有什么可以帮助您的', 37 | title : '对话', 38 | newConversation : '新对话', 39 | sttHint : '使用 CTRL+SpaceBar 切换收听', 40 | sttOn : '语音识别:已启用', 41 | sttOff : '语音识别:已禁用', 42 | inputHint : '您可以使用 CTRL+Enter 进行验证,或使用 CTRL+SpaceBar 进行语音识别', 43 | wordCount : '单词统计', 44 | }, 45 | profiles: { 46 | title : '个人资料', 47 | newProfile : '新资料', 48 | selectProfile : '选择资料', 49 | selected : '活动资料:{{name}}', 50 | name : '标题', 51 | active : '活动中', 52 | tags : '标签', 53 | description : '描述', 54 | prompt : '系统提示', 55 | 56 | }, 57 | settings: { 58 | general : '通用', 59 | global : '全局设置' 60 | }, 61 | searchSettings: { 62 | interfaceLanguage : '界面语言', 63 | showSidebarOnAllSites : '在所有网站上显示侧边栏', 64 | triggerModeTitle : '触发模式', 65 | triggerModeManual : '手动', 66 | triggerModeManualHint : '仅在手动单击时触发搜索', 67 | triggerModeQuestion : '问题', 68 | triggerModeQuestionHint : '仅在以问号结尾时触发搜索', 69 | triggerModeAuto : '自动', 70 | triggerModeAutoHint : '系统性地查询搜索引擎', 71 | apiKeyTitle : 'API 密钥', 72 | apiKeyHint : '您的 OpenAI API 密钥', 73 | model : '模型', 74 | } 75 | } 76 | }; 77 | -------------------------------------------------------------------------------- /src/js/api/openai.api.ts: -------------------------------------------------------------------------------- 1 | import { fetchSSE } from '../libs/fetch-sse'; 2 | import { IPTResult } from '../libs/types/IGPTResponse'; 3 | import { IPTChatMessages } from '../libs/types/IPTChatRequest'; 4 | import { db } from '../controllers/dbController'; 5 | class OpenAi { 6 | constructor() { 7 | // do something 8 | } 9 | 10 | // TODO: session status can be 200, yet content is empty: handle this case 11 | // https://chat.openai.com/api/auth/session 12 | async getSession() { 13 | //console.log('openai.api.getSession()'); 14 | 15 | // try to get settings from database 16 | return await db.getUserSettings().then((data) => { 17 | //console.log('getSession()::getUserSettings()', data); 18 | 19 | if (data.apiKey) { 20 | return { 21 | error : false, 22 | status : 200, 23 | content: { 24 | accessToken: data.apiKey 25 | } 26 | } 27 | } 28 | else { 29 | return { 30 | error : true, 31 | status : 401, 32 | content: { 33 | accessToken: '', 34 | message : 'No API key found' 35 | } 36 | } 37 | } 38 | }); 39 | } 40 | 41 | async getAnswerStreamed( 42 | messages : IPTChatMessages[], 43 | accessToken: string, 44 | abortSignal: AbortSignal, 45 | callBack : (response: IPTResult) => void 46 | ) { 47 | console.log('openai.api.getAnswerStreamed()'); 48 | 49 | // https://platform.openai.com/docs/api-reference/completions/create 50 | // TODO: add dynamic question prefix/suffix 51 | 52 | const userSettings = await db.getUserSettings(); 53 | //console.log('getAnswerStreamed::userSettings', userSettings); 54 | 55 | const api = { 56 | 'model' : userSettings.model?.value || 'gpt-3.5-turbo', 57 | 'messages' : messages, 58 | 'temperature' : 1, 59 | 'stream' : true 60 | } 61 | 62 | //console.log('getAnswerStreamed', api); 63 | 64 | const requestOptions = { 65 | signal : abortSignal, 66 | method : 'POST', 67 | headers: { 68 | 'Content-Type' : 'application/json', 69 | 'Authorization': `Bearer ${accessToken}` 70 | }, 71 | body: JSON.stringify(api), 72 | onMessage(message: IPTResult) { 73 | //console.log('onMessage', message); 74 | callBack(message) 75 | }, 76 | }; 77 | 78 | try { 79 | await fetchSSE('https://api.openai.com/v1/chat/completions', requestOptions); 80 | } catch (error) { 81 | console.log('getAnswerStreamed error', error); 82 | } 83 | } 84 | } 85 | 86 | export const openAi = new OpenAi(); 87 | -------------------------------------------------------------------------------- /src/js/controllers/dbController.ts: -------------------------------------------------------------------------------- 1 | // db.js 2 | import Dexie, { IndexableType } from 'dexie'; 3 | import { IUserSettings, } from 'src/js/libs/types/IUserSettings'; 4 | import { IDBConversation } from 'src/js/libs/types/IDBConversation'; 5 | import { IDBProfile } from 'src/js/libs/types/IDBProfile'; 6 | 7 | import { triggerMode } from 'src/js/data/triggerMode'; 8 | import { gptModels } from 'src/js/data/gpt-models'; 9 | 10 | // INFO: database works in background.js 11 | // You cannot see the data from the serviceworker dev tools panel 12 | // However you can see the data from the content.js dev tools panel 13 | class dbController extends Dexie { 14 | //#region ------------------------ Setup --------------------- 15 | userSettings! : Dexie.Table; 16 | conversations!: Dexie.Table; 17 | profiles!: Dexie.Table; 18 | 19 | constructor() { 20 | super('powertoys.oai'); 21 | 22 | // INFO: See indexable types: https://dexie.org/docs/Indexable-Type 23 | this.version(1).stores({ 24 | // INFO: Normally only declare items that we want indexed 25 | userSettings : '++id, triggerMode, darkMode, apiKey, licenseKey', 26 | conversations: '++id, timestamp, title, conversation' 27 | }); 28 | 29 | this.version(2).stores({ 30 | // INFO: Normally only declare items that we want indexed 31 | userSettings : '++id, triggerMode, darkMode, apiKey, licenseKey', 32 | conversations: '++id, timestamp, title, conversation', 33 | profiles : '++id, *tags, selected' 34 | }); 35 | 36 | // v0.2.2 37 | this.version(3).stores({ 38 | // INFO: Normally only declare items that we want indexed 39 | userSettings : '++id, triggerMode, darkMode, apiKey, licenseKey', 40 | conversations: '++id, timestamp, title, conversation, profileId', 41 | profiles : '++id, *tags, selected' 42 | }); 43 | } 44 | //#endregion --------------------- Setup --------------------- 45 | 46 | //#region ------------------------ DB Operations --------------------- 47 | /** 48 | * Delete Database 49 | */ 50 | async deleteDatabase() { 51 | return this.delete(); 52 | } 53 | 54 | /** 55 | * Delete an entry from the database 56 | * @param {String} table Name of the table 57 | * @param {Mixed} key Key or Composite key 58 | */ 59 | async deleteItem(table: string, key: IndexableType) { 60 | this.table(table).delete(key); 61 | } 62 | //#endregion --------------------- DB Operations --------------------- 63 | 64 | //#region ------------------------ User Settings --------------------- 65 | async getUserSettings() : Promise { 66 | const userSettings = await this.userSettings.get({id: 0}); 67 | 68 | // set default values for items that may not exist (e.g. after updates) 69 | if (userSettings) { 70 | if (!userSettings.model) { 71 | userSettings.model = gptModels[0]; 72 | } 73 | 74 | return userSettings; 75 | } 76 | 77 | // no user settings found, return default values 78 | return { 79 | id : 0, 80 | darkMode : false, 81 | triggerMode: triggerMode.QUESTION, 82 | apiKey : '', 83 | licenseKey : '', 84 | showSidebar: false, 85 | model : gptModels[0] 86 | }; 87 | } 88 | 89 | async saveUserSettings(settings: IUserSettings) { 90 | //console.log('db.saveUserSettings()', settings); 91 | 92 | return this.userSettings 93 | //https://stackoverflow.com/questions/57087581/dexiedb-put-method-not-working-when-adding-primary-key-as-argument 94 | .put(settings) 95 | .then(() => { 96 | return settings; 97 | }) 98 | .catch(() => { 99 | return false; 100 | }); 101 | } 102 | //#endregion --------------------- User Settings --------------------- 103 | 104 | 105 | //#region ------------------------ Conversations --------------------- 106 | async getConversation(conversationId: number) : Promise { 107 | return await this.conversations.get({id: conversationId}) || undefined; 108 | } 109 | 110 | async getConversations() : Promise { 111 | return await this.conversations.orderBy('timestamp').reverse().toArray() ?? []; 112 | } 113 | 114 | async saveConversation(conversation: IDBConversation) : Promise { 115 | //console.log('db.saveConversation()', conversation); 116 | 117 | // https://dexie.org/docs/Table/Table.put() 118 | // https://stackoverflow.com/questions/57087581/dexiedb-put-method-not-working-when-adding-primary-key-as-argument 119 | return this.conversations 120 | .put(conversation) 121 | .then((conversationId: number) => { 122 | //console.log('db::savedConversation', conversation, conversationId); 123 | return conversationId; 124 | }) 125 | .catch((error) => { 126 | console.log('db::saveConversationError', error); 127 | return 0; 128 | }); 129 | } 130 | 131 | async updateConversation(id: number, changes: {[keyPath: string]: any }) : Promise { 132 | //console.log('db.updateConversation()', key, changes); 133 | return this.conversations 134 | .update(id, changes) 135 | .catch((error) => { 136 | console.log('db::updateConversation', error); 137 | return 0; 138 | }); 139 | } 140 | 141 | async deleteConversation(conversationId: number) { 142 | return await this.conversations.delete(conversationId); 143 | } 144 | //#endregion --------------------- Conversations --------------------- 145 | 146 | //#region ------------------------ Profiles --------------------- 147 | async getProfiles() : Promise { 148 | return await this.profiles.orderBy('id').toArray() ?? []; 149 | } 150 | 151 | async getProfile(profileId: number) : Promise { 152 | //console.log('db.getProfile()', profileId); 153 | if (!profileId) { 154 | return 0; 155 | } 156 | 157 | return await this.profiles.get({id: profileId}) || 0; 158 | } 159 | 160 | async saveProfile(profile: IDBProfile) : Promise { 161 | //console.log('db.saveProfile()', profile); 162 | 163 | if (profile.selected) { 164 | // make sure this is the only profile that is selected 165 | await this.profiles.where({ selected: 1 }).modify({ selected: 0 }); 166 | } 167 | 168 | // https://dexie.org/docs/Table/Table.put() 169 | // https://stackoverflow.com/questions/57087581/dexiedb-put-method-not-working-when-adding-primary-key-as-argument 170 | return this.profiles 171 | .put(profile) 172 | .then((profileId: number) => { 173 | //console.log('db::savedProfile', profile, profileId); 174 | return profileId; 175 | }) 176 | .catch((error) => { 177 | console.log('db::saveProfileError', error); 178 | return 0; 179 | }); 180 | } 181 | 182 | async insertProfiles(profiles: IDBProfile[]) : Promise { 183 | //console.log('db.insertProfiles()', profiles); 184 | // https://dexie.org/docs/Table/Table.put() 185 | // https://stackoverflow.com/questions/57087581/dexiedb-put-method-not-working-when-adding-primary-key-as-argument 186 | return this.profiles 187 | .bulkPut(profiles) 188 | .then((profileId: number) => { 189 | //console.log('db::insertProfiles', profiles, profileId); 190 | return profileId; 191 | }) 192 | .catch((error) => { 193 | console.log('db::insertProfileError', error); 194 | return 0; 195 | }); 196 | } 197 | 198 | async getActiveProfile() { 199 | return await this.profiles.where({ selected: 1 }).first(); 200 | } 201 | 202 | async setAsDefaultProfile(profileId: number) { 203 | // set all other profiles to false 204 | await this.profiles.where({ selected: 1 }).modify({ selected: 0 }); 205 | 206 | return this.profiles 207 | .update(profileId, { selected: 1 }) 208 | .catch((/*error*/) => { 209 | //console.log('db::setAsDefaultProfileError', error); 210 | return 0; 211 | }); 212 | } 213 | 214 | async deleteProfile(profileId: number) { 215 | return await this.profiles.delete(profileId); 216 | } 217 | //#endregion --------------------- Profiles --------------------- 218 | } 219 | 220 | // export 221 | export const db = new dbController(); 222 | -------------------------------------------------------------------------------- /src/js/controllers/speechController.js: -------------------------------------------------------------------------------- 1 | export function speechToText(options = { language: 'en-US', callback: any }) { 2 | var started = false; 3 | var speech = function() { 4 | var recognition = new webkitSpeechRecognition(); 5 | 6 | function toggleRecognition() { 7 | if (started) { 8 | started = false; 9 | recognition.stop(); 10 | //console.log('recognition stop'); 11 | } 12 | else { 13 | started = true; 14 | recognition.start(); 15 | //console.log('recognition start'); 16 | } 17 | } 18 | 19 | // make sure we don't add the handler multiple times 20 | document.removeEventListener('toggle-record', toggleRecognition); 21 | document.addEventListener('toggle-record' , toggleRecognition); 22 | 23 | recognition.continuous = true; 24 | recognition.interimResults = false; 25 | recognition.lang = options.language; 26 | 27 | recognition.onresult = function(event) { 28 | var result = event.results[event.results.length - 1][0].transcript; 29 | 30 | //console.log('recognition onresult', options.target); 31 | options.callback(result); 32 | } 33 | } 34 | 35 | speech(); 36 | } 37 | 38 | export function textToSpeech(options = { sentence, language: 'en-US' }) { 39 | // config 40 | let speechSynthesis = { 41 | language: options.language, 42 | enabled : true, 43 | }; 44 | 45 | speechSynthesis.msg = new window.SpeechSynthesisUtterance(options.sentence); 46 | speechSynthesis.msg.lang = speechSynthesis.language; 47 | 48 | // speak text 49 | if (speechSynthesis.enabled) { 50 | window.speechSynthesis.speak(speechSynthesis.msg); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/js/data/default-tags.json: -------------------------------------------------------------------------------- 1 | [ 2 | "design", 3 | "development", 4 | "finance", 5 | "management", 6 | "marketing", 7 | "productivity", 8 | "recruitment", 9 | "sales", 10 | "other" 11 | ] 12 | -------------------------------------------------------------------------------- /src/js/data/gpt-models.ts: -------------------------------------------------------------------------------- 1 | export const gptModels = [ 2 | { 3 | label : 'GPT 3.5 (4 096 tokens)', 4 | value : 'gpt-3.5-turbo', 5 | maxTokens: 4096, 6 | pricing: { 7 | prompt : 0.0015, 8 | completion : 0.002 9 | } 10 | }, 11 | { 12 | label : 'GPT 3.5 (16 384 tokens)', 13 | value : 'gpt-3.5-turbo-16k', 14 | maxTokens: 16384, 15 | pricing: { 16 | prompt : 0.003, 17 | completion : 0.004 18 | } 19 | }, 20 | { 21 | label : 'GPT 4 (8 192 tokens)', 22 | value : 'gpt-4', 23 | maxTokens: 8192, 24 | pricing: { 25 | prompt : 0.03, 26 | completion : 0.06 27 | } 28 | }, 29 | { 30 | label : 'GPT 4 (32 768 tokens)', 31 | value : 'gpt-4-32k', 32 | maxTokens: 32768, 33 | pricing: { 34 | prompt : 0.06, 35 | completion : 0.12 36 | } 37 | } 38 | ] 39 | -------------------------------------------------------------------------------- /src/js/data/triggerMode.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * How the searchbox is triggered 3 | */ 4 | export const triggerMode = { 5 | MANUAL : 0, 6 | QUESTION: 1, 7 | AUTO : 2 8 | } 9 | -------------------------------------------------------------------------------- /src/js/libs/fetch-sse.ts: -------------------------------------------------------------------------------- 1 | import { createParser } from 'eventsource-parser'; 2 | import { IPTResult, IOAIStreamedResponse } from './types/IGPTResponse'; 3 | 4 | // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function* 5 | async function* streamAsyncIterable(stream: ReadableStream) { 6 | const reader = stream.getReader(); 7 | try { 8 | while (true) { 9 | const { done, value } = await reader.read(); 10 | if (done) { 11 | return; 12 | } 13 | yield value; 14 | } 15 | } 16 | catch (error) { 17 | console.log('streamAsyncIterable::error', error); 18 | } finally { 19 | reader.releaseLock(); 20 | } 21 | } 22 | 23 | export async function fetchSSE( 24 | url : string, 25 | options : RequestInit & { onMessage: (message: IPTResult) => void } 26 | ) { 27 | console.log('fetchSSE'); 28 | 29 | const { onMessage, ...requestOptions } = options; 30 | const resp = await fetch(url, requestOptions); 31 | 32 | if (!resp.ok) { 33 | // do something 34 | console.log('fetchSSE::error', resp); 35 | onMessage({ 36 | error : true, 37 | status : resp.status, 38 | message : resp.statusText, 39 | response: null 40 | }); 41 | } 42 | 43 | const parser = createParser((event) => { 44 | //console.log('parser', event); 45 | if (event.type === 'event') { 46 | if (event.data === '[DONE]') { 47 | // all done ! 48 | onMessage({ 49 | error : false, 50 | status : 999, 51 | message: '[DONE]', 52 | response: null 53 | }); 54 | return; 55 | } 56 | 57 | let data: IOAIStreamedResponse; 58 | try { 59 | data = JSON.parse(event.data); 60 | //console.log('streamed reponse', data); 61 | } 62 | catch (error) { 63 | console.log('Error parsing IOAIStreamedResponse json()', error); 64 | onMessage({ 65 | error : true, 66 | status : resp.status, 67 | message : `${error}`, 68 | response: null 69 | }); 70 | 71 | return; 72 | } 73 | 74 | onMessage({ 75 | error : false, 76 | status : resp.status, 77 | message : null, 78 | response: data, 79 | }); 80 | } 81 | }); 82 | 83 | for await (const chunk of streamAsyncIterable(resp.body)) { 84 | const str = new TextDecoder().decode(chunk); 85 | parser.feed(str); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/js/libs/helpers.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * See if a string ends with a question mark 3 | * @param question 4 | */ 5 | export function endsWithQuestionMark(question: string) : boolean { 6 | return ( 7 | question.endsWith('?') || // ASCII 8 | question.endsWith('?') || // Chinese/Japanese 9 | question.endsWith('؟') || // Arabic 10 | question.endsWith('⸮') // Arabic 11 | ) 12 | } 13 | 14 | /** 15 | * 16 | * @param str Returns an estimate of the number of tokens used 17 | */ 18 | export function getTokenCount(str: string) : number { 19 | if (str.length == 0) return 0; 20 | 21 | // get the average of words and 4 characters 22 | return Math.round((str.split(' ').length + str.length/4) / 2); 23 | } 24 | 25 | /** 26 | * Get the number of words in a string 27 | * @param s 28 | */ 29 | export function getWordCount(s: string) { 30 | // Info: try different versions 31 | // https://stackoverflow.com/a/18679657/896849 32 | 33 | s = s.replace(/(^\s*)|(\s*$)/gi,''); // exclude start and end white-space 34 | s = s.replace(/[ ]{2,}/gi,' '); // 2 or more space to 1 35 | s = s.replace(/\n /,'\n'); // exclude newline with a start spacing 36 | return s.split(' ').filter(function(str: string) { return str != ''; }).length; 37 | //return s.split(' ').filter(String).length; // - this can also be used 38 | } 39 | 40 | /** 41 | * Convert 2 letter code to a fully qualified language code 42 | * @param lang 43 | */ 44 | export function getFullyQualifiedLanguage(lang: string) { 45 | switch (lang) { 46 | case 'en': 47 | return 'en-US'; 48 | 49 | case 'fr': 50 | return 'fr-FR'; 51 | 52 | default: 53 | return 'en-US'; 54 | } 55 | } 56 | 57 | /** 58 | * Generate a random GUID 59 | */ 60 | export function guid() : string { 61 | return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, (c: number) => 62 | (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16) 63 | ); 64 | } 65 | 66 | /** 67 | * 68 | * @param file Gets the base64 representation of a file 69 | */ 70 | export function getBase64(file: File) : Promise { 71 | return new Promise((resolve, reject) => { 72 | const reader = new FileReader(); 73 | 74 | reader.onerror = (error) => { 75 | reject(error); 76 | } 77 | 78 | reader.onload = () => { 79 | resolve(reader.result as string); 80 | } 81 | 82 | reader.readAsDataURL(file); 83 | }); 84 | } 85 | 86 | /** 87 | * Get the body text from a URL 88 | * @param url 89 | */ 90 | export async function getTextFromUrl(url: string) { 91 | return await fetch(url) 92 | .then(response => response.text()) 93 | .then(html => { 94 | const parser = new DOMParser(); 95 | const doc = parser.parseFromString(html, 'text/html'); 96 | 97 | return doc.body.innerText; 98 | }) 99 | .catch(error => { 100 | return error; 101 | }); 102 | } 103 | -------------------------------------------------------------------------------- /src/js/libs/types/IDBConversation.ts: -------------------------------------------------------------------------------- 1 | import { IPTChatMessages } from './IPTChatRequest'; 2 | 3 | /** 4 | * Conversations as stored in the Database 5 | */ 6 | export interface IDBConversation { 7 | id? : number, 8 | timestamp : number, 9 | title : string, 10 | conversation: IPTChatMessages[], 11 | profileId : number 12 | } 13 | -------------------------------------------------------------------------------- /src/js/libs/types/IDBProfile.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Profiles as stored in the Database 3 | */ 4 | export interface IDBProfile { 5 | id? : number, 6 | title : string, 7 | description : string, 8 | tags : string[] 9 | content : string, 10 | icon? : string, 11 | isUserPrompt : number, 12 | selected : number 13 | } 14 | -------------------------------------------------------------------------------- /src/js/libs/types/IGPTResponse.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Final message sent back to UI backend (internal) 3 | */ 4 | export interface IPTResult { 5 | error : boolean, 6 | status : number, 7 | message : string | null, 8 | response : IOAIStreamedResponse | null, 9 | } 10 | 11 | /** 12 | * Streaming response format (from OpenAI) 13 | */ 14 | export interface IOAIStreamedResponse { 15 | id : string, 16 | model : string, 17 | created: number, 18 | choices: { 19 | index : number, 20 | delta : IOAIResponseMessage, 21 | finish_reason : null | 'stop', 22 | }[] 23 | } 24 | 25 | export interface IOAIResponseMessage { 26 | role? : 'system' | 'assistant' | 'user', 27 | content?: string, 28 | } 29 | -------------------------------------------------------------------------------- /src/js/libs/types/IPTChatRequest.ts: -------------------------------------------------------------------------------- 1 | export interface IPTChatRequest { 2 | model : string, 3 | messages : IPTChatMessages, 4 | temperature? : number, 5 | top_p? : number, 6 | n? : number, 7 | stream? : boolean, 8 | stop? : string | string[], 9 | max_tokens? : number, 10 | presence_penalty? : number, 11 | frequency_penalty? : number, 12 | logit_bias? : object, 13 | user? : string 14 | } 15 | 16 | export interface IPTChatMessages { 17 | role : 'system' | 'assistant' | 'user', 18 | content: string 19 | } 20 | -------------------------------------------------------------------------------- /src/js/libs/types/IUserSettings.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Settings stored in IndexedDB 3 | */ 4 | export interface IUserSettings { 5 | id : number, 6 | triggerMode : number, 7 | darkMode : boolean, 8 | apiKey? : string, 9 | licenseKey? : string, 10 | showSidebar?: boolean, 11 | model? : IGPTModel 12 | } 13 | 14 | export interface IGPTModel { 15 | label : string, 16 | value : string, 17 | maxTokens: number, 18 | pricing: { 19 | prompt : number, 20 | completion : number 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/layouts/CompanionLayout.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 12 | -------------------------------------------------------------------------------- /src/layouts/MainLayout.vue: -------------------------------------------------------------------------------- 1 | 111 | 112 | 159 | -------------------------------------------------------------------------------- /src/pages/ErrorNotFound.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 28 | -------------------------------------------------------------------------------- /src/quasar.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | // Forces TS to apply `@quasar/app-vite` augmentations of `quasar` package 4 | // Removing this would break `quasar/wrappers` imports as those typings are declared 5 | // into `@quasar/app-vite` 6 | // As a side effect, since `@quasar/app-vite` reference `quasar` to augment it, 7 | // this declaration also apply `quasar` own 8 | // augmentations (eg. adds `$q` into Vue component context) 9 | /// 10 | -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { route } from 'quasar/wrappers'; 2 | import { 3 | createMemoryHistory, 4 | createRouter, 5 | createWebHashHistory, 6 | createWebHistory, 7 | } from 'vue-router'; 8 | 9 | import routes from './routes'; 10 | 11 | /* 12 | * If not building with SSR mode, you can 13 | * directly export the Router instantiation; 14 | * 15 | * The function below can be async too; either use 16 | * async/await or return a Promise which resolves 17 | * with the Router instance. 18 | */ 19 | 20 | export default route(function (/* { store, ssrContext } */) { 21 | const createHistory = process.env.SERVER 22 | ? createMemoryHistory 23 | : (process.env.VUE_ROUTER_MODE === 'history' ? createWebHistory : createWebHashHistory); 24 | 25 | const Router = createRouter({ 26 | scrollBehavior: () => ({ left: 0, top: 0 }), 27 | routes, 28 | 29 | // Leave this as is and make changes in quasar.conf.js instead! 30 | // quasar.conf.js -> build -> vueRouterMode 31 | // quasar.conf.js -> build -> publicPath 32 | history: createHistory(process.env.VUE_ROUTER_BASE), 33 | }); 34 | 35 | return Router; 36 | }); 37 | -------------------------------------------------------------------------------- /src/router/routes.ts: -------------------------------------------------------------------------------- 1 | import { RouteRecordRaw } from 'vue-router'; 2 | 3 | const routes: RouteRecordRaw[] = [ 4 | // Routes for BEX Application 5 | { 6 | path : '/index', 7 | component: () => import('layouts/MainLayout.vue'), 8 | children : [ 9 | { 10 | path : '', 11 | component: () => import('src/pages/ConversationsPage.vue'), 12 | props : () => { 13 | return { 14 | settings : null 15 | } 16 | } 17 | }, 18 | { 19 | path : '/index/conversations/:conversationId(\\d+)', 20 | component: () => import('src/pages/ConversationsPage.vue'), 21 | props : (route) => { 22 | return { 23 | settings : null, 24 | conversationId : Number.parseInt(route.params.conversationId as string, 10) || 0 25 | } 26 | } 27 | }, 28 | { 29 | path : '/index/profiles/:id(\\d+)', 30 | component: () => import('src/pages/ProfilesPage.vue'), 31 | props : (route) => { 32 | return { 33 | id : Number.parseInt(route.params.id as string, 10) || 0 34 | } 35 | } 36 | }, 37 | { 38 | // TODO: move this to it's own page 39 | path : '/index/settings/:type?', 40 | component: () => import('src/pages/ConversationsPage.vue'), 41 | props : (route) => { 42 | return { 43 | settings : route.params.type || null 44 | } 45 | } 46 | }, 47 | ], 48 | }, 49 | // Routes for BEX 50 | { 51 | path : '/search-companion', 52 | component: () => import('src/layouts/CompanionLayout.vue'), 53 | children : [ 54 | { 55 | path : '', 56 | component: () => import('src/pages/CompanionPage.vue') 57 | }, 58 | ] 59 | }, 60 | 61 | // Always leave this as last one, 62 | // but you can also remove it 63 | { 64 | path : '/:catchAll(.*)*', 65 | component : () => import('pages/ErrorNotFound.vue'), 66 | beforeEnter: () => { 67 | // reject the navigation 68 | return true; 69 | }, 70 | }, 71 | ]; 72 | 73 | export default routes; 74 | -------------------------------------------------------------------------------- /src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | /// 4 | 5 | // Mocks all files ending in `.vue` showing them as plain Vue instances 6 | declare module '*.vue' { 7 | import type { DefineComponent } from 'vue'; 8 | const component: DefineComponent<{}, {}, any>; 9 | export default component; 10 | } 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@quasar/app-vite/tsconfig-preset", 3 | "compilerOptions": { 4 | "baseUrl": "." 5 | } 6 | } --------------------------------------------------------------------------------