├── .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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
39 |
40 | 
41 |
42 | 
43 |
44 | ## Star History
45 |
46 |
47 |
48 |
49 |
50 |
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 |
--------------------------------------------------------------------------------
/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 |
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 | }
53 | mr={2}
54 | size="md"
55 | onClick={() => setSettingsMode(true)}
56 | >
57 | settings
58 |
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 |