├── .eslintrc ├── .gitignore ├── .prettierrc ├── DOCS.md ├── LICENSE ├── README.md ├── config ├── aliases.js ├── helpers.js ├── manifest │ ├── app_info.js │ ├── chrome.js │ ├── permissions.js │ └── v3.js ├── plugins.js ├── resolve.js ├── rules.js └── webpack.config.js ├── docs ├── connections.png ├── pageContext.png ├── params.png ├── persona.png ├── sidellama_app.png ├── vim.png ├── webSearch.png └── yt.png ├── eslint.config.mjs ├── package-lock.json ├── package.json ├── public ├── .DS_Store └── images │ ├── .DS_Store │ ├── android-chrome-384x384.png │ ├── apple-touch-icon.png │ ├── browserconfig.xml │ ├── chrome.png │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon.ico │ ├── favicon.png │ ├── mstile-150x150.png │ ├── nose.png │ ├── safari-pinned-tab.svg │ ├── sidellama.png │ └── site.webmanifest ├── src ├── .DS_Store ├── Globals.d.ts ├── background │ ├── index.ts │ └── util.ts ├── content │ ├── contracts │ │ └── ContentProvider.ts │ ├── controllers │ │ └── CursorController.tsx │ ├── index.scss │ ├── index.tsx │ └── messaging.svg ├── sidePanel │ ├── AddToChat.tsx │ ├── Automation.tsx │ ├── Background.tsx │ ├── ChatHistory.tsx │ ├── ConfigContext.tsx │ ├── Connect.tsx │ ├── ConnectClaude.tsx │ ├── ConnectGemini.tsx │ ├── ConnectGroq.tsx │ ├── ConnectLmStudio.tsx │ ├── ConnectOllama.tsx │ ├── ConnectOpenAI.tsx │ ├── Docs.tsx │ ├── Donate.tsx │ ├── Header.tsx │ ├── Input.tsx │ ├── Message.tsx │ ├── Messages.tsx │ ├── PageContext.tsx │ ├── Params.tsx │ ├── Persona.tsx │ ├── Send.tsx │ ├── Settings.tsx │ ├── SettingsTitle.tsx │ ├── SideLlama.tsx │ ├── Themes.tsx │ ├── VersionInfo.tsx │ ├── WebSearch.tsx │ ├── constants.ts │ ├── hooks │ │ ├── useChatTitle.ts │ │ ├── useSendMessage.ts │ │ └── useUpdateModels.ts │ ├── index.html │ ├── index.tsx │ ├── messageUtils.ts │ └── network.tsx ├── state │ ├── State.ts │ ├── hooks │ │ └── useAppDispatch.ts │ ├── slices │ │ ├── content.ts │ │ └── sidePanel.ts │ └── store.ts ├── types │ ├── PortNames.ts │ ├── Position.ts │ └── SyntheticEvent.ts └── util │ ├── arrayUtil.ts │ ├── colorUtil.ts │ ├── dateUtil.ts │ ├── debounce.ts │ └── storageUtil.ts └── tsconfig.json /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["react", "simple-import-sort", "unused-imports"], 3 | "parser": "@typescript-eslint/parser", 4 | "extends": [ 5 | "react-app", 6 | "airbnb" 7 | ], 8 | "ignorePatterns": [ 9 | "*.test.(tsx|js|jsx)", 10 | "test/*", 11 | "jest.config.js", 12 | "dist" 13 | ], 14 | "parserOptions": { 15 | "project": "./tsconfig.json", 16 | "ecmaFeatures": { 17 | "jsx": true, 18 | "arrowFunctions": true, 19 | "blockBindings": true, 20 | "classes": true, 21 | "defaultParams": true, 22 | "destructuring": true, 23 | "forOf": true, 24 | "generators": false, 25 | "modules": true, 26 | "objectLiteralComputedProperties": true, 27 | "objectLiteralDuplicateProperties": false, 28 | "objectLiteralShorthandMethods": true, 29 | "objectLiteralShorthandProperties": true, 30 | "spread": true, 31 | "superInFunctions": true, 32 | "templateStrings": true, 33 | "restParams": true 34 | } 35 | }, 36 | "env": { 37 | "browser": true, 38 | "node": true, 39 | "es2021": true, 40 | "jest": true 41 | }, 42 | "rules": { 43 | "array-callback-return": [0], 44 | "arrow-parens": ["error", "as-needed"], 45 | "class-methods-use-this": "warn", 46 | "comma-dangle": ["error", "never"], 47 | "function-paren-newline": "off", 48 | "import/extensions": [0], 49 | "import/no-extraneous-dependencies": [0], 50 | "import/no-unresolved": [0], 51 | "import/prefer-default-export": [0], 52 | "max-len": [1, 180, 2, { "ignoreStrings": true }], 53 | "no-console": "off", 54 | "no-multiple-empty-lines": ["error", { "max": 1 }], 55 | "no-param-reassign": [0], 56 | "no-unused-expressions": [0], 57 | "object-curly-newline": ["error", { 58 | "ObjectExpression": { "multiline": true }, 59 | "ImportDeclaration": { "multiline": true }, 60 | }], 61 | "object-curly-spacing": ["error", "always"], 62 | "object-property-newline": ["error", { 63 | "allowAllPropertiesOnSameLine": true, 64 | "allowMultiplePropertiesPerLine": false 65 | }], 66 | "padding-line-between-statements": [ 67 | "error", 68 | { "blankLine": "always", "prev": "multiline-block-like", "next": "*" }, 69 | { "blankLine": "always", "prev": "return", "next": "*" }, 70 | { "blankLine": "always", "prev": "multiline-expression", "next": "*" }, 71 | { "blankLine": "always", "prev": "switch", "next": "*" } 72 | ], 73 | "react/forbid-prop-types": ["error", { "forbid": ["any"] }], 74 | "react/function-component-definition": [2, { "namedComponents": "arrow-function", "unnamedComponents": "arrow-function" }], 75 | "template-curly-spacing": ["error", "never"], 76 | "lines-around-comment": ["error", { 77 | "beforeBlockComment": true, 78 | "beforeLineComment": true, 79 | "allowBlockStart": true, 80 | "allowBlockEnd": true, 81 | "allowObjectStart": true, 82 | "allowObjectEnd": true, 83 | "allowArrayStart": true, 84 | "allowArrayEnd": true 85 | }], 86 | 87 | /* 88 | * Typscript rules 89 | */ 90 | "@typescript-eslint/comma-dangle": "off", 91 | "@typescript-eslint/no-use-before-define": "off", 92 | "@typescript-eslint/no-unused-vars": "warn", 93 | "@typescript-eslint/lines-between-class-members": ["error", "always", { 94 | "exceptAfterSingleLine": true, 95 | }], 96 | 97 | /* 98 | * React Plugin Rules 99 | */ 100 | "react/react-in-jsx-scope": "off", 101 | "react/prop-types": "off", 102 | "react/jsx-filename-extension": [0], 103 | "react-hooks/exhaustive-deps": "off", 104 | "react/jsx-props-no-spreading": [0], 105 | "react/no-children-prop": [0], 106 | "react/require-default-props": [2, { "ignoreFunctionalComponents": true }], 107 | "react/jsx-max-props-per-line": [1, { "maximum": 1, "when": "multiline" }], 108 | "react/button-has-type": "off", 109 | "react/jsx-sort-props": [1, { 110 | "callbacksLast": true, 111 | "shorthandFirst": true, 112 | "shorthandLast": true, 113 | "ignoreCase": true 114 | }], 115 | 116 | /* 117 | * Simple Import Sort Plugin Rules 118 | */ 119 | "simple-import-sort/imports": ["error", { 120 | "groups": [ 121 | // Side effect imports. 122 | ["^\\u0000"], 123 | // Packages. `react` related packages come first. 124 | ["^react", "^@?\\w"], 125 | // Internal aliases & Parent imports. Put `..` last. 126 | [ 127 | "^src", 128 | "^\\.\\.(?!/?$)", 129 | "^\\.\\./?$" 130 | ], 131 | // Other relative imports. Put same-folder imports and `.` last. 132 | ["^\\./(?=.*/)(?!/?$)", "^\\.(?!/?$)", "^\\./?$"], 133 | // Style imports. 134 | ["^.+\\.s?css$"] 135 | ] 136 | }], 137 | "simple-import-sort/exports": "error", 138 | 139 | /* 140 | * Unused Imports Plugin Rules 141 | */ 142 | "no-unused-vars": "off", // or "@typescript-eslint/no-unused-vars": "off", 143 | "unused-imports/no-unused-imports": "error", 144 | "unused-imports/no-unused-vars": [ 145 | "warn", 146 | { "vars": "all", "varsIgnorePattern": "^_", "args": "after-used", "argsIgnorePattern": "^_" } 147 | ] 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea 3 | dist -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "tabWidth": 2, 4 | "printWidth": 100, 5 | "singleQuote": true, 6 | "trailingComma": "all", 7 | "jsxSingleQuote": true, 8 | "bracketSpacing": true 9 | } -------------------------------------------------------------------------------- /DOCS.md: -------------------------------------------------------------------------------- 1 | # sidellama 2 | 3 | ## connections 4 | 5 | ![](/docs/connections.png) 6 | 7 | ### ollama 8 | 9 | - [install ollama](https://ollama.com/download) 10 | - or install it with `curl -fsSL https://ollama.com/install.sh | sh` 11 | 12 | ``` 13 | # select a model from https://ollama.com/library 14 | ollama pull phi3 15 | 16 | # start the daemon 17 | ollama serve 18 | ``` 19 | 20 | ### LM Studio 21 | 22 | - [install LM Studio](https://lmstudio.ai/) 23 | - download a model from the home screen, or use the search tab to pull from huggingface 24 | - go to `Local server` tab, hit `Start server`, and select your downloaded model 25 | 26 | ### groq 27 | 28 | Groq offers a wide variety of models with a generous free tier. 29 | - [Website](https://groq.com/) 30 | - [Create an API key](https://console.groq.com/keys) 31 | 32 | 33 | ## persona 34 | 35 | ![](/docs/persona.png) 36 | 37 | Create and modify your own personal assistants! 38 | 39 | Check out these collections for inspiration: 40 | - [0xeb/TheBigPromptLibrary](https://github.com/0xeb/TheBigPromptLibrary) 41 | - [sockcymbal/persona_library](https://github.com/sockcymbal/enhanced-llm-reasoning-tree-of-thoughts/blob/main/persona_library.md) 42 | - [abilzerian/LLM-Prompt-Library](https://github.com/abilzerian/LLM-Prompt-Library) 43 | - [kaushikb11/awesome-llm-agents](https://github.com/kaushikb11/awesome-llm-agents) 44 | 45 | ## page context 46 | 47 | ![](/docs/pageContext.png) 48 | 49 | Augment your conversation with the content of your (currently visited) web page. 50 | 51 | - select `text mode` to share the text content of your page 52 | - select `html mode` to share the source code of the site (resource intensive, 53 | only for development purposes) 54 | - adjust `char limit` to control the maximum amount of characters you want to share in your conversation. decrease this amount if you have limited context window. 55 | 56 | ## web search 57 | 58 | ![](/docs/webSearch.png) 59 | 60 | Basic web-augmentation for your chats. Enter your web search query, and sidellama will load up an async web search to answer your questions based on live public data. 61 | 62 | - you can choose `duckduckgo` or `brave` as your web source 63 | - adjust `char limit` to control the maximum amount of characters you want to share in your conversation. decrease this amount if you have limited context window. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Pákozdi György 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](/public/images/sidellama.png) 2 | 3 | # sidellama 4 | 5 | tiny browser-augmented chat client for open-source language models. 6 | 7 | ## installation 8 | 9 | ### install from chrome web store 10 | 11 | [Add sidellama to chrome](https://chromewebstore.google.com/detail/sidellama/lcgkoaonfgonjamccahahodpkdkfhijo) 12 | 13 | ### install manually 14 | 15 | - download the latest [release](https://github.com/gyopak/sidellama/releases) 16 | - enable chrome `Extensions > Developer mode` 17 | - load the content of the extracted zip with `Load unpacked` button 18 | 19 | ### install from source 20 | 21 | - clone the repo 22 | - run `npm i && npm start` to generate your bundle located in `dist/chrome` 23 | - enable chrome `Extensions > Developer mode` 24 | - load the content of `dist/chrome` folder with `Load unpacked` button 25 | 26 | ## docs 27 | 28 | Check out the [documentation page](/DOCS.md) 29 | 30 | ## community 31 | 32 | Check out the [discord community](https://discord.gg/2pFtRgqp) 33 | 34 | ## support 💚 35 | 36 | Check out [sidellama @ ko-fi](https://ko-fi.com/sidellama) 37 | 38 | ![](/docs/sidellama_app.png) 39 | 40 | ![](/docs/vim.png) 41 | 42 | ![](/docs/yt.png) 43 | 44 | ## Star History 45 | 46 | 47 | 48 | 49 | 50 | Star History Chart 51 | 52 | 53 | -------------------------------------------------------------------------------- /config/aliases.js: -------------------------------------------------------------------------------- 1 | const { createWebpackAliases } = require('./helpers'); 2 | 3 | /** 4 | * Export Webpack Aliases 5 | * 6 | * Tip: Some text editors will show the errors or invalid intellisense reports 7 | * based on these config aliases, make sure to update `tsconfig.json` file also 8 | * to match the `paths` we using in here for aliases in project. 9 | */ 10 | module.exports = createWebpackAliases({ 11 | 'src': 'src', 12 | 'public': 'public' 13 | }); 14 | -------------------------------------------------------------------------------- /config/helpers.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const cwd = process.cwd(); 3 | 4 | /** 5 | * Are we in development mode? 6 | */ 7 | function inDev() { 8 | return process.env.NODE_ENV === 'development'; 9 | } 10 | 11 | /** 12 | * Create config aliases 13 | */ 14 | function createWebpackAliases (aliases) { 15 | const result = {}; 16 | for (const name in aliases) { 17 | result[name] = path.join(cwd, aliases[name]); 18 | } 19 | 20 | return result; 21 | } 22 | 23 | // Export helpers 24 | module.exports = { 25 | inDev, 26 | createWebpackAliases 27 | }; 28 | -------------------------------------------------------------------------------- /config/manifest/app_info.js: -------------------------------------------------------------------------------- 1 | const packageJsonPath = require('path').join(__dirname, '../../package.json'); 2 | 3 | const packageJson = require(packageJsonPath); 4 | 5 | module.exports = { 6 | name: packageJson.name, 7 | description: packageJson.description, 8 | version: packageJson.version 9 | }; 10 | -------------------------------------------------------------------------------- /config/manifest/chrome.js: -------------------------------------------------------------------------------- 1 | const base = require('./v3'); 2 | 3 | module.exports = { 4 | ...base, 5 | 6 | // Replace with your Chrome extension public key to maintain consistent extension id 7 | // see https://stackoverflow.com/questions/21497781/how-to-change-chrome-packaged-app-id-or-why-do-we-need-key-field-in-the-manifest/21500707#21500707 8 | key: undefined 9 | }; 10 | -------------------------------------------------------------------------------- /config/manifest/permissions.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | 'activeTab', 3 | 'storage', 4 | 'sidePanel', 5 | 'scripting', 6 | 'tabs', 7 | 'declarativeNetRequest' 8 | ]; 9 | -------------------------------------------------------------------------------- /config/manifest/v3.js: -------------------------------------------------------------------------------- 1 | const permissions = require('./permissions'); 2 | const { name, description, version } = require('./app_info'); 3 | 4 | module.exports = { 5 | version, 6 | manifest_version: 3, 7 | name, 8 | description, 9 | permissions, 10 | minimum_chrome_version: '114', 11 | action: { default_title: 'Click to open panel' }, 12 | side_panel: { default_path: 'assets/sidePanel.html' }, 13 | icons: { 128: 'assets/images/sidellama.png' }, 14 | background: { service_worker: 'background.js' }, 15 | web_accessible_resources: [{ 16 | resources: ['assets/**', 'content.js.map'], 17 | matches: [''] 18 | }], 19 | "host_permissions": [""], 20 | content_security_policy: { 21 | extension_pages: ` 22 | default-src 'self' 'unsafe-eval' http://localhost:* http://127.0.0.1:* https://api.groq.com https://html.duckduckgo.com https://generativelanguage.googleapis.com https://search.brave.com https://api.openai.com; 23 | script-src 'self'; 24 | style-src 'self' 'unsafe-inline' 25 | https://fonts.googleapis.com 26 | https://fonts.gstatic.com; 27 | font-src 'self' 28 | https://fonts.googleapis.com 29 | https://fonts.gstatic.com; 30 | img-src 'unsafe-inline' 'self' data: https://upload.wikimedia.org; 31 | ` 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /config/plugins.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 3 | 4 | module.exports = [ 5 | new webpack.DefinePlugin(Object.keys(process.env).reduce((accum, key) => { 6 | if (key.startsWith('REACT_APP_') || key.startsWith('NODE_')) { 7 | return { ...accum, [`process.env.${key}`]: JSON.stringify(process.env[key]) }; 8 | } 9 | 10 | return accum; 11 | }, {})), 12 | new MiniCssExtractPlugin({ 13 | filename: '[name].[chunkhash].css', 14 | chunkFilename: '[name].[chunkhash].chunk.css' 15 | }) 16 | ]; -------------------------------------------------------------------------------- /config/resolve.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gyopak/sidellama/247ad9c7970d27cc7afa396e51a6020af07b90b2/config/resolve.js -------------------------------------------------------------------------------- /config/rules.js: -------------------------------------------------------------------------------- 1 | const { inDev } = require('./helpers'); 2 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 3 | 4 | module.exports = [ 5 | { 6 | test: /\.tsx?$/, 7 | use: { 8 | loader: 'ts-loader', 9 | options: { transpileOnly: true } 10 | } 11 | }, 12 | { 13 | test: /\.css$/, 14 | use: [ 15 | { loader: inDev() ? 'style-loader' : MiniCssExtractPlugin.loader }, 16 | { loader: 'css-loader' } 17 | ] 18 | }, 19 | { 20 | test: /\.s[ac]ss$/i, 21 | use: [ 22 | { loader: inDev() ? 'style-loader' : MiniCssExtractPlugin.loader }, 23 | { loader: 'css-loader' }, 24 | { loader: 'sass-loader' } 25 | ] 26 | }, 27 | { 28 | test: /\.less$/, 29 | use: [ 30 | { loader: inDev() ? 'style-loader' : MiniCssExtractPlugin.loader }, 31 | { loader: 'css-loader' }, 32 | { loader: 'less-loader' } 33 | ] 34 | }, 35 | { 36 | test: /\.(gif|jpe?g|tiff|png|webp|bmp|svg|eot|ttf|woff|woff2)$/i, 37 | use: [ 38 | { 39 | loader: 'file-loader', 40 | options: { 41 | name: '[name].[ext]', 42 | outputPath: 'assets' 43 | } 44 | } 45 | ] 46 | } 47 | ]; 48 | -------------------------------------------------------------------------------- /config/webpack.config.js: -------------------------------------------------------------------------------- 1 | const CopyPlugin = require('copy-webpack-plugin'); 2 | const GenerateJsonFromJsPlugin = require('generate-json-from-js-webpack-plugin'); 3 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | const dotenv = require('dotenv'); 5 | const { join } = require('path'); 6 | const { inDev } = require("./helpers"); 7 | 8 | dotenv.config(); 9 | 10 | const browsers = [ 11 | 'chrome' 12 | ]; 13 | 14 | const Root = join(__dirname, '..'); 15 | const Source = join(Root, 'src'); 16 | const Dist = join(Root, 'dist'); 17 | 18 | const Public = join(Root, 'public'); 19 | const Background = join(Source, 'background'); 20 | const Content = join(Source, 'content'); 21 | const SidePanel = join(Source, 'sidePanel'); 22 | const Lib = join(Source, 'lib'); 23 | const Options = join(Source, 'options'); 24 | 25 | const config = { 26 | mode: process.env.NODE_ENV, 27 | target: 'web', 28 | devtool: inDev() ? 'source-map' : undefined, 29 | entry: { 30 | background: join(Background, 'index.ts'), 31 | content: join(Content, 'index.tsx'), 32 | app: join(SidePanel, 'index.tsx') 33 | }, 34 | module: { rules: require('./rules') }, 35 | resolve: { 36 | extensions: ['.js', '.ts', '.jsx', '.tsx', '.css', '.scss'], 37 | alias: { 38 | lib: Lib, 39 | background: Background, 40 | sidePanel: SidePanel, 41 | content: Content, 42 | assets: Public, 43 | options: Options, 44 | ...require('./aliases') 45 | } 46 | }, 47 | plugins: [ 48 | ...require('./plugins'), 49 | new HtmlWebpackPlugin( 50 | { 51 | inject: 'body', 52 | template: join(SidePanel, 'index.html'), 53 | filename: 'assets/sidePanel.html', 54 | chunks: ['app'] 55 | }), 56 | ...browsers.map(browser => new GenerateJsonFromJsPlugin({ 57 | path: join(__dirname, 'manifest', `${browser}.js`), 58 | filename: 'manifest.json', 59 | options: { 60 | replacer: (key, value) => { 61 | switch (key) { 62 | case 'extension_pages': 63 | return value.replace(/\s+/g, ' '); 64 | default: 65 | return value; 66 | } 67 | } 68 | } 69 | })), 70 | new CopyPlugin({ 71 | patterns: [ 72 | { 73 | from: Public, 74 | to: 'assets' 75 | } 76 | ] 77 | }) 78 | ], 79 | optimization: { 80 | splitChunks: { 81 | cacheGroups: { 82 | vendor: { 83 | test: /[\\/]node_modules[\\/].*[\\/]/, 84 | name: 'assets/vendor', 85 | chunks: chunk => chunk.name !== "background" 86 | } 87 | } 88 | } 89 | } 90 | }; 91 | 92 | const buildConfig = browser => ({ 93 | ...config, 94 | name: browser, 95 | output: { 96 | path: join(Dist, browser), 97 | filename: '[name].js', 98 | publicPath: process.env.EXTENSION_PUBLIC_PATH 99 | } 100 | }); 101 | 102 | module.exports = buildConfig(process.env.BROWSER || 'chrome'); 103 | 104 | -------------------------------------------------------------------------------- /docs/connections.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gyopak/sidellama/247ad9c7970d27cc7afa396e51a6020af07b90b2/docs/connections.png -------------------------------------------------------------------------------- /docs/pageContext.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gyopak/sidellama/247ad9c7970d27cc7afa396e51a6020af07b90b2/docs/pageContext.png -------------------------------------------------------------------------------- /docs/params.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gyopak/sidellama/247ad9c7970d27cc7afa396e51a6020af07b90b2/docs/params.png -------------------------------------------------------------------------------- /docs/persona.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gyopak/sidellama/247ad9c7970d27cc7afa396e51a6020af07b90b2/docs/persona.png -------------------------------------------------------------------------------- /docs/sidellama_app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gyopak/sidellama/247ad9c7970d27cc7afa396e51a6020af07b90b2/docs/sidellama_app.png -------------------------------------------------------------------------------- /docs/vim.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gyopak/sidellama/247ad9c7970d27cc7afa396e51a6020af07b90b2/docs/vim.png -------------------------------------------------------------------------------- /docs/webSearch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gyopak/sidellama/247ad9c7970d27cc7afa396e51a6020af07b90b2/docs/webSearch.png -------------------------------------------------------------------------------- /docs/yt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gyopak/sidellama/247ad9c7970d27cc7afa396e51a6020af07b90b2/docs/yt.png -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import globals from "globals"; 2 | import pluginJs from "@eslint/js"; 3 | import tseslint from "typescript-eslint"; 4 | import pluginReactConfig from "eslint-plugin-react/configs/recommended.js"; 5 | 6 | 7 | export default [ 8 | {languageOptions: { globals: globals.browser }}, 9 | pluginJs.configs.recommended, 10 | ...tseslint.configs.recommended, 11 | pluginReactConfig, 12 | ]; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sidellama", 3 | "version": "0.0.1", 4 | "description": "sidellama", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "npm run clear && webpack -w --config config/webpack.config.js", 8 | "clear": "rimraf dist && rimraf node_modules/.cache" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com.git" 13 | }, 14 | "keywords": [ 15 | "sidepanel", 16 | "side", 17 | "panel", 18 | "chrome", 19 | "extension" 20 | ], 21 | "author": "gyopak", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "" 25 | }, 26 | "homepage": "", 27 | "dependencies": { 28 | "@chakra-ui/icons": "^2.1.1", 29 | "@chakra-ui/react": "^2.7.1", 30 | "@emotion/react": "^11.11.4", 31 | "@eslint/js": "^9.2.0", 32 | "@reduxjs/toolkit": "^1.9.5", 33 | "@types/chrome": "^0.0.238", 34 | "@types/react": "^18.2.14", 35 | "@types/react-dom": "^18.2.6", 36 | "@types/redux-logger": "^3.0.9", 37 | "@typescript-eslint/eslint-plugin": "^5.60.0", 38 | "@typescript-eslint/parser": "^5.60.0", 39 | "babel-preset-react-app": "^10.0.1", 40 | "copy-webpack-plugin": "^11.0.0", 41 | "dom-to-image": "^2.6.0", 42 | "dotenv": "^16.3.1", 43 | "eslint-config-prettier": "^8.8.0", 44 | "eslint-config-react-app": "^7.0.1", 45 | "eslint-import-resolver-typescript": "^3.5.5", 46 | "eslint-import-resolver-webpack": "^0.13.2", 47 | "eslint-plugin-prettier": "^4.2.1", 48 | "eslint-plugin-simple-import-sort": "^10.0.0", 49 | "eslint-plugin-unused-imports": "^2.0.0", 50 | "fetch-event-stream": "^0.1.5", 51 | "framer-motion": "^11.1.7", 52 | "generate-json-from-js-webpack-plugin": "^0.1.1", 53 | "globals": "^15.1.0", 54 | "html-to-image": "^1.11.11", 55 | "html-webpack-plugin": "^5.5.3", 56 | "localforage": "^1.10.0", 57 | "mini-css-extract-plugin": "^2.7.6", 58 | "react": "^18.2.0", 59 | "react-dom": "^18.2.0", 60 | "react-hot-toast": "^2.4.1", 61 | "react-markdown": "^9.0.1", 62 | "react-redux": "^8.1.1", 63 | "react-textarea-autosize": "^8.5.3", 64 | "redux-logger": "^3.0.6", 65 | "redux-thunk": "^2.4.2", 66 | "ts-loader": "^9.4.3", 67 | "typescript": "^5.1.3", 68 | "typescript-eslint": "^7.8.0", 69 | "webext-redux": "^2.1.9", 70 | "webpack": "^5.91.0", 71 | "webpack-cli": "^5.1.4" 72 | }, 73 | "devDependencies": { 74 | "@types/dom-to-image": "^2.6.7", 75 | "eslint": "^8.57.0", 76 | "eslint-config-airbnb": "^19.0.4", 77 | "eslint-plugin-import": "^2.29.1", 78 | "eslint-plugin-jsx-a11y": "^6.8.0", 79 | "eslint-plugin-react": "^7.34.1", 80 | "eslint-plugin-react-hooks": "^4.6.2", 81 | "rimraf": "^6.0.1" 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /public/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gyopak/sidellama/247ad9c7970d27cc7afa396e51a6020af07b90b2/public/.DS_Store -------------------------------------------------------------------------------- /public/images/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gyopak/sidellama/247ad9c7970d27cc7afa396e51a6020af07b90b2/public/images/.DS_Store -------------------------------------------------------------------------------- /public/images/android-chrome-384x384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gyopak/sidellama/247ad9c7970d27cc7afa396e51a6020af07b90b2/public/images/android-chrome-384x384.png -------------------------------------------------------------------------------- /public/images/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gyopak/sidellama/247ad9c7970d27cc7afa396e51a6020af07b90b2/public/images/apple-touch-icon.png -------------------------------------------------------------------------------- /public/images/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #da532c 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /public/images/chrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gyopak/sidellama/247ad9c7970d27cc7afa396e51a6020af07b90b2/public/images/chrome.png -------------------------------------------------------------------------------- /public/images/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gyopak/sidellama/247ad9c7970d27cc7afa396e51a6020af07b90b2/public/images/favicon-16x16.png -------------------------------------------------------------------------------- /public/images/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gyopak/sidellama/247ad9c7970d27cc7afa396e51a6020af07b90b2/public/images/favicon-32x32.png -------------------------------------------------------------------------------- /public/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gyopak/sidellama/247ad9c7970d27cc7afa396e51a6020af07b90b2/public/images/favicon.ico -------------------------------------------------------------------------------- /public/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gyopak/sidellama/247ad9c7970d27cc7afa396e51a6020af07b90b2/public/images/favicon.png -------------------------------------------------------------------------------- /public/images/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gyopak/sidellama/247ad9c7970d27cc7afa396e51a6020af07b90b2/public/images/mstile-150x150.png -------------------------------------------------------------------------------- /public/images/nose.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gyopak/sidellama/247ad9c7970d27cc7afa396e51a6020af07b90b2/public/images/nose.png -------------------------------------------------------------------------------- /public/images/sidellama.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gyopak/sidellama/247ad9c7970d27cc7afa396e51a6020af07b90b2/public/images/sidellama.png -------------------------------------------------------------------------------- /public/images/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "short_name": "", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-384x384.png", 12 | "sizes": "384x384", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#aec5dd", 17 | "background_color": "#aec5dd", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /src/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gyopak/sidellama/247ad9c7970d27cc7afa396e51a6020af07b90b2/src/.DS_Store -------------------------------------------------------------------------------- /src/Globals.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.module.css'; 2 | declare module '*.module.scss'; 3 | -------------------------------------------------------------------------------- /src/background/index.ts: -------------------------------------------------------------------------------- 1 | import { getCurrentTab, injectContentScript } from 'src/background/util'; 2 | import buildStoreWithDefaults from 'src/state/store'; 3 | import storage from 'src/util/storageUtil'; 4 | import PortNames from '../types/PortNames'; 5 | 6 | buildStoreWithDefaults({ portName: PortNames.ContentPort }); 7 | 8 | storage.setItem('panelOpen', false); 9 | 10 | chrome.sidePanel 11 | .setPanelBehavior({ openPanelOnActionClick: true }) 12 | .catch((error: any) => console.error(error)); 13 | 14 | chrome.tabs.onUpdated 15 | .addListener(async (tabId, changeInfo, tab) => { 16 | if (!(tab.id && changeInfo.status === 'complete')) return; 17 | 18 | console.log('tab connected: ', tab.url, changeInfo); 19 | 20 | if (await storage.getItem('panelOpen')) { 21 | console.log('panel open'); 22 | injectContentScript(tabId); 23 | } 24 | }); 25 | 26 | chrome.runtime.onConnect.addListener(port => { 27 | port.onMessage.addListener(async msg => { 28 | if (port.name === PortNames.SidePanelPort) { 29 | if (msg.type === 'init') { 30 | console.log('panel opened'); 31 | 32 | await storage.setItem('panelOpen', true); 33 | 34 | port.onDisconnect.addListener(async () => { 35 | await storage.setItem('panelOpen', false); 36 | console.log('panel closed'); 37 | console.log('port disconnected: ', port.name); 38 | }); 39 | 40 | const tab = await getCurrentTab(); 41 | 42 | if (!tab?.id) { 43 | console.error("Couldn't get current tab"); 44 | return; 45 | } 46 | 47 | injectContentScript(tab.id); 48 | 49 | port.postMessage({ 50 | type: 'handle-init', 51 | message: 'panel open' 52 | }); 53 | } 54 | } 55 | }); 56 | }); 57 | 58 | export {}; 59 | -------------------------------------------------------------------------------- /src/background/util.ts: -------------------------------------------------------------------------------- 1 | export async function getCurrentTab() { 2 | const queryOptions = { active: true, lastFocusedWindow: true }; 3 | 4 | // `tab` will either be a `tabs.Tab` instance or `undefined`. 5 | const [tab] = await chrome.tabs.query(queryOptions); 6 | return tab; 7 | } 8 | 9 | export async function injectContentScript(tabId: number) { 10 | console.log('injecting content script'); 11 | 12 | chrome.scripting.executeScript({ 13 | // @ts-ignore 14 | target: { tabId }, 15 | files: [ 16 | 'assets/vendor.js', 17 | 'content.js' 18 | ] 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /src/content/contracts/ContentProvider.ts: -------------------------------------------------------------------------------- 1 | interface ContentProvider { 2 | register: () => Promise; 3 | } 4 | 5 | export default ContentProvider; 6 | -------------------------------------------------------------------------------- /src/content/controllers/CursorController.tsx: -------------------------------------------------------------------------------- 1 | import { Store } from 'webext-redux'; 2 | 3 | import { setCursorPosition } from "src/state/slices/content"; 4 | import { State } from 'src/state/State'; 5 | import { createStoreProxy } from 'src/state/store'; 6 | import PortNames from "src/types/PortNames"; 7 | import ContentProvider from '../contracts/ContentProvider'; 8 | 9 | class CursorController implements ContentProvider { 10 | app: HTMLElement | null = null; 11 | store: Store = createStoreProxy(PortNames.ContentPort); 12 | async register() { 13 | console.log('==================================='); 14 | console.log('registering cursor controller'); 15 | console.log('==================================='); 16 | 17 | this.store.ready().then(() => { 18 | document.addEventListener('mousemove', this.handleMouseMove); 19 | }); 20 | 21 | return this; 22 | } 23 | 24 | handleMouseMove = (e:MouseEvent) => { 25 | const cursorPosition = { x: e.clientX, y: e.clientY }; 26 | this.store.dispatch(setCursorPosition(cursorPosition)); 27 | }; 28 | } 29 | 30 | export default CursorController; 31 | -------------------------------------------------------------------------------- /src/content/index.scss: -------------------------------------------------------------------------------- 1 | #app-root { 2 | width: 400px; 3 | height: 75px; 4 | padding: 2px; 5 | 6 | h1 { 7 | text-align: center; 8 | } 9 | } -------------------------------------------------------------------------------- /src/content/index.tsx: -------------------------------------------------------------------------------- 1 | import { contentLoaded } from 'src/state/slices/content'; 2 | import { createStoreProxy } from 'src/state/store'; 3 | import PortNames from '../types/PortNames'; 4 | 5 | import CursorController from './controllers/CursorController'; 6 | 7 | const initialize = async () => { 8 | const store = createStoreProxy(PortNames.ContentPort); 9 | 10 | const controllers = [ 11 | new CursorController() 12 | ]; 13 | 14 | await store.ready(); 15 | await Promise.all(controllers.map(controller => controller.register())); 16 | store.dispatch(contentLoaded()); 17 | }; 18 | 19 | initialize(); 20 | 21 | export {}; 22 | -------------------------------------------------------------------------------- /src/content/messaging.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
HighlightController
HighlightController
highlight event
highlight event
ImageController
ImageController
AppController
AppController
Background
Background
CardCreatorHandler
CardCreatorHandler
render card creator
render card creator
Text is not SVG - cannot display
-------------------------------------------------------------------------------- /src/sidePanel/AddToChat.tsx: -------------------------------------------------------------------------------- 1 | import { AttachmentIcon } from '@chakra-ui/icons'; 2 | import { Button, Menu, MenuButton, MenuItem, MenuList } from '@chakra-ui/react'; 3 | 4 | import { useConfig } from './ConfigContext'; 5 | 6 | export const AddToChat = () => { 7 | const { config, updateConfig } = useConfig(); 8 | return ( 9 | 10 | : undefined} 24 | size="md" 25 | variant="outlined" 26 | color="var(--text)" 27 | zIndex={2} 28 | > 29 | {config?.chatMode} 30 | 31 | 43 | updateConfig({ chatMode: undefined })} 51 | > 52 | chat 53 | 54 | updateConfig({ chatMode: 'page' })} 62 | > 63 | page chat 64 | 65 | updateConfig({ chatMode: 'web' })} 72 | > 73 | web chat 74 | 75 | 76 | 77 | ); 78 | }; 79 | -------------------------------------------------------------------------------- /src/sidePanel/Automation.tsx: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react'; 3 | import { AccordionButton, AccordionItem, AccordionPanel, Button, Grid, Text } from '@chakra-ui/react'; 4 | 5 | import { useConfig } from './ConfigContext'; 6 | import { SettingTitle } from './SettingsTitle'; 7 | 8 | export const Automation = () => { 9 | const { config } = useConfig(); 10 | return ( 11 | 17 | 18 | 24 | {false 25 | && ( 26 | 44 | )} 45 | 46 | )} 47 | /> 48 | 49 | 50 | coming soon! 51 | 52 | 53 | ); 54 | }; 55 | -------------------------------------------------------------------------------- /src/sidePanel/Background.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Image } from '@chakra-ui/react'; 2 | 3 | export const Background = () => ( 4 | 11 | 15 | 16 | ); 17 | -------------------------------------------------------------------------------- /src/sidePanel/ChatHistory.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { useEffect, useState } from 'react'; 3 | import { DeleteIcon } from '@chakra-ui/icons'; 4 | import { Box, IconButton, Text } from '@chakra-ui/react'; 5 | import { motion } from 'framer-motion'; 6 | import localforage from 'localforage'; 7 | 8 | const dateToString = date => new Date(date).toLocaleDateString('sv-SE'); 9 | 10 | export type ChatMessage = { 11 | last_updated: number; 12 | id: string; 13 | messages: string[]; 14 | title: string; 15 | } 16 | 17 | export const ChatHistory = ({ loadChat }) => { 18 | const [messages, setMessages] = useState([]); 19 | const [hoverId, setHoverId] = useState(''); 20 | const [removeId, setRemoveId] = useState(''); 21 | 22 | const messagesWithDates = messages.map(message => ({ 23 | ...message, 24 | date: dateToString(message.last_updated) 25 | })); 26 | 27 | const uniqueDates = Array.from( 28 | new Set(messagesWithDates.map(message => message.date)) 29 | ); 30 | 31 | useEffect(() => { 32 | localforage.keys().then(async keys => { 33 | const storedMessages = await Promise.all(keys.map(key => localforage.getItem(key))) as ChatMessage[]; 34 | setMessages(storedMessages.sort((a, b) => b.last_updated - a.last_updated)); 35 | }); 36 | }, []); 37 | 38 | const deleteMessage = (id: string) => { 39 | localforage.removeItem(id).then(async () => { 40 | localforage.keys().then(async keys => { 41 | const storedMessages = await Promise.all(keys.map(key => localforage.getItem(key))) as ChatMessage[]; 42 | setMessages(storedMessages.sort((a, b) => b.last_updated - a.last_updated)); 43 | }); 44 | }); 45 | }; 46 | 47 | return ( 48 | 57 | {uniqueDates.map(date => ( 58 | 59 | 71 | {date === dateToString(new Date()) ? 'today' : date} 72 | 73 | {messagesWithDates.filter(m => m.date === date).map(message => ( 74 | setHoverId(message.id)} 78 | onMouseLeave={() => setHoverId('')} 79 | > 80 | 88 | {(`0${(new Date(message.last_updated)).getHours()}`).slice(-2)} 89 | : 90 | {(`0${(new Date(message.last_updated)).getMinutes()}`).slice(-2)} 91 | 92 | 93 | loadChat(message)} 114 | > 115 | {message.title} 116 | 117 | { 118 | message.id === hoverId && ( 119 | } 124 | variant="outlined" 125 | whileHover={{ rotate: '15deg', cursor: 'pointer' }} 126 | onClick={() => deleteMessage(message.id)} 127 | onMouseEnter={() => setRemoveId(message.id)} 128 | onMouseLeave={() => setRemoveId('')} 129 | /> 130 | ) 131 | } 132 | 133 | ))} 134 | 135 | ))} 136 | 137 | 138 | ); 139 | }; 140 | -------------------------------------------------------------------------------- /src/sidePanel/ConfigContext.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, useContext, useEffect, useState } from 'react'; 2 | 3 | export const ConfigContext = createContext({}); 4 | 5 | export const personas = { 6 | sidellama: `You are a meticulous academic proficient in analyzing research papers: 7 | - Concisely restate the core problem statement(s); 8 | - Summarize the central arguments and key findings, list the data and facts particularly; 9 | - Extract the primary takeaways and their implications; 10 | - Formulate 3 insightful questions stemming from the paper, and provide well-reasoned answers based on the text. 11 | Avoid speculative answers not supported by the provided content.`, 12 | }; 13 | 14 | const defaultConfig = { 15 | personas, 16 | generateTitle: true, 17 | backgroundImage: true, 18 | persona: 'sidellama', 19 | webMode: 'brave', 20 | webLimit: 10, 21 | contextLimit: 10, 22 | }; 23 | 24 | export const ConfigProvider = ({ children }: any) => { 25 | const initialConfig = JSON.parse(localStorage.getItem('config') || JSON.stringify(defaultConfig)); 26 | 27 | const [config, setConfig] = useState(initialConfig); 28 | 29 | useEffect(() => { 30 | localStorage.setItem('config', JSON.stringify(config)); 31 | }, [config]); 32 | 33 | useEffect(() => { 34 | if (config?.fontSize) { 35 | document.documentElement.style.setProperty('font-size', `${config?.fontSize}px`); 36 | } 37 | }, [config?.fontSize]); 38 | 39 | const updateConfig = (newConfig: any) => { 40 | setConfig({ ...config, ...newConfig }); 41 | }; 42 | 43 | return ( 44 | 45 | {children} 46 | 47 | ); 48 | }; 49 | 50 | export const useConfig = () => useContext(ConfigContext) as { config: any, updateConfig: (v: any) => void }; 51 | -------------------------------------------------------------------------------- /src/sidePanel/Connect.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ExternalLinkIcon } from '@chakra-ui/icons'; 3 | import { AccordionButton, AccordionItem, AccordionPanel, Link, Text } from '@chakra-ui/react'; 4 | import { ConnectGroq } from './ConnectGroq'; 5 | import { ConnectOllama } from './ConnectOllama'; 6 | import { SettingTitle } from './SettingsTitle'; 7 | import { ConnectClaude } from './ConnectClaude'; 8 | import { ConnectLmStudio } from './ConnectLmStudio'; 9 | import { ConnectOpenAI } from './ConnectOpenAI'; 10 | import { ConnectGemini } from './ConnectGemini'; 11 | 12 | type ConnectionProps = { 13 | title: string; 14 | Component: React.FC; 15 | link?: string; 16 | }; 17 | 18 | const borderStyle: string = '2px solid var(--text)'; 19 | const textStyle = { 20 | fontWeight: 800, 21 | paddingTop: 2, 22 | paddingBottom: 2, 23 | paddingLeft: 4, 24 | fontSize: 'lg', 25 | color: 'var(--text)' 26 | }; 27 | 28 | const ConnectionSection: React.FC = ({ title, Component, link }) => ( 29 | <> 30 | 31 | {title} 32 | {' '} 33 | {link && ( 34 | 35 | api keys 36 | {' '} 37 | 38 | 39 | )} 40 | 41 | 42 | 43 | ); 44 | 45 | export const Connect: React.FC = () => ( 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | ); 59 | -------------------------------------------------------------------------------- /src/sidePanel/ConnectClaude.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useState } from 'react'; 3 | import { ViewIcon, ViewOffIcon } from '@chakra-ui/icons'; 4 | import { Box, Button, IconButton, Input } from '@chakra-ui/react'; 5 | 6 | import { useConfig } from './ConfigContext'; 7 | import { GROQ_URL } from './constants'; 8 | 9 | export const ConnectClaude = () => { 10 | const { config } = useConfig(); 11 | const [apiKey, setApiKey] = useState(config?.groqApiKey); 12 | const [visibleApiKeys, setVisibleApiKeys] = useState(false); 13 | 14 | const onConnect = () => { 15 | return; 16 | }; 17 | 18 | const disabled = config?.groqApiKey === apiKey; 19 | const isConnected = config?.groqConnected && config?.groqApiKey === apiKey && false; 20 | 21 | return ( 22 | 23 | setApiKey(e.target.value)} 49 | /> 50 | {!isConnected && ( 51 | 66 | )} 67 | {isConnected && ( 68 | : } 80 | size="sm" 81 | variant="solid" 82 | onClick={() => setVisibleApiKeys(!visibleApiKeys)} 83 | /> 84 | )} 85 | 86 | 87 | ); 88 | }; 89 | -------------------------------------------------------------------------------- /src/sidePanel/ConnectGemini.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Box, Button, Input } from '@chakra-ui/react'; 3 | import { useConfig } from './ConfigContext'; 4 | import toast from 'react-hot-toast'; 5 | import { GEMINI_URL } from './constants'; 6 | 7 | export const ConnectGemini = () => { 8 | const { config, updateConfig } = useConfig(); 9 | const [apiKey, setApiKey] = useState(config?.geminiApiKey); 10 | const [visibleApiKeys, setVisibleApiKeys] = useState(false); 11 | const onConnect = () => { 12 | fetch(GEMINI_URL, { headers: { Authorization: `Bearer ${apiKey}` } }) 13 | .then(res => { 14 | if (!res.ok) { 15 | throw new Error(`HTTP error! status: ${res.status}`); 16 | } 17 | return res.json(); 18 | }) 19 | .then(data => { 20 | if (data?.error) { 21 | toast.error(`${data?.error?.message}`); 22 | updateConfig({ geminiError: data?.error?.message, geminiConnected: false }); 23 | } else { 24 | toast.success('connected to Gemini'); 25 | updateConfig({ geminiApiKey: apiKey, geminiConnected: true, geminiError: undefined }); 26 | } 27 | }) 28 | .catch(err => { 29 | toast.error(err.message); 30 | }); 31 | }; 32 | 33 | const disabled = config?.geminiApiKey === apiKey; 34 | const isConnected = config?.geminiConnected && config?.geminiApiKey === apiKey; 35 | 36 | return ( 37 | 38 | setApiKey(e.target.value)} 56 | /> 57 | 68 | {!isConnected && ( 69 | 81 | )} 82 | 83 | ); 84 | }; -------------------------------------------------------------------------------- /src/sidePanel/ConnectGroq.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useState } from 'react'; 3 | import { ViewIcon, ViewOffIcon } from '@chakra-ui/icons'; 4 | import { Box, Button, IconButton, Input } from '@chakra-ui/react'; 5 | 6 | import { useConfig } from './ConfigContext'; 7 | import { GROQ_URL } from './constants'; 8 | import toast from 'react-hot-toast'; 9 | 10 | export const ConnectGroq = () => { 11 | const { config, updateConfig } = useConfig(); 12 | const [apiKey, setApiKey] = useState(config?.groqApiKey); 13 | const [visibleApiKeys, setVisibleApiKeys] = useState(false); 14 | const onConnect = () => { 15 | fetch(GROQ_URL, { headers: { Authorization: `Bearer ${apiKey}` } }) 16 | .then(res => res.json()) 17 | .then(data => { 18 | if (data?.error) { 19 | toast.error(`${data?.error?.message}`) 20 | 21 | updateConfig({ groqError: data?.error?.message, groqConnected: false }); 22 | } else { 23 | toast.success('connected to groq'); 24 | 25 | updateConfig({ 26 | groqApiKey: apiKey, 27 | groqConnected: true, 28 | groqError: undefined 29 | }); 30 | } 31 | }) 32 | .catch(err => { 33 | toast.error(err.message); 34 | }); 35 | }; 36 | 37 | const disabled = config?.groqApiKey === apiKey; 38 | const isConnected = config?.groqConnected && config?.groqApiKey === apiKey; 39 | 40 | return ( 41 | 42 | setApiKey(e.target.value)} 67 | /> 68 | {!isConnected && ( 69 | 84 | )} 85 | {isConnected && ( 86 | : } 98 | size="sm" 99 | variant="solid" 100 | onClick={() => setVisibleApiKeys(!visibleApiKeys)} 101 | /> 102 | )} 103 | 104 | 105 | ); 106 | }; 107 | -------------------------------------------------------------------------------- /src/sidePanel/ConnectLmStudio.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useState } from 'react'; 3 | import { CheckIcon } from '@chakra-ui/icons'; 4 | import { Box, Button, IconButton, Input } from '@chakra-ui/react'; 5 | 6 | import { useConfig } from './ConfigContext'; 7 | import toast from 'react-hot-toast'; 8 | 9 | export const ConnectLmStudio = () => { 10 | const { config, updateConfig } = useConfig(); 11 | const [url, setUrl] = useState(config?.lmStudioUrl || 'http://localhost:1234'); 12 | const onConnect = () => { 13 | fetch(`${url}/v1/models`) 14 | .then(res => res.json()) 15 | .then(data => { 16 | if (data?.error) { 17 | updateConfig({ 18 | lmStudioError: data?.error?.message, 19 | lmStudioConnected: false 20 | }); 21 | toast.error(data.error.message); 22 | } else { 23 | updateConfig({ 24 | lmStudioConnected: true, 25 | lmStudioUrl: url, 26 | lmStudioError: undefined 27 | }); 28 | toast.success("connected to LM Studio") 29 | } 30 | }) 31 | .catch(err => { 32 | toast.error(err.message); 33 | 34 | updateConfig({ 35 | lmStudioError: err, 36 | lmStudioConnected: false 37 | }); 38 | }); 39 | }; 40 | 41 | const isConnected = config?.lmStudioConnected && config?.lmStudioUrl === url; 42 | return ( 43 | 44 | setUrl(e.target.value)} 66 | /> 67 | {!isConnected && ( 68 | 82 | )} 83 | {isConnected && ( 84 | } 96 | size="sm" 97 | variant="solid" 98 | onClick={() => updateConfig({ visibleApiKeys: !config?.visibleApiKeys })} 99 | /> 100 | )} 101 | 102 | ); 103 | }; 104 | -------------------------------------------------------------------------------- /src/sidePanel/ConnectOllama.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useState } from 'react'; 3 | import { CheckIcon } from '@chakra-ui/icons'; 4 | import { Box, Button, IconButton, Input } from '@chakra-ui/react'; 5 | 6 | import { useConfig } from './ConfigContext'; 7 | import toast from 'react-hot-toast'; 8 | 9 | export const ConnectOllama = () => { 10 | const { config, updateConfig } = useConfig(); 11 | const [url, setUrl] = useState(config?.ollamaUrl || 'http://localhost:11434'); 12 | const onConnect = () => { 13 | fetch(`${url}/api/tags`) 14 | .then(res => res.json()) 15 | .then(data => { 16 | if (data?.error) { 17 | updateConfig({ 18 | ollamaError: data?.error?.message, 19 | ollamaConnected: false 20 | }); 21 | toast.error(data.error); 22 | } else { 23 | updateConfig({ 24 | ollamaConnected: true, 25 | ollamaUrl: url, 26 | groqError: undefined 27 | }); 28 | 29 | toast.success('connected to ollama'); 30 | } 31 | }) 32 | .catch(err => { 33 | toast.error(err.message); 34 | 35 | updateConfig({ 36 | ollamaError: err, 37 | ollamaConnected: false 38 | }); 39 | }); 40 | }; 41 | 42 | const isConnected = config?.ollamaConnected && config?.ollamaUrl === url; 43 | return ( 44 | 45 | setUrl(e.target.value)} 67 | /> 68 | {!isConnected && ( 69 | 83 | )} 84 | {isConnected && ( 85 | } 97 | size="sm" 98 | variant="solid" 99 | onClick={() => updateConfig({ visibleApiKeys: !config?.visibleApiKeys })} 100 | /> 101 | )} 102 | 103 | ); 104 | }; 105 | -------------------------------------------------------------------------------- /src/sidePanel/ConnectOpenAI.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useState } from 'react'; 3 | import { ViewIcon, ViewOffIcon } from '@chakra-ui/icons'; 4 | import { Box, Button, IconButton, Input } from '@chakra-ui/react'; 5 | 6 | import { useConfig } from './ConfigContext'; 7 | import { OPENAI_URL } from './constants'; 8 | import toast from 'react-hot-toast'; 9 | 10 | export const ConnectOpenAI = () => { 11 | const { config, updateConfig } = useConfig(); 12 | const [apiKey, setApiKey] = useState(config?.openAiApiKey); 13 | const [visibleApiKeys, setVisibleApiKeys] = useState(false); 14 | const onConnect = () => { 15 | fetch(OPENAI_URL, { headers: { Authorization: `Bearer ${apiKey}` } }) 16 | .then(res => res.json()) 17 | .then(data => { 18 | if (data?.error) { 19 | toast.error(`${data?.error?.message}`) 20 | 21 | updateConfig({ openAiError: data?.error?.message, openAiConnected: false }); 22 | } else { 23 | toast.success('connected to OpenAI'); 24 | 25 | updateConfig({ 26 | openAiApiKey: apiKey, 27 | openAiConnected: true, 28 | openAiError: undefined 29 | }); 30 | } 31 | }) 32 | .catch(err => { 33 | toast.error(err.message); 34 | }); 35 | }; 36 | 37 | const disabled = config?.openAiApiKey === apiKey; 38 | const isConnected = config?.openAiConnected && config?.openAiApiKey === apiKey; 39 | 40 | return ( 41 | 42 | setApiKey(e.target.value)} 67 | /> 68 | {!isConnected && ( 69 | 84 | )} 85 | {isConnected && ( 86 | : } 98 | size="sm" 99 | variant="solid" 100 | onClick={() => setVisibleApiKeys(!visibleApiKeys)} 101 | /> 102 | )} 103 | 104 | 105 | ); 106 | }; 107 | -------------------------------------------------------------------------------- /src/sidePanel/Docs.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Box } from '@chakra-ui/react'; 3 | 4 | export const Docs = () => ( 5 | window.open("https://github.com/gyopak/sidellama/blob/master/DOCS.md", "_blank")} 13 | fontSize="md" 14 | background="var(--bg)" 15 | fontStyle="bold" 16 | fontWeight={600} 17 | pb={0.5} 18 | pl={3} 19 | pr={3} 20 | pt={0.5} 21 | mr={3} 22 | > 23 | docs 24 | 25 | ); 26 | -------------------------------------------------------------------------------- /src/sidePanel/Donate.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { InfoOutlineIcon } from '@chakra-ui/icons'; 3 | import { Box } from '@chakra-ui/react'; 4 | 5 | export const Donate = () => ( 6 | window.open("https://ko-fi.com/sidellama",'_blank')} 14 | background="var(--bg)" 15 | fontStyle="bold" 16 | fontWeight={600} 17 | pb={0.5} 18 | pl={3} 19 | pr={3} 20 | pt={0.5} 21 | mr={3} 22 | cursor="pointer" 23 | > 24 | support💚 25 | 26 | ); 27 | -------------------------------------------------------------------------------- /src/sidePanel/Header.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | /* eslint-disable @typescript-eslint/no-explicit-any */ 3 | /* eslint-disable react/prop-types */ 4 | import React from 'react'; 5 | import { 6 | DeleteIcon, 7 | SettingsIcon, 8 | SmallCloseIcon 9 | } from '@chakra-ui/icons'; 10 | import { 11 | Box, 12 | Button, 13 | Drawer, 14 | DrawerBody, 15 | DrawerContent, 16 | DrawerOverlay, 17 | IconButton, 18 | Modal, 19 | ModalBody, 20 | ModalContent, 21 | ModalHeader, 22 | ModalOverlay, 23 | Select, 24 | Text, 25 | useDisclosure 26 | } from '@chakra-ui/react'; 27 | import { motion } from 'framer-motion'; 28 | 29 | import { useConfig } from './ConfigContext'; 30 | import { VersionInfo } from './VersionInfo'; 31 | import { Docs } from './Docs'; 32 | import { Support } from './Support'; 33 | import { Donate } from './Donate'; 34 | 35 | // eslint-disable-next-line react/prop-types 36 | const WelcomeModal = ({ isOpen, onClose, setSettingsMode }) => ( 37 | 38 | 39 | 40 | 👋 welcome to sidellama 👋 41 | 42 | 43 | you have no active connection 44 | 45 | 46 | 59 | 60 | 61 | 62 | 63 | ); 64 | 65 | // eslint-disable-next-line react/prop-types 66 | const Badge = ({ children }) => ( 67 | 85 | {children} 86 | 87 | ); 88 | 89 | // eslint-disable-next-line react/prop-types 90 | const DrawerHeader = ({ onClose }) => ( 91 | 92 | } 97 | ml={1} 98 | mr={1} 99 | variant="outlined" 100 | whileHover={{ cursor: 'pointer' }} 101 | onClick={onClose} 102 | /> 103 | settings 104 | 105 | ); 106 | 107 | // eslint-disable-next-line react/prop-types 108 | const DrawerSection = ({ title, children }) => ( 109 | 110 | {title} 111 | {children} 112 | 113 | ); 114 | 115 | // eslint-disable-next-line react/prop-types 116 | const DrawerLinkSection = ({ title, onClick }) => ( 117 | 118 | 126 | {title} 127 | 128 | 129 | ); 130 | 131 | // eslint-disable-next-line react/prop-types 132 | const SettingsDrawer = ({ isOpen, onClose, config, updateConfig, availableModelNames, setSettingsMode, downloadText, downloadJson, downloadImage, setHistoryMode }) => ( 133 | 134 | 135 | 136 | 137 | 138 | 139 | 158 | 159 | 160 | 191 | 192 | { setSettingsMode(true); onClose(); }} /> 193 | { setHistoryMode(true); onClose(); }} 196 | /> 197 | { onClose(); downloadText(); }} 200 | /> 201 | { onClose(); downloadJson(); }} 204 | /> 205 | { downloadImage(); onClose(); }} 208 | /> 209 | 210 | 211 | 212 | ); 213 | 214 | export const Header = ({ 215 | chatTitle = '', 216 | reset = () => {}, 217 | downloadText = () => {}, 218 | downloadJson = () => {}, 219 | downloadImage = () => {}, 220 | settingsMode = false, 221 | setSettingsMode = _s => {}, 222 | historyMode = false, 223 | setHistoryMode = _s => {} 224 | }) => { 225 | const { config, updateConfig } = useConfig(); 226 | const { isOpen, onOpen, onClose } = useDisclosure(); 227 | const availableModelNames = config?.models?.map(({ id }) => id); 228 | 229 | const visibleTitle = chatTitle && !settingsMode && !historyMode; 230 | 231 | return ( 232 | 239 | 246 | {(!config?.models || config?.models.length === 0) && !settingsMode && ( 247 | {}} /> 248 | )} 249 | 250 | 251 | {!settingsMode && !historyMode ? ( 252 | } 257 | ml={1} 258 | mr={1} 259 | variant="outlined" 260 | whileHover={{ rotate: '90deg', cursor: 'pointer' }} 261 | onClick={onOpen} 262 | /> 263 | ) : ( 264 | } 269 | ml={1} 270 | mr={1} 271 | variant="outlined" 272 | whileHover={{ cursor: 'pointer' }} 273 | onClick={() => { 274 | setSettingsMode(false); 275 | setHistoryMode(false); 276 | }} 277 | /> 278 | )} 279 | 280 | {visibleTitle && {chatTitle}} 281 | {!visibleTitle && !historyMode && !settingsMode && ( 282 | 283 | {' '} 284 | {config?.persona || ''} 285 | {' '} 286 | @ 287 | {' '} 288 | {config?.selectedModel || ''} 289 | 290 | )} 291 | {!historyMode && settingsMode && } 292 | {!historyMode && settingsMode && } 293 | {!historyMode && settingsMode && } 294 | {historyMode && chat history} 295 | 296 | {!settingsMode && !historyMode && ( 297 | } 302 | variant="outlined" 303 | whileHover={{ rotate: '15deg', cursor: 'pointer' }} 304 | onClick={reset} 305 | /> 306 | )} 307 | 308 | 320 | 321 | ); 322 | }; 323 | -------------------------------------------------------------------------------- /src/sidePanel/Input.tsx: -------------------------------------------------------------------------------- 1 | import React, { ForwardedRef, useEffect, useRef } from 'react'; 2 | import ResizeTextarea from 'react-textarea-autosize'; 3 | import { Textarea } from '@chakra-ui/react'; 4 | 5 | import { useConfig } from './ConfigContext'; 6 | 7 | export const AutoResizeTextarea = React.forwardRef((props, ref) => ( 8 |