├── .gitignore ├── LICENSE ├── README.md ├── chat-ollama.png ├── config.edn ├── dist ├── assets │ ├── android-icon-144x144.png │ ├── android-icon-192x192.png │ ├── android-icon-36x36.png │ ├── android-icon-48x48.png │ ├── android-icon-72x72.png │ ├── android-icon-96x96.png │ ├── apple-icon-114x114.png │ ├── apple-icon-120x120.png │ ├── apple-icon-144x144.png │ ├── apple-icon-152x152.png │ ├── apple-icon-180x180.png │ ├── apple-icon-57x57.png │ ├── apple-icon-60x60.png │ ├── apple-icon-72x72.png │ ├── apple-icon-76x76.png │ ├── apple-icon-precomposed.png │ ├── apple-icon.png │ ├── browserconfig.xml │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon-96x96.png │ ├── manifest.json │ ├── ms-icon-144x144.png │ ├── ms-icon-150x150.png │ ├── ms-icon-310x310.png │ └── ms-icon-70x70.png ├── favicon.ico ├── index.html ├── js │ └── main.js ├── license.txt └── style.css ├── package-lock.json ├── package.json ├── public ├── assets │ ├── android-icon-144x144.png │ ├── android-icon-192x192.png │ ├── android-icon-36x36.png │ ├── android-icon-48x48.png │ ├── android-icon-72x72.png │ ├── android-icon-96x96.png │ ├── apple-icon-114x114.png │ ├── apple-icon-120x120.png │ ├── apple-icon-144x144.png │ ├── apple-icon-152x152.png │ ├── apple-icon-180x180.png │ ├── apple-icon-57x57.png │ ├── apple-icon-60x60.png │ ├── apple-icon-72x72.png │ ├── apple-icon-76x76.png │ ├── apple-icon-precomposed.png │ ├── apple-icon.png │ ├── browserconfig.xml │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon-96x96.png │ ├── manifest.json │ ├── ms-icon-144x144.png │ ├── ms-icon-150x150.png │ ├── ms-icon-310x310.png │ └── ms-icon-70x70.png ├── favicon.ico ├── index.html └── license.txt ├── shadow-cljs.edn ├── src ├── cljs │ └── chat_ollama │ │ ├── core.cljs │ │ ├── db.cljs │ │ ├── db │ │ ├── dialog.cljs │ │ └── model.cljs │ │ ├── events.cljs │ │ ├── fx.cljs │ │ ├── hooks.cljs │ │ ├── lib.cljc │ │ ├── subs.cljs │ │ ├── utils.cljs │ │ └── views.cljs └── css │ └── style.css ├── tailwind.config.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist-ssr 12 | *.local 13 | 14 | dist/js/manifest.edn 15 | public/js 16 | public/style.css 17 | 18 | # Editor directories and files 19 | .vscode/* 20 | !.vscode/extensions.json 21 | .lsp 22 | .idea 23 | .calva 24 | .clj-kondo/* 25 | .shadow-cljs 26 | .DS_Store 27 | *.suo 28 | *.ntvs* 29 | *.njsproj 30 | *.sln 31 | *.sw? 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Jonathan Dale 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 | # Chat-Ollama 2 | 3 | > Chat with your [Ollama](https://ollama.ai) models, locally 4 | 5 | Chat-Ollama is a local chat app for your Ollama models in a web browser. With multiple dialogs and message formatting support, you can easily communicate with your ollama models without using the CLI. 6 | 7 | ![Chat Ollama Demo](chat-ollama.png) 8 | 9 | ## Usage 10 | 11 | To use chat-ollama _without_ building from source: 12 | 13 | ```shell 14 | # First, clone and move into the repo. 15 | $ git clone https://github.com/jonathandale/chat-ollama 16 | $ cd chat-ollama 17 | ``` 18 | 19 | ```shell 20 | # Then, serve bundled app from /dist 21 | $ yarn serve 22 | # Visit http://localhost:1420 23 | ``` 24 | _NB_: If you don't want to install [serve](https://github.com/vercel/serve), consider an [alternative](https://gist.github.com/willurd/5720255). 25 | 26 | ## Development 27 | 28 | Chat-Ollama is built with [ClojureScript](https://clojurescript.org/), using [Shadow CLJS](https://github.com/thheller/shadow-cljs) for building and [Helix](https://github.com/lilactown/helix) for rendering views. Global state management is handled by [Refx](https://github.com/ferdinand-beyer/refx) (a [re-frame](https://github.com/day8/re-frame) for Helix). Tailwind is used for css. 29 | 30 | ### Requirements 31 | 32 | - node.js (v6.0.0+, most recent version preferred) 33 | - npm (comes bundled with node.js) or yarn 34 | - Java SDK (Version 11+, Latest LTS Version recommended) 35 | 36 | ### Installation 37 | 38 | With [yarn](https://yarnpkg.com/en/): 39 | 40 | ```shell 41 | $ yarn install 42 | ``` 43 | 44 | ### Development 45 | 46 | Running in development watches source files (including css changes), and uses fast-refresh resulting in near-instant feedback. 47 | 48 | ```shell 49 | $ yarn dev 50 | # Visit http://localhost:1420 51 | ``` 52 | ### Release 53 | 54 | Running build compiles javascript and css to the `dist` folder. 55 | 56 | ```shell 57 | $ yarn build 58 | ``` 59 | 60 | Serve the built app. 61 | 62 | ```shell 63 | # Serve bundled app from /dist 64 | $ yarn serve 65 | # Visit http://localhost:1420 66 | ``` 67 | 68 | ## License 69 | 70 | Distributed under the [MIT License](LICENSE). 71 | 72 | Copyright © 2023 Jonathan Dale 73 | -------------------------------------------------------------------------------- /chat-ollama.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathandale/chat-ollama/077e18cc11975d0bdeee66d0b25957c080b2b667/chat-ollama.png -------------------------------------------------------------------------------- /config.edn: -------------------------------------------------------------------------------- 1 | {:lint-as {chat-ollama.lib/defnc clojure.core/defn}} 2 | -------------------------------------------------------------------------------- /dist/assets/android-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathandale/chat-ollama/077e18cc11975d0bdeee66d0b25957c080b2b667/dist/assets/android-icon-144x144.png -------------------------------------------------------------------------------- /dist/assets/android-icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathandale/chat-ollama/077e18cc11975d0bdeee66d0b25957c080b2b667/dist/assets/android-icon-192x192.png -------------------------------------------------------------------------------- /dist/assets/android-icon-36x36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathandale/chat-ollama/077e18cc11975d0bdeee66d0b25957c080b2b667/dist/assets/android-icon-36x36.png -------------------------------------------------------------------------------- /dist/assets/android-icon-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathandale/chat-ollama/077e18cc11975d0bdeee66d0b25957c080b2b667/dist/assets/android-icon-48x48.png -------------------------------------------------------------------------------- /dist/assets/android-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathandale/chat-ollama/077e18cc11975d0bdeee66d0b25957c080b2b667/dist/assets/android-icon-72x72.png -------------------------------------------------------------------------------- /dist/assets/android-icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathandale/chat-ollama/077e18cc11975d0bdeee66d0b25957c080b2b667/dist/assets/android-icon-96x96.png -------------------------------------------------------------------------------- /dist/assets/apple-icon-114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathandale/chat-ollama/077e18cc11975d0bdeee66d0b25957c080b2b667/dist/assets/apple-icon-114x114.png -------------------------------------------------------------------------------- /dist/assets/apple-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathandale/chat-ollama/077e18cc11975d0bdeee66d0b25957c080b2b667/dist/assets/apple-icon-120x120.png -------------------------------------------------------------------------------- /dist/assets/apple-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathandale/chat-ollama/077e18cc11975d0bdeee66d0b25957c080b2b667/dist/assets/apple-icon-144x144.png -------------------------------------------------------------------------------- /dist/assets/apple-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathandale/chat-ollama/077e18cc11975d0bdeee66d0b25957c080b2b667/dist/assets/apple-icon-152x152.png -------------------------------------------------------------------------------- /dist/assets/apple-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathandale/chat-ollama/077e18cc11975d0bdeee66d0b25957c080b2b667/dist/assets/apple-icon-180x180.png -------------------------------------------------------------------------------- /dist/assets/apple-icon-57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathandale/chat-ollama/077e18cc11975d0bdeee66d0b25957c080b2b667/dist/assets/apple-icon-57x57.png -------------------------------------------------------------------------------- /dist/assets/apple-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathandale/chat-ollama/077e18cc11975d0bdeee66d0b25957c080b2b667/dist/assets/apple-icon-60x60.png -------------------------------------------------------------------------------- /dist/assets/apple-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathandale/chat-ollama/077e18cc11975d0bdeee66d0b25957c080b2b667/dist/assets/apple-icon-72x72.png -------------------------------------------------------------------------------- /dist/assets/apple-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathandale/chat-ollama/077e18cc11975d0bdeee66d0b25957c080b2b667/dist/assets/apple-icon-76x76.png -------------------------------------------------------------------------------- /dist/assets/apple-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathandale/chat-ollama/077e18cc11975d0bdeee66d0b25957c080b2b667/dist/assets/apple-icon-precomposed.png -------------------------------------------------------------------------------- /dist/assets/apple-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathandale/chat-ollama/077e18cc11975d0bdeee66d0b25957c080b2b667/dist/assets/apple-icon.png -------------------------------------------------------------------------------- /dist/assets/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | #ffffff -------------------------------------------------------------------------------- /dist/assets/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathandale/chat-ollama/077e18cc11975d0bdeee66d0b25957c080b2b667/dist/assets/favicon-16x16.png -------------------------------------------------------------------------------- /dist/assets/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathandale/chat-ollama/077e18cc11975d0bdeee66d0b25957c080b2b667/dist/assets/favicon-32x32.png -------------------------------------------------------------------------------- /dist/assets/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathandale/chat-ollama/077e18cc11975d0bdeee66d0b25957c080b2b667/dist/assets/favicon-96x96.png -------------------------------------------------------------------------------- /dist/assets/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "App", 3 | "icons": [ 4 | { 5 | "src": "\/android-icon-36x36.png", 6 | "sizes": "36x36", 7 | "type": "image\/png", 8 | "density": "0.75" 9 | }, 10 | { 11 | "src": "\/android-icon-48x48.png", 12 | "sizes": "48x48", 13 | "type": "image\/png", 14 | "density": "1.0" 15 | }, 16 | { 17 | "src": "\/android-icon-72x72.png", 18 | "sizes": "72x72", 19 | "type": "image\/png", 20 | "density": "1.5" 21 | }, 22 | { 23 | "src": "\/android-icon-96x96.png", 24 | "sizes": "96x96", 25 | "type": "image\/png", 26 | "density": "2.0" 27 | }, 28 | { 29 | "src": "\/android-icon-144x144.png", 30 | "sizes": "144x144", 31 | "type": "image\/png", 32 | "density": "3.0" 33 | }, 34 | { 35 | "src": "\/android-icon-192x192.png", 36 | "sizes": "192x192", 37 | "type": "image\/png", 38 | "density": "4.0" 39 | } 40 | ] 41 | } -------------------------------------------------------------------------------- /dist/assets/ms-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathandale/chat-ollama/077e18cc11975d0bdeee66d0b25957c080b2b667/dist/assets/ms-icon-144x144.png -------------------------------------------------------------------------------- /dist/assets/ms-icon-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathandale/chat-ollama/077e18cc11975d0bdeee66d0b25957c080b2b667/dist/assets/ms-icon-150x150.png -------------------------------------------------------------------------------- /dist/assets/ms-icon-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathandale/chat-ollama/077e18cc11975d0bdeee66d0b25957c080b2b667/dist/assets/ms-icon-310x310.png -------------------------------------------------------------------------------- /dist/assets/ms-icon-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathandale/chat-ollama/077e18cc11975d0bdeee66d0b25957c080b2b667/dist/assets/ms-icon-70x70.png -------------------------------------------------------------------------------- /dist/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathandale/chat-ollama/077e18cc11975d0bdeee66d0b25957c080b2b667/dist/favicon.ico -------------------------------------------------------------------------------- /dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Chat Ollama 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 |
30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /dist/license.txt: -------------------------------------------------------------------------------- 1 | # Licenses 2 | 3 | ## @react-spring/web 4 | 5 | MIT License 6 | 7 | Copyright (c) 2018-present Paul Henschel, react-spring, all contributors 8 | 9 | https://opensource.org/license/mit/ 10 | 11 | ## date-fns 12 | 13 | MIT License 14 | 15 | Copyright (c) 2021 Sasha Koss and Lesha Koss https://kossnocorp.mit-license.org 16 | 17 | https://opensource.org/license/mit/ 18 | 19 | ## lucide-react 20 | 21 | ISC License 22 | 23 | Copyright (c) for portions of Lucide are held by Cole Bemis 2013-2022 as part of Feather (MIT). All other copyright (c) for Lucide are held by Lucide Contributors 2022. 24 | 25 | https://opensource.org/license/isc-license-txt/ 26 | 27 | ## react 28 | 29 | MIT License 30 | 31 | Copyright (c) Facebook, Inc. and its affiliates. 32 | 33 | https://opensource.org/license/mit/ 34 | 35 | ## react-dom 36 | 37 | MIT License 38 | 39 | Copyright (c) Facebook, Inc. and its affiliates. 40 | 41 | https://opensource.org/license/mit/ 42 | 43 | ## react-hotkeys-hook 44 | 45 | MIT License 46 | 47 | Copyright (c) 2018 Johannes Klauss 48 | 49 | https://opensource.org/license/mit/ 50 | 51 | ## react-markdown 52 | 53 | The MIT License (MIT) 54 | 55 | Copyright (c) 2015 Espen Hovlandsdal 56 | 57 | https://opensource.org/license/mit/ 58 | 59 | ## react-refresh 60 | 61 | MIT License 62 | 63 | Copyright (c) Facebook, Inc. and its affiliates. 64 | 65 | https://opensource.org/license/mit/ 66 | 67 | ## react-syntax-highlighter 68 | 69 | MIT License 70 | 71 | Copyright (c) 2019 Conor Hastings 72 | 73 | https://opensource.org/license/mit/ 74 | 75 | ## use-sync-external-store 76 | 77 | MIT License 78 | 79 | Copyright (c) Facebook, Inc. and its affiliates. 80 | 81 | https://opensource.org/license/mit/ 82 | 83 | ## lilactown/helix "0.1.10" 84 | 85 | Eclipse Public License - v 2.0 86 | 87 | https://opensource.org/license/epl-2-0/ 88 | 89 | ## com.fbeyer/refx "0.0.49" 90 | 91 | MIT License 92 | 93 | Copyright (c) 2022 Ferdinand Beyer 94 | 95 | https://opensource.org/license/mit/ 96 | 97 | ## applied-science/js-interop "0.4.2" 98 | 99 | Eclipse Public License - v 2.0 100 | 101 | https://opensource.org/license/epl-2-0/ 102 | 103 | ## cljs-bean "1.9.0" 104 | 105 | Eclipse Public License - v 1.0 106 | 107 | https://opensource.org/license/epl-1-0/ 108 | 109 | ## funcool/promesa "11.0.678" 110 | 111 | Mozilla Public License Version 2.0 112 | 113 | https://opensource.org/license/mpl-2-0/ 114 | 115 | ## com.cognitect/transit-cljs "0.8.280" 116 | 117 | Apache License 118 | Version 2.0, January 2004 119 | http://www.apache.org/licenses/ 120 | -------------------------------------------------------------------------------- /dist/style.css: -------------------------------------------------------------------------------- 1 | /* 2 | ! tailwindcss v3.3.3 | MIT License | https://tailwindcss.com 3 | */ 4 | 5 | /* 6 | 1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) 7 | 2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116) 8 | */ 9 | 10 | *, 11 | ::before, 12 | ::after { 13 | box-sizing: border-box; 14 | /* 1 */ 15 | border-width: 0; 16 | /* 2 */ 17 | border-style: solid; 18 | /* 2 */ 19 | border-color: #CED3DE; 20 | /* 2 */ 21 | } 22 | 23 | ::before, 24 | ::after { 25 | --tw-content: ''; 26 | } 27 | 28 | /* 29 | 1. Use a consistent sensible line-height in all browsers. 30 | 2. Prevent adjustments of font size after orientation changes in iOS. 31 | 3. Use a more readable tab size. 32 | 4. Use the user's configured `sans` font-family by default. 33 | 5. Use the user's configured `sans` font-feature-settings by default. 34 | 6. Use the user's configured `sans` font-variation-settings by default. 35 | */ 36 | 37 | html { 38 | line-height: 1.5; 39 | /* 1 */ 40 | -webkit-text-size-adjust: 100%; 41 | /* 2 */ 42 | -moz-tab-size: 4; 43 | /* 3 */ 44 | -o-tab-size: 4; 45 | tab-size: 4; 46 | /* 3 */ 47 | font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; 48 | /* 4 */ 49 | font-feature-settings: normal; 50 | /* 5 */ 51 | font-variation-settings: normal; 52 | /* 6 */ 53 | } 54 | 55 | /* 56 | 1. Remove the margin in all browsers. 57 | 2. Inherit line-height from `html` so users can set them as a class directly on the `html` element. 58 | */ 59 | 60 | body { 61 | margin: 0; 62 | /* 1 */ 63 | line-height: inherit; 64 | /* 2 */ 65 | } 66 | 67 | /* 68 | 1. Add the correct height in Firefox. 69 | 2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) 70 | 3. Ensure horizontal rules are visible by default. 71 | */ 72 | 73 | hr { 74 | height: 0; 75 | /* 1 */ 76 | color: inherit; 77 | /* 2 */ 78 | border-top-width: 1px; 79 | /* 3 */ 80 | } 81 | 82 | /* 83 | Add the correct text decoration in Chrome, Edge, and Safari. 84 | */ 85 | 86 | abbr:where([title]) { 87 | -webkit-text-decoration: underline dotted; 88 | text-decoration: underline dotted; 89 | } 90 | 91 | /* 92 | Remove the default font size and weight for headings. 93 | */ 94 | 95 | h1, 96 | h2, 97 | h3, 98 | h4, 99 | h5, 100 | h6 { 101 | font-size: inherit; 102 | font-weight: inherit; 103 | } 104 | 105 | /* 106 | Reset links to optimize for opt-in styling instead of opt-out. 107 | */ 108 | 109 | a { 110 | color: inherit; 111 | text-decoration: inherit; 112 | } 113 | 114 | /* 115 | Add the correct font weight in Edge and Safari. 116 | */ 117 | 118 | b, 119 | strong { 120 | font-weight: bolder; 121 | } 122 | 123 | /* 124 | 1. Use the user's configured `mono` font family by default. 125 | 2. Correct the odd `em` font sizing in all browsers. 126 | */ 127 | 128 | code, 129 | kbd, 130 | samp, 131 | pre { 132 | font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; 133 | /* 1 */ 134 | font-size: 1em; 135 | /* 2 */ 136 | } 137 | 138 | /* 139 | Add the correct font size in all browsers. 140 | */ 141 | 142 | small { 143 | font-size: 80%; 144 | } 145 | 146 | /* 147 | Prevent `sub` and `sup` elements from affecting the line height in all browsers. 148 | */ 149 | 150 | sub, 151 | sup { 152 | font-size: 75%; 153 | line-height: 0; 154 | position: relative; 155 | vertical-align: baseline; 156 | } 157 | 158 | sub { 159 | bottom: -0.25em; 160 | } 161 | 162 | sup { 163 | top: -0.5em; 164 | } 165 | 166 | /* 167 | 1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) 168 | 2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) 169 | 3. Remove gaps between table borders by default. 170 | */ 171 | 172 | table { 173 | text-indent: 0; 174 | /* 1 */ 175 | border-color: inherit; 176 | /* 2 */ 177 | border-collapse: collapse; 178 | /* 3 */ 179 | } 180 | 181 | /* 182 | 1. Change the font styles in all browsers. 183 | 2. Remove the margin in Firefox and Safari. 184 | 3. Remove default padding in all browsers. 185 | */ 186 | 187 | button, 188 | input, 189 | optgroup, 190 | select, 191 | textarea { 192 | font-family: inherit; 193 | /* 1 */ 194 | font-feature-settings: inherit; 195 | /* 1 */ 196 | font-variation-settings: inherit; 197 | /* 1 */ 198 | font-size: 100%; 199 | /* 1 */ 200 | font-weight: inherit; 201 | /* 1 */ 202 | line-height: inherit; 203 | /* 1 */ 204 | color: inherit; 205 | /* 1 */ 206 | margin: 0; 207 | /* 2 */ 208 | padding: 0; 209 | /* 3 */ 210 | } 211 | 212 | /* 213 | Remove the inheritance of text transform in Edge and Firefox. 214 | */ 215 | 216 | button, 217 | select { 218 | text-transform: none; 219 | } 220 | 221 | /* 222 | 1. Correct the inability to style clickable types in iOS and Safari. 223 | 2. Remove default button styles. 224 | */ 225 | 226 | button, 227 | [type='button'], 228 | [type='reset'], 229 | [type='submit'] { 230 | -webkit-appearance: button; 231 | /* 1 */ 232 | background-color: transparent; 233 | /* 2 */ 234 | background-image: none; 235 | /* 2 */ 236 | } 237 | 238 | /* 239 | Use the modern Firefox focus style for all focusable elements. 240 | */ 241 | 242 | :-moz-focusring { 243 | outline: auto; 244 | } 245 | 246 | /* 247 | Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) 248 | */ 249 | 250 | :-moz-ui-invalid { 251 | box-shadow: none; 252 | } 253 | 254 | /* 255 | Add the correct vertical alignment in Chrome and Firefox. 256 | */ 257 | 258 | progress { 259 | vertical-align: baseline; 260 | } 261 | 262 | /* 263 | Correct the cursor style of increment and decrement buttons in Safari. 264 | */ 265 | 266 | ::-webkit-inner-spin-button, 267 | ::-webkit-outer-spin-button { 268 | height: auto; 269 | } 270 | 271 | /* 272 | 1. Correct the odd appearance in Chrome and Safari. 273 | 2. Correct the outline style in Safari. 274 | */ 275 | 276 | [type='search'] { 277 | -webkit-appearance: textfield; 278 | /* 1 */ 279 | outline-offset: -2px; 280 | /* 2 */ 281 | } 282 | 283 | /* 284 | Remove the inner padding in Chrome and Safari on macOS. 285 | */ 286 | 287 | ::-webkit-search-decoration { 288 | -webkit-appearance: none; 289 | } 290 | 291 | /* 292 | 1. Correct the inability to style clickable types in iOS and Safari. 293 | 2. Change font properties to `inherit` in Safari. 294 | */ 295 | 296 | ::-webkit-file-upload-button { 297 | -webkit-appearance: button; 298 | /* 1 */ 299 | font: inherit; 300 | /* 2 */ 301 | } 302 | 303 | /* 304 | Add the correct display in Chrome and Safari. 305 | */ 306 | 307 | summary { 308 | display: list-item; 309 | } 310 | 311 | /* 312 | Removes the default spacing and border for appropriate elements. 313 | */ 314 | 315 | blockquote, 316 | dl, 317 | dd, 318 | h1, 319 | h2, 320 | h3, 321 | h4, 322 | h5, 323 | h6, 324 | hr, 325 | figure, 326 | p, 327 | pre { 328 | margin: 0; 329 | } 330 | 331 | fieldset { 332 | margin: 0; 333 | padding: 0; 334 | } 335 | 336 | legend { 337 | padding: 0; 338 | } 339 | 340 | ol, 341 | ul, 342 | menu { 343 | list-style: none; 344 | margin: 0; 345 | padding: 0; 346 | } 347 | 348 | /* 349 | Reset default styling for dialogs. 350 | */ 351 | 352 | dialog { 353 | padding: 0; 354 | } 355 | 356 | /* 357 | Prevent resizing textareas horizontally by default. 358 | */ 359 | 360 | textarea { 361 | resize: vertical; 362 | } 363 | 364 | /* 365 | 1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) 366 | 2. Set the default placeholder color to the user's configured gray 400 color. 367 | */ 368 | 369 | input::-moz-placeholder, textarea::-moz-placeholder { 370 | opacity: 1; 371 | /* 1 */ 372 | color: #919CB6; 373 | /* 2 */ 374 | } 375 | 376 | input::placeholder, 377 | textarea::placeholder { 378 | opacity: 1; 379 | /* 1 */ 380 | color: #919CB6; 381 | /* 2 */ 382 | } 383 | 384 | /* 385 | Set the default cursor for buttons. 386 | */ 387 | 388 | button, 389 | [role="button"] { 390 | cursor: pointer; 391 | } 392 | 393 | /* 394 | Make sure disabled buttons don't get the pointer cursor. 395 | */ 396 | 397 | :disabled { 398 | cursor: default; 399 | } 400 | 401 | /* 402 | 1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14) 403 | 2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) 404 | This can trigger a poorly considered lint error in some tools but is included by design. 405 | */ 406 | 407 | img, 408 | svg, 409 | video, 410 | canvas, 411 | audio, 412 | iframe, 413 | embed, 414 | object { 415 | display: block; 416 | /* 1 */ 417 | vertical-align: middle; 418 | /* 2 */ 419 | } 420 | 421 | /* 422 | Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) 423 | */ 424 | 425 | img, 426 | video { 427 | max-width: 100%; 428 | height: auto; 429 | } 430 | 431 | /* Make elements with the HTML hidden attribute stay hidden by default */ 432 | 433 | [hidden] { 434 | display: none; 435 | } 436 | 437 | *, ::before, ::after { 438 | --tw-border-spacing-x: 0; 439 | --tw-border-spacing-y: 0; 440 | --tw-translate-x: 0; 441 | --tw-translate-y: 0; 442 | --tw-rotate: 0; 443 | --tw-skew-x: 0; 444 | --tw-skew-y: 0; 445 | --tw-scale-x: 1; 446 | --tw-scale-y: 1; 447 | --tw-pan-x: ; 448 | --tw-pan-y: ; 449 | --tw-pinch-zoom: ; 450 | --tw-scroll-snap-strictness: proximity; 451 | --tw-gradient-from-position: ; 452 | --tw-gradient-via-position: ; 453 | --tw-gradient-to-position: ; 454 | --tw-ordinal: ; 455 | --tw-slashed-zero: ; 456 | --tw-numeric-figure: ; 457 | --tw-numeric-spacing: ; 458 | --tw-numeric-fraction: ; 459 | --tw-ring-inset: ; 460 | --tw-ring-offset-width: 0px; 461 | --tw-ring-offset-color: #fff; 462 | --tw-ring-color: rgb(59 130 246 / 0.5); 463 | --tw-ring-offset-shadow: 0 0 #0000; 464 | --tw-ring-shadow: 0 0 #0000; 465 | --tw-shadow: 0 0 #0000; 466 | --tw-shadow-colored: 0 0 #0000; 467 | --tw-blur: ; 468 | --tw-brightness: ; 469 | --tw-contrast: ; 470 | --tw-grayscale: ; 471 | --tw-hue-rotate: ; 472 | --tw-invert: ; 473 | --tw-saturate: ; 474 | --tw-sepia: ; 475 | --tw-drop-shadow: ; 476 | --tw-backdrop-blur: ; 477 | --tw-backdrop-brightness: ; 478 | --tw-backdrop-contrast: ; 479 | --tw-backdrop-grayscale: ; 480 | --tw-backdrop-hue-rotate: ; 481 | --tw-backdrop-invert: ; 482 | --tw-backdrop-opacity: ; 483 | --tw-backdrop-saturate: ; 484 | --tw-backdrop-sepia: ; 485 | } 486 | 487 | ::backdrop { 488 | --tw-border-spacing-x: 0; 489 | --tw-border-spacing-y: 0; 490 | --tw-translate-x: 0; 491 | --tw-translate-y: 0; 492 | --tw-rotate: 0; 493 | --tw-skew-x: 0; 494 | --tw-skew-y: 0; 495 | --tw-scale-x: 1; 496 | --tw-scale-y: 1; 497 | --tw-pan-x: ; 498 | --tw-pan-y: ; 499 | --tw-pinch-zoom: ; 500 | --tw-scroll-snap-strictness: proximity; 501 | --tw-gradient-from-position: ; 502 | --tw-gradient-via-position: ; 503 | --tw-gradient-to-position: ; 504 | --tw-ordinal: ; 505 | --tw-slashed-zero: ; 506 | --tw-numeric-figure: ; 507 | --tw-numeric-spacing: ; 508 | --tw-numeric-fraction: ; 509 | --tw-ring-inset: ; 510 | --tw-ring-offset-width: 0px; 511 | --tw-ring-offset-color: #fff; 512 | --tw-ring-color: rgb(59 130 246 / 0.5); 513 | --tw-ring-offset-shadow: 0 0 #0000; 514 | --tw-ring-shadow: 0 0 #0000; 515 | --tw-shadow: 0 0 #0000; 516 | --tw-shadow-colored: 0 0 #0000; 517 | --tw-blur: ; 518 | --tw-brightness: ; 519 | --tw-contrast: ; 520 | --tw-grayscale: ; 521 | --tw-hue-rotate: ; 522 | --tw-invert: ; 523 | --tw-saturate: ; 524 | --tw-sepia: ; 525 | --tw-drop-shadow: ; 526 | --tw-backdrop-blur: ; 527 | --tw-backdrop-brightness: ; 528 | --tw-backdrop-contrast: ; 529 | --tw-backdrop-grayscale: ; 530 | --tw-backdrop-hue-rotate: ; 531 | --tw-backdrop-invert: ; 532 | --tw-backdrop-opacity: ; 533 | --tw-backdrop-saturate: ; 534 | --tw-backdrop-sepia: ; 535 | } 536 | 537 | .pointer-events-none { 538 | pointer-events: none; 539 | } 540 | 541 | .absolute { 542 | position: absolute; 543 | } 544 | 545 | .relative { 546 | position: relative; 547 | } 548 | 549 | .inset-0 { 550 | inset: 0px; 551 | } 552 | 553 | .inset-x-0 { 554 | left: 0px; 555 | right: 0px; 556 | } 557 | 558 | .inset-x-16 { 559 | left: 4rem; 560 | right: 4rem; 561 | } 562 | 563 | .bottom-0 { 564 | bottom: 0px; 565 | } 566 | 567 | .bottom-20 { 568 | bottom: 5rem; 569 | } 570 | 571 | .bottom-9 { 572 | bottom: 2.25rem; 573 | } 574 | 575 | .left-0 { 576 | left: 0px; 577 | } 578 | 579 | .right-0 { 580 | right: 0px; 581 | } 582 | 583 | .right-1 { 584 | right: 0.25rem; 585 | } 586 | 587 | .right-1\.5 { 588 | right: 0.375rem; 589 | } 590 | 591 | .right-3 { 592 | right: 0.75rem; 593 | } 594 | 595 | .right-3\.5 { 596 | right: 0.875rem; 597 | } 598 | 599 | .right-4 { 600 | right: 1rem; 601 | } 602 | 603 | .top-0 { 604 | top: 0px; 605 | } 606 | 607 | .top-1 { 608 | top: 0.25rem; 609 | } 610 | 611 | .top-1\.5 { 612 | top: 0.375rem; 613 | } 614 | 615 | .top-4 { 616 | top: 1rem; 617 | } 618 | 619 | .z-0 { 620 | z-index: 0; 621 | } 622 | 623 | .z-10 { 624 | z-index: 10; 625 | } 626 | 627 | .z-20 { 628 | z-index: 20; 629 | } 630 | 631 | .z-30 { 632 | z-index: 30; 633 | } 634 | 635 | .z-50 { 636 | z-index: 50; 637 | } 638 | 639 | .-m-2 { 640 | margin: -0.5rem; 641 | } 642 | 643 | .mx-auto { 644 | margin-left: auto; 645 | margin-right: auto; 646 | } 647 | 648 | .mb-1 { 649 | margin-bottom: 0.25rem; 650 | } 651 | 652 | .mb-1\.5 { 653 | margin-bottom: 0.375rem; 654 | } 655 | 656 | .mb-10 { 657 | margin-bottom: 2.5rem; 658 | } 659 | 660 | .mb-3 { 661 | margin-bottom: 0.75rem; 662 | } 663 | 664 | .mb-3\.5 { 665 | margin-bottom: 0.875rem; 666 | } 667 | 668 | .mb-4 { 669 | margin-bottom: 1rem; 670 | } 671 | 672 | .ml-2 { 673 | margin-left: 0.5rem; 674 | } 675 | 676 | .ml-6 { 677 | margin-left: 1.5rem; 678 | } 679 | 680 | .mr-3 { 681 | margin-right: 0.75rem; 682 | } 683 | 684 | .mt-12 { 685 | margin-top: 3rem; 686 | } 687 | 688 | .mt-2 { 689 | margin-top: 0.5rem; 690 | } 691 | 692 | .mt-6 { 693 | margin-top: 1.5rem; 694 | } 695 | 696 | .inline { 697 | display: inline; 698 | } 699 | 700 | .flex { 701 | display: flex; 702 | } 703 | 704 | .hidden { 705 | display: none; 706 | } 707 | 708 | .h-10 { 709 | height: 2.5rem; 710 | } 711 | 712 | .h-12 { 713 | height: 3rem; 714 | } 715 | 716 | .h-2 { 717 | height: 0.5rem; 718 | } 719 | 720 | .h-9 { 721 | height: 2.25rem; 722 | } 723 | 724 | .h-\[101px\] { 725 | height: 101px; 726 | } 727 | 728 | .h-\[27px\] { 729 | height: 27px; 730 | } 731 | 732 | .h-fit { 733 | height: -moz-fit-content; 734 | height: fit-content; 735 | } 736 | 737 | .h-full { 738 | height: 100%; 739 | } 740 | 741 | .h-screen { 742 | height: 100vh; 743 | } 744 | 745 | .w-10 { 746 | width: 2.5rem; 747 | } 748 | 749 | .w-\[21px\] { 750 | width: 21px; 751 | } 752 | 753 | .w-\[75\%\] { 754 | width: 75%; 755 | } 756 | 757 | .w-\[77px\] { 758 | width: 77px; 759 | } 760 | 761 | .w-full { 762 | width: 100%; 763 | } 764 | 765 | .w-screen { 766 | width: 100vw; 767 | } 768 | 769 | .min-w-\[250px\] { 770 | min-width: 250px; 771 | } 772 | 773 | .max-w-2xl { 774 | max-width: 42rem; 775 | } 776 | 777 | .max-w-5xl { 778 | max-width: 64rem; 779 | } 780 | 781 | .max-w-6xl { 782 | max-width: 72rem; 783 | } 784 | 785 | .max-w-full { 786 | max-width: 100%; 787 | } 788 | 789 | .shrink-0 { 790 | flex-shrink: 0; 791 | } 792 | 793 | .grow { 794 | flex-grow: 1; 795 | } 796 | 797 | .-translate-y-full { 798 | --tw-translate-y: -100%; 799 | transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); 800 | } 801 | 802 | .scale-125 { 803 | --tw-scale-x: 1.25; 804 | --tw-scale-y: 1.25; 805 | transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); 806 | } 807 | 808 | @keyframes pulse { 809 | 50% { 810 | opacity: .5; 811 | } 812 | } 813 | 814 | .animate-pulse { 815 | animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; 816 | } 817 | 818 | .cursor-default { 819 | cursor: default; 820 | } 821 | 822 | .select-none { 823 | -webkit-user-select: none; 824 | -moz-user-select: none; 825 | user-select: none; 826 | } 827 | 828 | .resize-none { 829 | resize: none; 830 | } 831 | 832 | .flex-row-reverse { 833 | flex-direction: row-reverse; 834 | } 835 | 836 | .flex-col { 837 | flex-direction: column; 838 | } 839 | 840 | .items-end { 841 | align-items: flex-end; 842 | } 843 | 844 | .items-center { 845 | align-items: center; 846 | } 847 | 848 | .justify-end { 849 | justify-content: flex-end; 850 | } 851 | 852 | .justify-center { 853 | justify-content: center; 854 | } 855 | 856 | .justify-between { 857 | justify-content: space-between; 858 | } 859 | 860 | .gap-2 { 861 | gap: 0.5rem; 862 | } 863 | 864 | .gap-2\.5 { 865 | gap: 0.625rem; 866 | } 867 | 868 | .gap-3 { 869 | gap: 0.75rem; 870 | } 871 | 872 | .gap-6 { 873 | gap: 1.5rem; 874 | } 875 | 876 | .gap-y-1 { 877 | row-gap: 0.25rem; 878 | } 879 | 880 | .divide-y > :not([hidden]) ~ :not([hidden]) { 881 | --tw-divide-y-reverse: 0; 882 | border-top-width: calc(1px * calc(1 - var(--tw-divide-y-reverse))); 883 | border-bottom-width: calc(1px * var(--tw-divide-y-reverse)); 884 | } 885 | 886 | .divide-gray-400\/50 > :not([hidden]) ~ :not([hidden]) { 887 | border-color: rgb(145 156 182 / 0.5); 888 | } 889 | 890 | .place-self-start { 891 | place-self: start; 892 | } 893 | 894 | .place-self-end { 895 | place-self: end; 896 | } 897 | 898 | .overflow-hidden { 899 | overflow: hidden; 900 | } 901 | 902 | .overflow-scroll { 903 | overflow: scroll; 904 | } 905 | 906 | .truncate { 907 | overflow: hidden; 908 | text-overflow: ellipsis; 909 | white-space: nowrap; 910 | } 911 | 912 | .rounded { 913 | border-radius: 0.25rem; 914 | } 915 | 916 | .rounded-md { 917 | border-radius: 0.375rem; 918 | } 919 | 920 | .rounded-sm { 921 | border-radius: 0.125rem; 922 | } 923 | 924 | .border { 925 | border-width: 1px; 926 | } 927 | 928 | .border-l-4 { 929 | border-left-width: 4px; 930 | } 931 | 932 | .border-cyan-600 { 933 | --tw-border-opacity: 1; 934 | border-color: rgb(8 145 178 / var(--tw-border-opacity)); 935 | } 936 | 937 | .border-gray-300\/50 { 938 | border-color: rgb(175 183 202 / 0.5); 939 | } 940 | 941 | .border-gray-300\/60 { 942 | border-color: rgb(175 183 202 / 0.6); 943 | } 944 | 945 | .border-gray-400\/50 { 946 | border-color: rgb(145 156 182 / 0.5); 947 | } 948 | 949 | .border-transparent { 950 | border-color: transparent; 951 | } 952 | 953 | .bg-cyan-600 { 954 | --tw-bg-opacity: 1; 955 | background-color: rgb(8 145 178 / var(--tw-bg-opacity)); 956 | } 957 | 958 | .bg-gray-100 { 959 | --tw-bg-opacity: 1; 960 | background-color: rgb(240 241 245 / var(--tw-bg-opacity)); 961 | } 962 | 963 | .bg-gray-200 { 964 | --tw-bg-opacity: 1; 965 | background-color: rgb(206 211 222 / var(--tw-bg-opacity)); 966 | } 967 | 968 | .bg-gray-200\/75 { 969 | background-color: rgb(206 211 222 / 0.75); 970 | } 971 | 972 | .bg-gray-300\/20 { 973 | background-color: rgb(175 183 202 / 0.2); 974 | } 975 | 976 | .bg-gray-300\/40 { 977 | background-color: rgb(175 183 202 / 0.4); 978 | } 979 | 980 | .bg-gray-50 { 981 | --tw-bg-opacity: 1; 982 | background-color: rgb(243 245 247 / var(--tw-bg-opacity)); 983 | } 984 | 985 | .bg-gray-800 { 986 | --tw-bg-opacity: 1; 987 | background-color: rgb(47 54 70 / var(--tw-bg-opacity)); 988 | } 989 | 990 | .bg-white { 991 | --tw-bg-opacity: 1; 992 | background-color: rgb(255 255 255 / var(--tw-bg-opacity)); 993 | } 994 | 995 | .bg-white\/30 { 996 | background-color: rgb(255 255 255 / 0.3); 997 | } 998 | 999 | .bg-white\/75 { 1000 | background-color: rgb(255 255 255 / 0.75); 1001 | } 1002 | 1003 | .bg-gradient-to-t { 1004 | background-image: linear-gradient(to top, var(--tw-gradient-stops)); 1005 | } 1006 | 1007 | .from-transparent { 1008 | --tw-gradient-from: transparent var(--tw-gradient-from-position); 1009 | --tw-gradient-to: rgb(0 0 0 / 0) var(--tw-gradient-to-position); 1010 | --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to); 1011 | } 1012 | 1013 | .from-white { 1014 | --tw-gradient-from: #fff var(--tw-gradient-from-position); 1015 | --tw-gradient-to: rgb(255 255 255 / 0) var(--tw-gradient-to-position); 1016 | --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to); 1017 | } 1018 | 1019 | .to-transparent { 1020 | --tw-gradient-to: transparent var(--tw-gradient-to-position); 1021 | } 1022 | 1023 | .to-white { 1024 | --tw-gradient-to: #fff var(--tw-gradient-to-position); 1025 | } 1026 | 1027 | .fill-gray-900 { 1028 | fill: #1A1E27; 1029 | } 1030 | 1031 | .fill-white { 1032 | fill: #fff; 1033 | } 1034 | 1035 | .p-1 { 1036 | padding: 0.25rem; 1037 | } 1038 | 1039 | .p-1\.5 { 1040 | padding: 0.375rem; 1041 | } 1042 | 1043 | .p-2 { 1044 | padding: 0.5rem; 1045 | } 1046 | 1047 | .p-2\.5 { 1048 | padding: 0.625rem; 1049 | } 1050 | 1051 | .p-4 { 1052 | padding: 1rem; 1053 | } 1054 | 1055 | .p-6 { 1056 | padding: 1.5rem; 1057 | } 1058 | 1059 | .px-20 { 1060 | padding-left: 5rem; 1061 | padding-right: 5rem; 1062 | } 1063 | 1064 | .px-3 { 1065 | padding-left: 0.75rem; 1066 | padding-right: 0.75rem; 1067 | } 1068 | 1069 | .px-4 { 1070 | padding-left: 1rem; 1071 | padding-right: 1rem; 1072 | } 1073 | 1074 | .px-\[4\.5rem\] { 1075 | padding-left: 4.5rem; 1076 | padding-right: 4.5rem; 1077 | } 1078 | 1079 | .py-1 { 1080 | padding-top: 0.25rem; 1081 | padding-bottom: 0.25rem; 1082 | } 1083 | 1084 | .py-1\.5 { 1085 | padding-top: 0.375rem; 1086 | padding-bottom: 0.375rem; 1087 | } 1088 | 1089 | .py-12 { 1090 | padding-top: 3rem; 1091 | padding-bottom: 3rem; 1092 | } 1093 | 1094 | .py-16 { 1095 | padding-top: 4rem; 1096 | padding-bottom: 4rem; 1097 | } 1098 | 1099 | .py-2 { 1100 | padding-top: 0.5rem; 1101 | padding-bottom: 0.5rem; 1102 | } 1103 | 1104 | .py-2\.5 { 1105 | padding-top: 0.625rem; 1106 | padding-bottom: 0.625rem; 1107 | } 1108 | 1109 | .pb-28 { 1110 | padding-bottom: 7rem; 1111 | } 1112 | 1113 | .pb-6 { 1114 | padding-bottom: 1.5rem; 1115 | } 1116 | 1117 | .pl-3 { 1118 | padding-left: 0.75rem; 1119 | } 1120 | 1121 | .pl-3\.5 { 1122 | padding-left: 0.875rem; 1123 | } 1124 | 1125 | .pl-4 { 1126 | padding-left: 1rem; 1127 | } 1128 | 1129 | .pr-10 { 1130 | padding-right: 2.5rem; 1131 | } 1132 | 1133 | .pr-2 { 1134 | padding-right: 0.5rem; 1135 | } 1136 | 1137 | .pr-3 { 1138 | padding-right: 0.75rem; 1139 | } 1140 | 1141 | .pr-3\.5 { 1142 | padding-right: 0.875rem; 1143 | } 1144 | 1145 | .pt-6 { 1146 | padding-top: 1.5rem; 1147 | } 1148 | 1149 | .text-left { 1150 | text-align: left; 1151 | } 1152 | 1153 | .text-center { 1154 | text-align: center; 1155 | } 1156 | 1157 | .font-mono { 1158 | font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; 1159 | } 1160 | 1161 | .text-2xl { 1162 | font-size: 1.5rem; 1163 | line-height: 2rem; 1164 | } 1165 | 1166 | .text-3xl { 1167 | font-size: 1.875rem; 1168 | line-height: 2.25rem; 1169 | } 1170 | 1171 | .text-base { 1172 | font-size: 1rem; 1173 | line-height: 1.5rem; 1174 | } 1175 | 1176 | .text-lg { 1177 | font-size: 1.125rem; 1178 | line-height: 1.75rem; 1179 | } 1180 | 1181 | .text-sm { 1182 | font-size: 0.875rem; 1183 | line-height: 1.25rem; 1184 | } 1185 | 1186 | .text-xs { 1187 | font-size: 0.75rem; 1188 | line-height: 1rem; 1189 | } 1190 | 1191 | .font-normal { 1192 | font-weight: 400; 1193 | } 1194 | 1195 | .italic { 1196 | font-style: italic; 1197 | } 1198 | 1199 | .text-gray-300 { 1200 | --tw-text-opacity: 1; 1201 | color: rgb(175 183 202 / var(--tw-text-opacity)); 1202 | } 1203 | 1204 | .text-gray-400 { 1205 | --tw-text-opacity: 1; 1206 | color: rgb(145 156 182 / var(--tw-text-opacity)); 1207 | } 1208 | 1209 | .text-gray-600 { 1210 | --tw-text-opacity: 1; 1211 | color: rgb(88 101 132 / var(--tw-text-opacity)); 1212 | } 1213 | 1214 | .text-gray-600\/80 { 1215 | color: rgb(88 101 132 / 0.8); 1216 | } 1217 | 1218 | .text-gray-700 { 1219 | --tw-text-opacity: 1; 1220 | color: rgb(67 77 101 / var(--tw-text-opacity)); 1221 | } 1222 | 1223 | .text-gray-800\/60 { 1224 | color: rgb(47 54 70 / 0.6); 1225 | } 1226 | 1227 | .text-gray-900 { 1228 | --tw-text-opacity: 1; 1229 | color: rgb(26 30 39 / var(--tw-text-opacity)); 1230 | } 1231 | 1232 | .text-white { 1233 | --tw-text-opacity: 1; 1234 | color: rgb(255 255 255 / var(--tw-text-opacity)); 1235 | } 1236 | 1237 | .placeholder-gray-400\/75::-moz-placeholder { 1238 | color: rgb(145 156 182 / 0.75); 1239 | } 1240 | 1241 | .placeholder-gray-400\/75::placeholder { 1242 | color: rgb(145 156 182 / 0.75); 1243 | } 1244 | 1245 | .opacity-20 { 1246 | opacity: 0.2; 1247 | } 1248 | 1249 | .opacity-25 { 1250 | opacity: 0.25; 1251 | } 1252 | 1253 | .opacity-40 { 1254 | opacity: 0.4; 1255 | } 1256 | 1257 | .opacity-50 { 1258 | opacity: 0.5; 1259 | } 1260 | 1261 | .opacity-60 { 1262 | opacity: 0.6; 1263 | } 1264 | 1265 | .opacity-75 { 1266 | opacity: 0.75; 1267 | } 1268 | 1269 | .shadow { 1270 | --tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); 1271 | --tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color); 1272 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); 1273 | } 1274 | 1275 | .backdrop-blur { 1276 | --tw-backdrop-blur: blur(8px); 1277 | -webkit-backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia); 1278 | backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia); 1279 | } 1280 | 1281 | .backdrop-blur-md { 1282 | --tw-backdrop-blur: blur(12px); 1283 | -webkit-backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia); 1284 | backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia); 1285 | } 1286 | 1287 | @media (prefers-color-scheme: dark) { 1288 | .markdown-body { 1289 | color-scheme: dark; 1290 | --color-prettylights-syntax-comment: #8b949e; 1291 | --color-prettylights-syntax-constant: #79c0ff; 1292 | --color-prettylights-syntax-entity: #d2a8ff; 1293 | --color-prettylights-syntax-storage-modifier-import: #c9d1d9; 1294 | --color-prettylights-syntax-entity-tag: #7ee787; 1295 | --color-prettylights-syntax-keyword: #ff7b72; 1296 | --color-prettylights-syntax-string: #a5d6ff; 1297 | --color-prettylights-syntax-variable: #ffa657; 1298 | --color-prettylights-syntax-brackethighlighter-unmatched: #f85149; 1299 | --color-prettylights-syntax-invalid-illegal-text: #f0f6fc; 1300 | --color-prettylights-syntax-invalid-illegal-bg: #8e1519; 1301 | --color-prettylights-syntax-carriage-return-text: #f0f6fc; 1302 | --color-prettylights-syntax-carriage-return-bg: #b62324; 1303 | --color-prettylights-syntax-string-regexp: #7ee787; 1304 | --color-prettylights-syntax-markup-list: #f2cc60; 1305 | --color-prettylights-syntax-markup-heading: #1f6feb; 1306 | --color-prettylights-syntax-markup-italic: #c9d1d9; 1307 | --color-prettylights-syntax-markup-bold: #c9d1d9; 1308 | --color-prettylights-syntax-markup-deleted-text: #ffdcd7; 1309 | --color-prettylights-syntax-markup-deleted-bg: #67060c; 1310 | --color-prettylights-syntax-markup-inserted-text: #aff5b4; 1311 | --color-prettylights-syntax-markup-inserted-bg: #033a16; 1312 | --color-prettylights-syntax-markup-changed-text: #ffdfb6; 1313 | --color-prettylights-syntax-markup-changed-bg: #5a1e02; 1314 | --color-prettylights-syntax-markup-ignored-text: #c9d1d9; 1315 | --color-prettylights-syntax-markup-ignored-bg: #1158c7; 1316 | --color-prettylights-syntax-meta-diff-range: #d2a8ff; 1317 | --color-prettylights-syntax-brackethighlighter-angle: #8b949e; 1318 | --color-prettylights-syntax-sublimelinter-gutter-mark: #484f58; 1319 | --color-prettylights-syntax-constant-other-reference-link: #a5d6ff; 1320 | --color-fg-default: #c9d1d9; 1321 | --color-fg-muted: #8b949e; 1322 | --color-fg-subtle: #6e7681; 1323 | --color-canvas-default: #0d1117; 1324 | --color-canvas-subtle: #161b22; 1325 | --color-border-default: #30363d; 1326 | --color-border-muted: #21262d; 1327 | --color-neutral-muted: rgba(110, 118, 129, 0.4); 1328 | --color-accent-fg: #58a6ff; 1329 | --color-accent-emphasis: #1f6feb; 1330 | --color-attention-subtle: rgba(187, 128, 9, 0.15); 1331 | --color-danger-fg: #f85149; 1332 | } 1333 | } 1334 | 1335 | @media (prefers-color-scheme: light) { 1336 | .markdown-body { 1337 | color-scheme: light; 1338 | --color-prettylights-syntax-comment: #6e7781; 1339 | --color-prettylights-syntax-constant: #0550ae; 1340 | --color-prettylights-syntax-entity: #8250df; 1341 | --color-prettylights-syntax-storage-modifier-import: #24292f; 1342 | --color-prettylights-syntax-entity-tag: #116329; 1343 | --color-prettylights-syntax-keyword: #cf222e; 1344 | --color-prettylights-syntax-string: #0a3069; 1345 | --color-prettylights-syntax-variable: #953800; 1346 | --color-prettylights-syntax-brackethighlighter-unmatched: #82071e; 1347 | --color-prettylights-syntax-invalid-illegal-text: #f6f8fa; 1348 | --color-prettylights-syntax-invalid-illegal-bg: #82071e; 1349 | --color-prettylights-syntax-carriage-return-text: #f6f8fa; 1350 | --color-prettylights-syntax-carriage-return-bg: #cf222e; 1351 | --color-prettylights-syntax-string-regexp: #116329; 1352 | --color-prettylights-syntax-markup-list: #3b2300; 1353 | --color-prettylights-syntax-markup-heading: #0550ae; 1354 | --color-prettylights-syntax-markup-italic: #24292f; 1355 | --color-prettylights-syntax-markup-bold: #24292f; 1356 | --color-prettylights-syntax-markup-deleted-text: #82071e; 1357 | --color-prettylights-syntax-markup-deleted-bg: #ffebe9; 1358 | --color-prettylights-syntax-markup-inserted-text: #116329; 1359 | --color-prettylights-syntax-markup-inserted-bg: #dafbe1; 1360 | --color-prettylights-syntax-markup-changed-text: #953800; 1361 | --color-prettylights-syntax-markup-changed-bg: #ffd8b5; 1362 | --color-prettylights-syntax-markup-ignored-text: #eaeef2; 1363 | --color-prettylights-syntax-markup-ignored-bg: #0550ae; 1364 | --color-prettylights-syntax-meta-diff-range: #8250df; 1365 | --color-prettylights-syntax-brackethighlighter-angle: #57606a; 1366 | --color-prettylights-syntax-sublimelinter-gutter-mark: #8c959f; 1367 | --color-prettylights-syntax-constant-other-reference-link: #0a3069; 1368 | --color-fg-default: #24292f; 1369 | --color-fg-muted: #57606a; 1370 | --color-fg-subtle: #6e7781; 1371 | --color-canvas-default: #ffffff; 1372 | --color-canvas-subtle: #f6f8fa; 1373 | --color-border-default: #d0d7de; 1374 | --color-border-muted: hsla(210, 18%, 87%, 1); 1375 | --color-neutral-muted: rgba(175, 184, 193, 0.2); 1376 | --color-accent-fg: #0969da; 1377 | --color-accent-emphasis: #0969da; 1378 | --color-attention-subtle: #fff8c5; 1379 | --color-danger-fg: #cf222e; 1380 | } 1381 | } 1382 | 1383 | .markdown-body { 1384 | -ms-text-size-adjust: 100%; 1385 | -webkit-text-size-adjust: 100%; 1386 | margin: 0; 1387 | color: var(--color-fg-default); 1388 | /* background-color: var(--color-canvas-default); */ 1389 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"; 1390 | font-size: 16px; 1391 | line-height: 1.5; 1392 | word-wrap: break-word; 1393 | } 1394 | 1395 | .markdown-body .octicon { 1396 | display: inline-block; 1397 | fill: currentColor; 1398 | vertical-align: text-bottom; 1399 | } 1400 | 1401 | .markdown-body h1:hover .anchor .octicon-link:before, 1402 | .markdown-body h2:hover .anchor .octicon-link:before, 1403 | .markdown-body h3:hover .anchor .octicon-link:before, 1404 | .markdown-body h4:hover .anchor .octicon-link:before, 1405 | .markdown-body h5:hover .anchor .octicon-link:before, 1406 | .markdown-body h6:hover .anchor .octicon-link:before { 1407 | width: 16px; 1408 | height: 16px; 1409 | content: ' '; 1410 | display: inline-block; 1411 | background-color: currentColor; 1412 | -webkit-mask-image: url("data:image/svg+xml,"); 1413 | mask-image: url("data:image/svg+xml,"); 1414 | } 1415 | 1416 | .markdown-body details, 1417 | .markdown-body figcaption, 1418 | .markdown-body figure { 1419 | display: block; 1420 | } 1421 | 1422 | .markdown-body summary { 1423 | display: list-item; 1424 | } 1425 | 1426 | .markdown-body [hidden] { 1427 | display: none !important; 1428 | } 1429 | 1430 | .markdown-body a { 1431 | background-color: transparent; 1432 | color: var(--color-accent-fg); 1433 | text-decoration: none; 1434 | } 1435 | 1436 | .markdown-body abbr[title] { 1437 | border-bottom: none; 1438 | -webkit-text-decoration: underline dotted; 1439 | text-decoration: underline dotted; 1440 | } 1441 | 1442 | .markdown-body b, 1443 | .markdown-body strong { 1444 | font-weight: var(--base-text-weight-semibold, 600); 1445 | } 1446 | 1447 | .markdown-body dfn { 1448 | font-style: italic; 1449 | } 1450 | 1451 | .markdown-body h1 { 1452 | margin: .67em 0; 1453 | font-weight: var(--base-text-weight-semibold, 600); 1454 | padding-bottom: .3em; 1455 | font-size: 2em; 1456 | border-bottom: 1px solid var(--color-border-muted); 1457 | } 1458 | 1459 | .markdown-body mark { 1460 | background-color: var(--color-attention-subtle); 1461 | color: var(--color-fg-default); 1462 | } 1463 | 1464 | .markdown-body small { 1465 | font-size: 90%; 1466 | } 1467 | 1468 | .markdown-body sub, 1469 | .markdown-body sup { 1470 | font-size: 75%; 1471 | line-height: 0; 1472 | position: relative; 1473 | vertical-align: baseline; 1474 | } 1475 | 1476 | .markdown-body sub { 1477 | bottom: -0.25em; 1478 | } 1479 | 1480 | .markdown-body sup { 1481 | top: -0.5em; 1482 | } 1483 | 1484 | .markdown-body img { 1485 | border-style: none; 1486 | max-width: 100%; 1487 | box-sizing: content-box; 1488 | background-color: var(--color-canvas-default); 1489 | } 1490 | 1491 | .markdown-body code, 1492 | .markdown-body kbd, 1493 | .markdown-body pre, 1494 | .markdown-body samp { 1495 | font-family: monospace; 1496 | font-size: 1em; 1497 | } 1498 | 1499 | .markdown-body figure { 1500 | margin: 1em 40px; 1501 | } 1502 | 1503 | .markdown-body hr { 1504 | box-sizing: content-box; 1505 | overflow: hidden; 1506 | background: transparent; 1507 | border-bottom: 1px solid var(--color-border-muted); 1508 | height: .25em; 1509 | padding: 0; 1510 | margin: 24px 0; 1511 | background-color: var(--color-border-default); 1512 | border: 0; 1513 | } 1514 | 1515 | .markdown-body input { 1516 | font: inherit; 1517 | margin: 0; 1518 | overflow: visible; 1519 | font-family: inherit; 1520 | font-size: inherit; 1521 | line-height: inherit; 1522 | } 1523 | 1524 | .markdown-body [type=button], 1525 | .markdown-body [type=reset], 1526 | .markdown-body [type=submit] { 1527 | -webkit-appearance: button; 1528 | } 1529 | 1530 | .markdown-body [type=checkbox], 1531 | .markdown-body [type=radio] { 1532 | box-sizing: border-box; 1533 | padding: 0; 1534 | } 1535 | 1536 | .markdown-body [type=number]::-webkit-inner-spin-button, 1537 | .markdown-body [type=number]::-webkit-outer-spin-button { 1538 | height: auto; 1539 | } 1540 | 1541 | .markdown-body [type=search]::-webkit-search-cancel-button, 1542 | .markdown-body [type=search]::-webkit-search-decoration { 1543 | -webkit-appearance: none; 1544 | } 1545 | 1546 | .markdown-body ::-webkit-input-placeholder { 1547 | color: inherit; 1548 | opacity: .54; 1549 | } 1550 | 1551 | .markdown-body ::-webkit-file-upload-button { 1552 | -webkit-appearance: button; 1553 | font: inherit; 1554 | } 1555 | 1556 | .markdown-body a:hover { 1557 | text-decoration: underline; 1558 | } 1559 | 1560 | .markdown-body ::-moz-placeholder { 1561 | color: var(--color-fg-subtle); 1562 | opacity: 1; 1563 | } 1564 | 1565 | .markdown-body ::placeholder { 1566 | color: var(--color-fg-subtle); 1567 | opacity: 1; 1568 | } 1569 | 1570 | .markdown-body hr::before { 1571 | display: table; 1572 | content: ""; 1573 | } 1574 | 1575 | .markdown-body hr::after { 1576 | display: table; 1577 | clear: both; 1578 | content: ""; 1579 | } 1580 | 1581 | .markdown-body table { 1582 | border-spacing: 0; 1583 | border-collapse: collapse; 1584 | display: block; 1585 | width: -moz-max-content; 1586 | width: max-content; 1587 | max-width: 100%; 1588 | overflow: auto; 1589 | } 1590 | 1591 | .markdown-body td, 1592 | .markdown-body th { 1593 | padding: 0; 1594 | } 1595 | 1596 | .markdown-body details summary { 1597 | cursor: pointer; 1598 | } 1599 | 1600 | .markdown-body details:not([open])>*:not(summary) { 1601 | display: none !important; 1602 | } 1603 | 1604 | .markdown-body a:focus, 1605 | .markdown-body [role=button]:focus, 1606 | .markdown-body input[type=radio]:focus, 1607 | .markdown-body input[type=checkbox]:focus { 1608 | outline: 2px solid var(--color-accent-fg); 1609 | outline-offset: -2px; 1610 | box-shadow: none; 1611 | } 1612 | 1613 | .markdown-body a:focus:not(:focus-visible), 1614 | .markdown-body [role=button]:focus:not(:focus-visible), 1615 | .markdown-body input[type=radio]:focus:not(:focus-visible), 1616 | .markdown-body input[type=checkbox]:focus:not(:focus-visible) { 1617 | outline: solid 1px transparent; 1618 | } 1619 | 1620 | .markdown-body a:focus-visible, 1621 | .markdown-body [role=button]:focus-visible, 1622 | .markdown-body input[type=radio]:focus-visible, 1623 | .markdown-body input[type=checkbox]:focus-visible { 1624 | outline: 2px solid var(--color-accent-fg); 1625 | outline-offset: -2px; 1626 | box-shadow: none; 1627 | } 1628 | 1629 | .markdown-body a:not([class]):focus, 1630 | .markdown-body a:not([class]):focus-visible, 1631 | .markdown-body input[type=radio]:focus, 1632 | .markdown-body input[type=radio]:focus-visible, 1633 | .markdown-body input[type=checkbox]:focus, 1634 | .markdown-body input[type=checkbox]:focus-visible { 1635 | outline-offset: 0; 1636 | } 1637 | 1638 | .markdown-body kbd { 1639 | display: inline-block; 1640 | padding: 3px 5px; 1641 | font: 11px ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace; 1642 | line-height: 10px; 1643 | color: var(--color-fg-default); 1644 | vertical-align: middle; 1645 | background-color: var(--color-canvas-subtle); 1646 | border: solid 1px var(--color-neutral-muted); 1647 | border-bottom-color: var(--color-neutral-muted); 1648 | border-radius: 6px; 1649 | box-shadow: inset 0 -1px 0 var(--color-neutral-muted); 1650 | } 1651 | 1652 | .markdown-body h1, 1653 | .markdown-body h2, 1654 | .markdown-body h3, 1655 | .markdown-body h4, 1656 | .markdown-body h5, 1657 | .markdown-body h6 { 1658 | margin-top: 24px; 1659 | margin-bottom: 16px; 1660 | font-weight: var(--base-text-weight-semibold, 600); 1661 | line-height: 1.25; 1662 | } 1663 | 1664 | .markdown-body h2 { 1665 | font-weight: var(--base-text-weight-semibold, 600); 1666 | padding-bottom: .3em; 1667 | font-size: 1.5em; 1668 | border-bottom: 1px solid var(--color-border-muted); 1669 | } 1670 | 1671 | .markdown-body h3 { 1672 | font-weight: var(--base-text-weight-semibold, 600); 1673 | font-size: 1.25em; 1674 | } 1675 | 1676 | .markdown-body h4 { 1677 | font-weight: var(--base-text-weight-semibold, 600); 1678 | font-size: 1em; 1679 | } 1680 | 1681 | .markdown-body h5 { 1682 | font-weight: var(--base-text-weight-semibold, 600); 1683 | font-size: .875em; 1684 | } 1685 | 1686 | .markdown-body h6 { 1687 | font-weight: var(--base-text-weight-semibold, 600); 1688 | font-size: .85em; 1689 | color: var(--color-fg-muted); 1690 | } 1691 | 1692 | .markdown-body p { 1693 | margin-top: 0; 1694 | margin-bottom: 10px; 1695 | } 1696 | 1697 | .markdown-body blockquote { 1698 | margin: 0; 1699 | padding: 0 1em; 1700 | color: var(--color-fg-muted); 1701 | border-left: .25em solid var(--color-border-default); 1702 | } 1703 | 1704 | .markdown-body ul, 1705 | .markdown-body ol { 1706 | margin-top: 0; 1707 | margin-bottom: 0; 1708 | padding-left: 1rem; 1709 | } 1710 | 1711 | .markdown-body ul { 1712 | list-style: disc; 1713 | } 1714 | 1715 | .markdown-body ol { 1716 | list-style: decimal; 1717 | } 1718 | 1719 | .markdown-body ol ol, 1720 | .markdown-body ul ol { 1721 | list-style: lower-roman; 1722 | } 1723 | 1724 | .markdown-body ul ul ol, 1725 | .markdown-body ul ol ol, 1726 | .markdown-body ol ul ol, 1727 | .markdown-body ol ol ol { 1728 | list-style: lower-alpha; 1729 | } 1730 | 1731 | .markdown-body dd { 1732 | margin-left: 0; 1733 | } 1734 | 1735 | .markdown-body tt, 1736 | .markdown-body code, 1737 | .markdown-body samp { 1738 | font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace; 1739 | font-size: 12px; 1740 | } 1741 | 1742 | .markdown-body .octicon { 1743 | display: inline-block; 1744 | overflow: visible !important; 1745 | vertical-align: text-bottom; 1746 | fill: currentColor; 1747 | } 1748 | 1749 | .markdown-body input::-webkit-outer-spin-button, 1750 | .markdown-body input::-webkit-inner-spin-button { 1751 | margin: 0; 1752 | -webkit-appearance: none; 1753 | appearance: none; 1754 | } 1755 | 1756 | .markdown-body::before { 1757 | display: table; 1758 | content: ""; 1759 | } 1760 | 1761 | .markdown-body::after { 1762 | display: table; 1763 | clear: both; 1764 | content: ""; 1765 | } 1766 | 1767 | .markdown-body>*:first-child { 1768 | margin-top: 0 !important; 1769 | } 1770 | 1771 | .markdown-body>*:last-child { 1772 | margin-bottom: 0 !important; 1773 | } 1774 | 1775 | .markdown-body a:not([href]) { 1776 | color: inherit; 1777 | text-decoration: none; 1778 | } 1779 | 1780 | .markdown-body .absent { 1781 | color: var(--color-danger-fg); 1782 | } 1783 | 1784 | .markdown-body .anchor { 1785 | float: left; 1786 | padding-right: 4px; 1787 | margin-left: -20px; 1788 | line-height: 1; 1789 | } 1790 | 1791 | .markdown-body .anchor:focus { 1792 | outline: none; 1793 | } 1794 | 1795 | .markdown-body p, 1796 | .markdown-body blockquote, 1797 | .markdown-body ul, 1798 | .markdown-body ol, 1799 | .markdown-body dl, 1800 | .markdown-body table, 1801 | .markdown-body>pre, 1802 | .markdown-body details { 1803 | margin-top: 0; 1804 | margin-bottom: 16px; 1805 | } 1806 | 1807 | .markdown-body blockquote>:first-child { 1808 | margin-top: 0; 1809 | } 1810 | 1811 | .markdown-body blockquote>:last-child { 1812 | margin-bottom: 0; 1813 | } 1814 | 1815 | .markdown-body h1 .octicon-link, 1816 | .markdown-body h2 .octicon-link, 1817 | .markdown-body h3 .octicon-link, 1818 | .markdown-body h4 .octicon-link, 1819 | .markdown-body h5 .octicon-link, 1820 | .markdown-body h6 .octicon-link { 1821 | color: var(--color-fg-default); 1822 | vertical-align: middle; 1823 | visibility: hidden; 1824 | } 1825 | 1826 | .markdown-body h1:hover .anchor, 1827 | .markdown-body h2:hover .anchor, 1828 | .markdown-body h3:hover .anchor, 1829 | .markdown-body h4:hover .anchor, 1830 | .markdown-body h5:hover .anchor, 1831 | .markdown-body h6:hover .anchor { 1832 | text-decoration: none; 1833 | } 1834 | 1835 | .markdown-body h1:hover .anchor .octicon-link, 1836 | .markdown-body h2:hover .anchor .octicon-link, 1837 | .markdown-body h3:hover .anchor .octicon-link, 1838 | .markdown-body h4:hover .anchor .octicon-link, 1839 | .markdown-body h5:hover .anchor .octicon-link, 1840 | .markdown-body h6:hover .anchor .octicon-link { 1841 | visibility: visible; 1842 | } 1843 | 1844 | .markdown-body h1 tt, 1845 | .markdown-body h1 code, 1846 | .markdown-body h2 tt, 1847 | .markdown-body h2 code, 1848 | .markdown-body h3 tt, 1849 | .markdown-body h3 code, 1850 | .markdown-body h4 tt, 1851 | .markdown-body h4 code, 1852 | .markdown-body h5 tt, 1853 | .markdown-body h5 code, 1854 | .markdown-body h6 tt, 1855 | .markdown-body h6 code { 1856 | padding: 0 .2em; 1857 | font-size: inherit; 1858 | } 1859 | 1860 | .markdown-body summary h1, 1861 | .markdown-body summary h2, 1862 | .markdown-body summary h3, 1863 | .markdown-body summary h4, 1864 | .markdown-body summary h5, 1865 | .markdown-body summary h6 { 1866 | display: inline-block; 1867 | } 1868 | 1869 | .markdown-body summary h1 .anchor, 1870 | .markdown-body summary h2 .anchor, 1871 | .markdown-body summary h3 .anchor, 1872 | .markdown-body summary h4 .anchor, 1873 | .markdown-body summary h5 .anchor, 1874 | .markdown-body summary h6 .anchor { 1875 | margin-left: -40px; 1876 | } 1877 | 1878 | .markdown-body summary h1, 1879 | .markdown-body summary h2 { 1880 | padding-bottom: 0; 1881 | border-bottom: 0; 1882 | } 1883 | 1884 | .markdown-body ul.no-list, 1885 | .markdown-body ol.no-list { 1886 | padding: 0; 1887 | list-style: none; 1888 | } 1889 | 1890 | .markdown-body ol[type=a] { 1891 | list-style: lower-alpha; 1892 | } 1893 | 1894 | .markdown-body ol[type=A] { 1895 | list-style: upper-alpha; 1896 | } 1897 | 1898 | .markdown-body ol[type=i] { 1899 | list-style: lower-roman; 1900 | } 1901 | 1902 | .markdown-body ol[type=I] { 1903 | list-style: upper-roman; 1904 | } 1905 | 1906 | .markdown-body ol[type="1"] { 1907 | list-style: decimal; 1908 | } 1909 | 1910 | .markdown-body div>ol:not([type]) { 1911 | list-style: decimal; 1912 | } 1913 | 1914 | .markdown-body ul ul, 1915 | .markdown-body ul ol, 1916 | .markdown-body ol ol, 1917 | .markdown-body ol ul { 1918 | margin-top: 0; 1919 | margin-bottom: 0; 1920 | } 1921 | 1922 | .markdown-body li>p { 1923 | margin-top: 16px; 1924 | } 1925 | 1926 | .markdown-body li+li { 1927 | margin-top: .25em; 1928 | } 1929 | 1930 | .markdown-body dl { 1931 | padding: 0; 1932 | } 1933 | 1934 | .markdown-body dl dt { 1935 | padding: 0; 1936 | margin-top: 16px; 1937 | font-size: 1em; 1938 | font-style: italic; 1939 | font-weight: var(--base-text-weight-semibold, 600); 1940 | } 1941 | 1942 | .markdown-body dl dd { 1943 | padding: 0 16px; 1944 | margin-bottom: 16px; 1945 | } 1946 | 1947 | .markdown-body table th { 1948 | font-weight: var(--base-text-weight-semibold, 600); 1949 | } 1950 | 1951 | .markdown-body table th, 1952 | .markdown-body table td { 1953 | padding: 6px 13px; 1954 | border: 1px solid var(--color-border-default); 1955 | } 1956 | 1957 | .markdown-body table tr { 1958 | background-color: var(--color-canvas-default); 1959 | border-top: 1px solid var(--color-border-muted); 1960 | } 1961 | 1962 | .markdown-body table tr:nth-child(2n) { 1963 | background-color: var(--color-canvas-subtle); 1964 | } 1965 | 1966 | .markdown-body table img { 1967 | background-color: transparent; 1968 | } 1969 | 1970 | .markdown-body img[align=right] { 1971 | padding-left: 20px; 1972 | } 1973 | 1974 | .markdown-body img[align=left] { 1975 | padding-right: 20px; 1976 | } 1977 | 1978 | .markdown-body .emoji { 1979 | max-width: none; 1980 | vertical-align: text-top; 1981 | background-color: transparent; 1982 | } 1983 | 1984 | .markdown-body span.frame { 1985 | display: block; 1986 | overflow: hidden; 1987 | } 1988 | 1989 | .markdown-body span.frame>span { 1990 | display: block; 1991 | float: left; 1992 | width: auto; 1993 | padding: 7px; 1994 | margin: 13px 0 0; 1995 | overflow: hidden; 1996 | border: 1px solid var(--color-border-default); 1997 | } 1998 | 1999 | .markdown-body span.frame span img { 2000 | display: block; 2001 | float: left; 2002 | } 2003 | 2004 | .markdown-body span.frame span span { 2005 | display: block; 2006 | padding: 5px 0 0; 2007 | clear: both; 2008 | color: var(--color-fg-default); 2009 | } 2010 | 2011 | .markdown-body span.align-center { 2012 | display: block; 2013 | overflow: hidden; 2014 | clear: both; 2015 | } 2016 | 2017 | .markdown-body span.align-center>span { 2018 | display: block; 2019 | margin: 13px auto 0; 2020 | overflow: hidden; 2021 | text-align: center; 2022 | } 2023 | 2024 | .markdown-body span.align-center span img { 2025 | margin: 0 auto; 2026 | text-align: center; 2027 | } 2028 | 2029 | .markdown-body span.align-right { 2030 | display: block; 2031 | overflow: hidden; 2032 | clear: both; 2033 | } 2034 | 2035 | .markdown-body span.align-right>span { 2036 | display: block; 2037 | margin: 13px 0 0; 2038 | overflow: hidden; 2039 | text-align: right; 2040 | } 2041 | 2042 | .markdown-body span.align-right span img { 2043 | margin: 0; 2044 | text-align: right; 2045 | } 2046 | 2047 | .markdown-body span.float-left { 2048 | display: block; 2049 | float: left; 2050 | margin-right: 13px; 2051 | overflow: hidden; 2052 | } 2053 | 2054 | .markdown-body span.float-left span { 2055 | margin: 13px 0 0; 2056 | } 2057 | 2058 | .markdown-body span.float-right { 2059 | display: block; 2060 | float: right; 2061 | margin-left: 13px; 2062 | overflow: hidden; 2063 | } 2064 | 2065 | .markdown-body span.float-right>span { 2066 | display: block; 2067 | margin: 13px auto 0; 2068 | overflow: hidden; 2069 | text-align: right; 2070 | } 2071 | 2072 | .markdown-body code, 2073 | .markdown-body tt { 2074 | padding: .2em .4em; 2075 | margin: 0; 2076 | font-size: 85%; 2077 | white-space: break-spaces; 2078 | background-color: var(--color-neutral-muted); 2079 | border-radius: 6px; 2080 | } 2081 | 2082 | .markdown-body code br, 2083 | .markdown-body tt br { 2084 | display: none; 2085 | } 2086 | 2087 | .markdown-body del code { 2088 | text-decoration: inherit; 2089 | } 2090 | 2091 | .markdown-body samp { 2092 | font-size: 85%; 2093 | } 2094 | 2095 | .markdown-body pre code { 2096 | font-size: 100%; 2097 | } 2098 | 2099 | .markdown-body pre>code { 2100 | padding: 0; 2101 | margin: 0; 2102 | word-break: normal; 2103 | white-space: pre; 2104 | background: transparent; 2105 | border: 0; 2106 | } 2107 | 2108 | .markdown-body .highlight { 2109 | margin-bottom: 16px; 2110 | } 2111 | 2112 | .markdown-body .highlight pre { 2113 | margin-bottom: 0; 2114 | word-break: normal; 2115 | } 2116 | 2117 | .markdown-body .highlight pre, 2118 | .markdown-body pre { 2119 | font-size: 0.875rem; 2120 | position: relative; 2121 | } 2122 | 2123 | .markdown-body pre code, 2124 | .markdown-body pre tt { 2125 | display: inline; 2126 | max-width: auto; 2127 | padding: 0; 2128 | margin: 0; 2129 | overflow: visible; 2130 | line-height: inherit; 2131 | word-wrap: normal; 2132 | background-color: transparent; 2133 | border: 0; 2134 | } 2135 | 2136 | .markdown-body .csv-data td, 2137 | .markdown-body .csv-data th { 2138 | padding: 5px; 2139 | overflow: hidden; 2140 | font-size: 12px; 2141 | line-height: 1; 2142 | text-align: left; 2143 | white-space: nowrap; 2144 | } 2145 | 2146 | .markdown-body .csv-data .blob-num { 2147 | padding: 10px 8px 9px; 2148 | text-align: right; 2149 | background: var(--color-canvas-default); 2150 | border: 0; 2151 | } 2152 | 2153 | .markdown-body .csv-data tr { 2154 | border-top: 0; 2155 | } 2156 | 2157 | .markdown-body .csv-data th { 2158 | font-weight: var(--base-text-weight-semibold, 600); 2159 | background: var(--color-canvas-subtle); 2160 | border-top: 0; 2161 | } 2162 | 2163 | .markdown-body [data-footnote-ref]::before { 2164 | content: "["; 2165 | } 2166 | 2167 | .markdown-body [data-footnote-ref]::after { 2168 | content: "]"; 2169 | } 2170 | 2171 | .markdown-body .footnotes { 2172 | font-size: 12px; 2173 | color: var(--color-fg-muted); 2174 | border-top: 1px solid var(--color-border-default); 2175 | } 2176 | 2177 | .markdown-body .footnotes ol { 2178 | padding-left: 16px; 2179 | } 2180 | 2181 | .markdown-body .footnotes ol ul { 2182 | display: inline-block; 2183 | padding-left: 16px; 2184 | margin-top: 16px; 2185 | } 2186 | 2187 | .markdown-body .footnotes li { 2188 | position: relative; 2189 | } 2190 | 2191 | .markdown-body .footnotes li:target::before { 2192 | position: absolute; 2193 | top: -8px; 2194 | right: -8px; 2195 | bottom: -8px; 2196 | left: -24px; 2197 | pointer-events: none; 2198 | content: ""; 2199 | border: 2px solid var(--color-accent-emphasis); 2200 | border-radius: 6px; 2201 | } 2202 | 2203 | .markdown-body .footnotes li:target { 2204 | color: var(--color-fg-default); 2205 | } 2206 | 2207 | .markdown-body .footnotes .data-footnote-backref g-emoji { 2208 | font-family: monospace; 2209 | } 2210 | 2211 | .markdown-body .pl-c { 2212 | color: var(--color-prettylights-syntax-comment); 2213 | } 2214 | 2215 | .markdown-body .pl-c1, 2216 | .markdown-body .pl-s .pl-v { 2217 | color: var(--color-prettylights-syntax-constant); 2218 | } 2219 | 2220 | .markdown-body .pl-e, 2221 | .markdown-body .pl-en { 2222 | color: var(--color-prettylights-syntax-entity); 2223 | } 2224 | 2225 | .markdown-body .pl-smi, 2226 | .markdown-body .pl-s .pl-s1 { 2227 | color: var(--color-prettylights-syntax-storage-modifier-import); 2228 | } 2229 | 2230 | .markdown-body .pl-ent { 2231 | color: var(--color-prettylights-syntax-entity-tag); 2232 | } 2233 | 2234 | .markdown-body .pl-k { 2235 | color: var(--color-prettylights-syntax-keyword); 2236 | } 2237 | 2238 | .markdown-body .pl-s, 2239 | .markdown-body .pl-pds, 2240 | .markdown-body .pl-s .pl-pse .pl-s1, 2241 | .markdown-body .pl-sr, 2242 | .markdown-body .pl-sr .pl-cce, 2243 | .markdown-body .pl-sr .pl-sre, 2244 | .markdown-body .pl-sr .pl-sra { 2245 | color: var(--color-prettylights-syntax-string); 2246 | } 2247 | 2248 | .markdown-body .pl-v, 2249 | .markdown-body .pl-smw { 2250 | color: var(--color-prettylights-syntax-variable); 2251 | } 2252 | 2253 | .markdown-body .pl-bu { 2254 | color: var(--color-prettylights-syntax-brackethighlighter-unmatched); 2255 | } 2256 | 2257 | .markdown-body .pl-ii { 2258 | color: var(--color-prettylights-syntax-invalid-illegal-text); 2259 | background-color: var(--color-prettylights-syntax-invalid-illegal-bg); 2260 | } 2261 | 2262 | .markdown-body .pl-c2 { 2263 | color: var(--color-prettylights-syntax-carriage-return-text); 2264 | background-color: var(--color-prettylights-syntax-carriage-return-bg); 2265 | } 2266 | 2267 | .markdown-body .pl-sr .pl-cce { 2268 | font-weight: bold; 2269 | color: var(--color-prettylights-syntax-string-regexp); 2270 | } 2271 | 2272 | .markdown-body .pl-ml { 2273 | color: var(--color-prettylights-syntax-markup-list); 2274 | } 2275 | 2276 | .markdown-body .pl-mh, 2277 | .markdown-body .pl-mh .pl-en, 2278 | .markdown-body .pl-ms { 2279 | font-weight: bold; 2280 | color: var(--color-prettylights-syntax-markup-heading); 2281 | } 2282 | 2283 | .markdown-body .pl-mi { 2284 | font-style: italic; 2285 | color: var(--color-prettylights-syntax-markup-italic); 2286 | } 2287 | 2288 | .markdown-body .pl-mb { 2289 | font-weight: bold; 2290 | color: var(--color-prettylights-syntax-markup-bold); 2291 | } 2292 | 2293 | .markdown-body .pl-md { 2294 | color: var(--color-prettylights-syntax-markup-deleted-text); 2295 | background-color: var(--color-prettylights-syntax-markup-deleted-bg); 2296 | } 2297 | 2298 | .markdown-body .pl-mi1 { 2299 | color: var(--color-prettylights-syntax-markup-inserted-text); 2300 | background-color: var(--color-prettylights-syntax-markup-inserted-bg); 2301 | } 2302 | 2303 | .markdown-body .pl-mc { 2304 | color: var(--color-prettylights-syntax-markup-changed-text); 2305 | background-color: var(--color-prettylights-syntax-markup-changed-bg); 2306 | } 2307 | 2308 | .markdown-body .pl-mi2 { 2309 | color: var(--color-prettylights-syntax-markup-ignored-text); 2310 | background-color: var(--color-prettylights-syntax-markup-ignored-bg); 2311 | } 2312 | 2313 | .markdown-body .pl-mdr { 2314 | font-weight: bold; 2315 | color: var(--color-prettylights-syntax-meta-diff-range); 2316 | } 2317 | 2318 | .markdown-body .pl-ba { 2319 | color: var(--color-prettylights-syntax-brackethighlighter-angle); 2320 | } 2321 | 2322 | .markdown-body .pl-sg { 2323 | color: var(--color-prettylights-syntax-sublimelinter-gutter-mark); 2324 | } 2325 | 2326 | .markdown-body .pl-corl { 2327 | text-decoration: underline; 2328 | color: var(--color-prettylights-syntax-constant-other-reference-link); 2329 | } 2330 | 2331 | .markdown-body g-emoji { 2332 | display: inline-block; 2333 | min-width: 1ch; 2334 | font-family: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; 2335 | font-size: 1em; 2336 | font-style: normal !important; 2337 | font-weight: var(--base-text-weight-normal, 400); 2338 | line-height: 1; 2339 | vertical-align: -0.075em; 2340 | } 2341 | 2342 | .markdown-body g-emoji img { 2343 | width: 1em; 2344 | height: 1em; 2345 | } 2346 | 2347 | .markdown-body .task-list-item { 2348 | list-style: none; 2349 | } 2350 | 2351 | .markdown-body .task-list-item label { 2352 | font-weight: var(--base-text-weight-normal, 400); 2353 | } 2354 | 2355 | .markdown-body .task-list-item.enabled label { 2356 | cursor: pointer; 2357 | } 2358 | 2359 | .markdown-body .task-list-item+.task-list-item { 2360 | margin-top: 4px; 2361 | } 2362 | 2363 | .markdown-body .task-list-item .handle { 2364 | display: none; 2365 | } 2366 | 2367 | .markdown-body .task-list-item-checkbox { 2368 | margin: 0 .2em .25em -1.4em; 2369 | vertical-align: middle; 2370 | } 2371 | 2372 | .markdown-body .contains-task-list:dir(rtl) .task-list-item-checkbox { 2373 | margin: 0 -1.6em .25em .2em; 2374 | } 2375 | 2376 | .markdown-body .contains-task-list { 2377 | position: relative; 2378 | } 2379 | 2380 | .markdown-body .contains-task-list:hover .task-list-item-convert-container, 2381 | .markdown-body .contains-task-list:focus-within .task-list-item-convert-container { 2382 | display: block; 2383 | width: auto; 2384 | height: 24px; 2385 | overflow: visible; 2386 | clip: auto; 2387 | } 2388 | 2389 | .markdown-body ::-webkit-calendar-picker-indicator { 2390 | filter: invert(50%); 2391 | } 2392 | 2393 | .hover\:bg-cyan-700:hover { 2394 | --tw-bg-opacity: 1; 2395 | background-color: rgb(14 116 144 / var(--tw-bg-opacity)); 2396 | } 2397 | 2398 | .hover\:bg-gray-200:hover { 2399 | --tw-bg-opacity: 1; 2400 | background-color: rgb(206 211 222 / var(--tw-bg-opacity)); 2401 | } 2402 | 2403 | .hover\:bg-gray-300\/20:hover { 2404 | background-color: rgb(175 183 202 / 0.2); 2405 | } 2406 | 2407 | .hover\:bg-gray-300\/50:hover { 2408 | background-color: rgb(175 183 202 / 0.5); 2409 | } 2410 | 2411 | .hover\:text-white:hover { 2412 | --tw-text-opacity: 1; 2413 | color: rgb(255 255 255 / var(--tw-text-opacity)); 2414 | } 2415 | 2416 | .focus\:border-cyan-600:focus { 2417 | --tw-border-opacity: 1; 2418 | border-color: rgb(8 145 178 / var(--tw-border-opacity)); 2419 | } 2420 | 2421 | .focus\:outline-none:focus { 2422 | outline: 2px solid transparent; 2423 | outline-offset: 2px; 2424 | } 2425 | 2426 | .focus\:ring-1:focus { 2427 | --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); 2428 | --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color); 2429 | box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); 2430 | } 2431 | 2432 | .focus\:ring-cyan-600:focus { 2433 | --tw-ring-opacity: 1; 2434 | --tw-ring-color: rgb(8 145 178 / var(--tw-ring-opacity)); 2435 | } 2436 | 2437 | .enabled\:hover\:bg-gray-200\/30:hover:enabled { 2438 | background-color: rgb(206 211 222 / 0.3); 2439 | } 2440 | 2441 | .enabled\:hover\:text-gray-700:hover:enabled { 2442 | --tw-text-opacity: 1; 2443 | color: rgb(67 77 101 / var(--tw-text-opacity)); 2444 | } 2445 | 2446 | .disabled\:opacity-50:disabled { 2447 | opacity: 0.5; 2448 | } 2449 | 2450 | .disabled\:opacity-75:disabled { 2451 | opacity: 0.75; 2452 | } 2453 | 2454 | .group:hover .group-hover\:bg-white { 2455 | --tw-bg-opacity: 1; 2456 | background-color: rgb(255 255 255 / var(--tw-bg-opacity)); 2457 | } 2458 | 2459 | .group:hover .group-hover\:text-cyan-600 { 2460 | --tw-text-opacity: 1; 2461 | color: rgb(8 145 178 / var(--tw-text-opacity)); 2462 | } 2463 | 2464 | @media (prefers-color-scheme: dark) { 2465 | .dark\:block { 2466 | display: block; 2467 | } 2468 | 2469 | .dark\:divide-gray-700\/50 > :not([hidden]) ~ :not([hidden]) { 2470 | border-color: rgb(67 77 101 / 0.5); 2471 | } 2472 | 2473 | .dark\:border-none { 2474 | border-style: none; 2475 | } 2476 | 2477 | .dark\:border-gray-200\/10 { 2478 | border-color: rgb(206 211 222 / 0.1); 2479 | } 2480 | 2481 | .dark\:border-gray-700\/40 { 2482 | border-color: rgb(67 77 101 / 0.4); 2483 | } 2484 | 2485 | .dark\:bg-black\/20 { 2486 | background-color: rgb(0 0 0 / 0.2); 2487 | } 2488 | 2489 | .dark\:bg-gray-700\/75 { 2490 | background-color: rgb(67 77 101 / 0.75); 2491 | } 2492 | 2493 | .dark\:bg-gray-800\/50 { 2494 | background-color: rgb(47 54 70 / 0.5); 2495 | } 2496 | 2497 | .dark\:bg-gray-900 { 2498 | --tw-bg-opacity: 1; 2499 | background-color: rgb(26 30 39 / var(--tw-bg-opacity)); 2500 | } 2501 | 2502 | .dark\:bg-gray-900\/75 { 2503 | background-color: rgb(26 30 39 / 0.75); 2504 | } 2505 | 2506 | .dark\:bg-gray-950 { 2507 | --tw-bg-opacity: 1; 2508 | background-color: rgb(22 26 34 / var(--tw-bg-opacity)); 2509 | } 2510 | 2511 | .dark\:bg-gray-950\/30 { 2512 | background-color: rgb(22 26 34 / 0.3); 2513 | } 2514 | 2515 | .dark\:bg-white { 2516 | --tw-bg-opacity: 1; 2517 | background-color: rgb(255 255 255 / var(--tw-bg-opacity)); 2518 | } 2519 | 2520 | .dark\:bg-white\/10 { 2521 | background-color: rgb(255 255 255 / 0.1); 2522 | } 2523 | 2524 | .dark\:bg-white\/5 { 2525 | background-color: rgb(255 255 255 / 0.05); 2526 | } 2527 | 2528 | .dark\:from-gray-900 { 2529 | --tw-gradient-from: #1A1E27 var(--tw-gradient-from-position); 2530 | --tw-gradient-to: rgb(26 30 39 / 0) var(--tw-gradient-to-position); 2531 | --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to); 2532 | } 2533 | 2534 | .dark\:to-gray-900 { 2535 | --tw-gradient-to: #1A1E27 var(--tw-gradient-to-position); 2536 | } 2537 | 2538 | .dark\:fill-gray-900 { 2539 | fill: #1A1E27; 2540 | } 2541 | 2542 | .dark\:fill-white { 2543 | fill: #fff; 2544 | } 2545 | 2546 | .dark\:text-gray-100 { 2547 | --tw-text-opacity: 1; 2548 | color: rgb(240 241 245 / var(--tw-text-opacity)); 2549 | } 2550 | 2551 | .dark\:text-gray-300\/50 { 2552 | color: rgb(175 183 202 / 0.5); 2553 | } 2554 | 2555 | .dark\:text-gray-300\/80 { 2556 | color: rgb(175 183 202 / 0.8); 2557 | } 2558 | 2559 | .dark\:text-white { 2560 | --tw-text-opacity: 1; 2561 | color: rgb(255 255 255 / var(--tw-text-opacity)); 2562 | } 2563 | 2564 | .dark\:text-white\/20 { 2565 | color: rgb(255 255 255 / 0.2); 2566 | } 2567 | 2568 | .dark\:text-white\/40 { 2569 | color: rgb(255 255 255 / 0.4); 2570 | } 2571 | 2572 | .dark\:placeholder-gray-300\/40::-moz-placeholder { 2573 | color: rgb(175 183 202 / 0.4); 2574 | } 2575 | 2576 | .dark\:placeholder-gray-300\/40::placeholder { 2577 | color: rgb(175 183 202 / 0.4); 2578 | } 2579 | 2580 | .dark\:hover\:bg-gray-800\/60:hover { 2581 | background-color: rgb(47 54 70 / 0.6); 2582 | } 2583 | 2584 | .dark\:hover\:bg-gray-900:hover { 2585 | --tw-bg-opacity: 1; 2586 | background-color: rgb(26 30 39 / var(--tw-bg-opacity)); 2587 | } 2588 | 2589 | .dark\:hover\:bg-white\/20:hover { 2590 | background-color: rgb(255 255 255 / 0.2); 2591 | } 2592 | 2593 | .dark\:enabled\:hover\:bg-gray-800\/40:hover:enabled { 2594 | background-color: rgb(47 54 70 / 0.4); 2595 | } 2596 | 2597 | .dark\:enabled\:hover\:text-gray-100:hover:enabled { 2598 | --tw-text-opacity: 1; 2599 | color: rgb(240 241 245 / var(--tw-text-opacity)); 2600 | } 2601 | } 2602 | 2603 | @media (min-width: 1024px) { 2604 | .lg\:max-w-\[85\%\] { 2605 | max-width: 85%; 2606 | } 2607 | } 2608 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chat-ollama", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev-css": "npx tailwindcss -i ./src/css/style.css -o ./public/style.css --watch", 8 | "dev-app": "shadow-cljs watch app", 9 | "dev": "concurrently \"npm:dev-app\" \"npm:dev-css\"", 10 | "build-app": "shadow-cljs release app", 11 | "build-clean": "rimraf dist", 12 | "build-copy": "cp public/index.html public/favicon.ico public/license.txt dist/ && mkdir dist/assets && cp public/assets/* dist/assets && awk 'BEGIN {print \"/*! For license information please see LICENSE.txt */\"} {print}' dist/js/main.js > temp && mv temp dist/js/main.js", 13 | "build-css": "npx tailwindcss -i ./src/css/style.css -o ./dist/style.css", 14 | "build": "npm run build-clean && npm run build-css && npm run build-app && npm run build-copy", 15 | "serve": "npx serve dist -L -s -n -p 1420", 16 | "shadow-cljs": "shadow-cljs" 17 | }, 18 | "devDependencies": { 19 | "concurrently": "^9.1.2", 20 | "rimraf": "^6.0.1", 21 | "serve": "^14.2.4", 22 | "shadow-cljs": "^2.28.22", 23 | "tailwindcss": "^3.3.3" 24 | }, 25 | "dependencies": { 26 | "@react-spring/web": "^9.7.5", 27 | "date-fns": "^4.1.0", 28 | "lucide-react": "^0.487.0", 29 | "react": "^18.2.0", 30 | "react-dom": "^18.2.0", 31 | "react-hotkeys-hook": "^4.6.1", 32 | "react-markdown": "^10.1.0", 33 | "react-refresh": "^0.17.0", 34 | "react-syntax-highlighter": "^15.6.1", 35 | "use-sync-external-store": "^1.5.0" 36 | }, 37 | "overrides": { 38 | "browserify-sign": "^4.2.2", 39 | "postcss": "^8.4.31", 40 | "prismjs": "^1.30.0" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /public/assets/android-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathandale/chat-ollama/077e18cc11975d0bdeee66d0b25957c080b2b667/public/assets/android-icon-144x144.png -------------------------------------------------------------------------------- /public/assets/android-icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathandale/chat-ollama/077e18cc11975d0bdeee66d0b25957c080b2b667/public/assets/android-icon-192x192.png -------------------------------------------------------------------------------- /public/assets/android-icon-36x36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathandale/chat-ollama/077e18cc11975d0bdeee66d0b25957c080b2b667/public/assets/android-icon-36x36.png -------------------------------------------------------------------------------- /public/assets/android-icon-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathandale/chat-ollama/077e18cc11975d0bdeee66d0b25957c080b2b667/public/assets/android-icon-48x48.png -------------------------------------------------------------------------------- /public/assets/android-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathandale/chat-ollama/077e18cc11975d0bdeee66d0b25957c080b2b667/public/assets/android-icon-72x72.png -------------------------------------------------------------------------------- /public/assets/android-icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathandale/chat-ollama/077e18cc11975d0bdeee66d0b25957c080b2b667/public/assets/android-icon-96x96.png -------------------------------------------------------------------------------- /public/assets/apple-icon-114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathandale/chat-ollama/077e18cc11975d0bdeee66d0b25957c080b2b667/public/assets/apple-icon-114x114.png -------------------------------------------------------------------------------- /public/assets/apple-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathandale/chat-ollama/077e18cc11975d0bdeee66d0b25957c080b2b667/public/assets/apple-icon-120x120.png -------------------------------------------------------------------------------- /public/assets/apple-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathandale/chat-ollama/077e18cc11975d0bdeee66d0b25957c080b2b667/public/assets/apple-icon-144x144.png -------------------------------------------------------------------------------- /public/assets/apple-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathandale/chat-ollama/077e18cc11975d0bdeee66d0b25957c080b2b667/public/assets/apple-icon-152x152.png -------------------------------------------------------------------------------- /public/assets/apple-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathandale/chat-ollama/077e18cc11975d0bdeee66d0b25957c080b2b667/public/assets/apple-icon-180x180.png -------------------------------------------------------------------------------- /public/assets/apple-icon-57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathandale/chat-ollama/077e18cc11975d0bdeee66d0b25957c080b2b667/public/assets/apple-icon-57x57.png -------------------------------------------------------------------------------- /public/assets/apple-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathandale/chat-ollama/077e18cc11975d0bdeee66d0b25957c080b2b667/public/assets/apple-icon-60x60.png -------------------------------------------------------------------------------- /public/assets/apple-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathandale/chat-ollama/077e18cc11975d0bdeee66d0b25957c080b2b667/public/assets/apple-icon-72x72.png -------------------------------------------------------------------------------- /public/assets/apple-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathandale/chat-ollama/077e18cc11975d0bdeee66d0b25957c080b2b667/public/assets/apple-icon-76x76.png -------------------------------------------------------------------------------- /public/assets/apple-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathandale/chat-ollama/077e18cc11975d0bdeee66d0b25957c080b2b667/public/assets/apple-icon-precomposed.png -------------------------------------------------------------------------------- /public/assets/apple-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathandale/chat-ollama/077e18cc11975d0bdeee66d0b25957c080b2b667/public/assets/apple-icon.png -------------------------------------------------------------------------------- /public/assets/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | #ffffff -------------------------------------------------------------------------------- /public/assets/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathandale/chat-ollama/077e18cc11975d0bdeee66d0b25957c080b2b667/public/assets/favicon-16x16.png -------------------------------------------------------------------------------- /public/assets/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathandale/chat-ollama/077e18cc11975d0bdeee66d0b25957c080b2b667/public/assets/favicon-32x32.png -------------------------------------------------------------------------------- /public/assets/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathandale/chat-ollama/077e18cc11975d0bdeee66d0b25957c080b2b667/public/assets/favicon-96x96.png -------------------------------------------------------------------------------- /public/assets/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "App", 3 | "icons": [ 4 | { 5 | "src": "\/android-icon-36x36.png", 6 | "sizes": "36x36", 7 | "type": "image\/png", 8 | "density": "0.75" 9 | }, 10 | { 11 | "src": "\/android-icon-48x48.png", 12 | "sizes": "48x48", 13 | "type": "image\/png", 14 | "density": "1.0" 15 | }, 16 | { 17 | "src": "\/android-icon-72x72.png", 18 | "sizes": "72x72", 19 | "type": "image\/png", 20 | "density": "1.5" 21 | }, 22 | { 23 | "src": "\/android-icon-96x96.png", 24 | "sizes": "96x96", 25 | "type": "image\/png", 26 | "density": "2.0" 27 | }, 28 | { 29 | "src": "\/android-icon-144x144.png", 30 | "sizes": "144x144", 31 | "type": "image\/png", 32 | "density": "3.0" 33 | }, 34 | { 35 | "src": "\/android-icon-192x192.png", 36 | "sizes": "192x192", 37 | "type": "image\/png", 38 | "density": "4.0" 39 | } 40 | ] 41 | } -------------------------------------------------------------------------------- /public/assets/ms-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathandale/chat-ollama/077e18cc11975d0bdeee66d0b25957c080b2b667/public/assets/ms-icon-144x144.png -------------------------------------------------------------------------------- /public/assets/ms-icon-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathandale/chat-ollama/077e18cc11975d0bdeee66d0b25957c080b2b667/public/assets/ms-icon-150x150.png -------------------------------------------------------------------------------- /public/assets/ms-icon-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathandale/chat-ollama/077e18cc11975d0bdeee66d0b25957c080b2b667/public/assets/ms-icon-310x310.png -------------------------------------------------------------------------------- /public/assets/ms-icon-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathandale/chat-ollama/077e18cc11975d0bdeee66d0b25957c080b2b667/public/assets/ms-icon-70x70.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathandale/chat-ollama/077e18cc11975d0bdeee66d0b25957c080b2b667/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Chat Ollama 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 |
30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /public/license.txt: -------------------------------------------------------------------------------- 1 | # Licenses 2 | 3 | ## @react-spring/web 4 | 5 | MIT License 6 | 7 | Copyright (c) 2018-present Paul Henschel, react-spring, all contributors 8 | 9 | https://opensource.org/license/mit/ 10 | 11 | ## date-fns 12 | 13 | MIT License 14 | 15 | Copyright (c) 2021 Sasha Koss and Lesha Koss https://kossnocorp.mit-license.org 16 | 17 | https://opensource.org/license/mit/ 18 | 19 | ## lucide-react 20 | 21 | ISC License 22 | 23 | Copyright (c) for portions of Lucide are held by Cole Bemis 2013-2022 as part of Feather (MIT). All other copyright (c) for Lucide are held by Lucide Contributors 2022. 24 | 25 | https://opensource.org/license/isc-license-txt/ 26 | 27 | ## react 28 | 29 | MIT License 30 | 31 | Copyright (c) Facebook, Inc. and its affiliates. 32 | 33 | https://opensource.org/license/mit/ 34 | 35 | ## react-dom 36 | 37 | MIT License 38 | 39 | Copyright (c) Facebook, Inc. and its affiliates. 40 | 41 | https://opensource.org/license/mit/ 42 | 43 | ## react-hotkeys-hook 44 | 45 | MIT License 46 | 47 | Copyright (c) 2018 Johannes Klauss 48 | 49 | https://opensource.org/license/mit/ 50 | 51 | ## react-markdown 52 | 53 | The MIT License (MIT) 54 | 55 | Copyright (c) 2015 Espen Hovlandsdal 56 | 57 | https://opensource.org/license/mit/ 58 | 59 | ## react-refresh 60 | 61 | MIT License 62 | 63 | Copyright (c) Facebook, Inc. and its affiliates. 64 | 65 | https://opensource.org/license/mit/ 66 | 67 | ## react-syntax-highlighter 68 | 69 | MIT License 70 | 71 | Copyright (c) 2019 Conor Hastings 72 | 73 | https://opensource.org/license/mit/ 74 | 75 | ## use-sync-external-store 76 | 77 | MIT License 78 | 79 | Copyright (c) Facebook, Inc. and its affiliates. 80 | 81 | https://opensource.org/license/mit/ 82 | 83 | ## lilactown/helix "0.1.10" 84 | 85 | Eclipse Public License - v 2.0 86 | 87 | https://opensource.org/license/epl-2-0/ 88 | 89 | ## com.fbeyer/refx "0.0.49" 90 | 91 | MIT License 92 | 93 | Copyright (c) 2022 Ferdinand Beyer 94 | 95 | https://opensource.org/license/mit/ 96 | 97 | ## applied-science/js-interop "0.4.2" 98 | 99 | Eclipse Public License - v 2.0 100 | 101 | https://opensource.org/license/epl-2-0/ 102 | 103 | ## cljs-bean "1.9.0" 104 | 105 | Eclipse Public License - v 1.0 106 | 107 | https://opensource.org/license/epl-1-0/ 108 | 109 | ## funcool/promesa "11.0.678" 110 | 111 | Mozilla Public License Version 2.0 112 | 113 | https://opensource.org/license/mpl-2-0/ 114 | 115 | ## com.cognitect/transit-cljs "0.8.280" 116 | 117 | Apache License 118 | Version 2.0, January 2004 119 | http://www.apache.org/licenses/ 120 | -------------------------------------------------------------------------------- /shadow-cljs.edn: -------------------------------------------------------------------------------- 1 | {:source-paths ["src/cljs"] 2 | 3 | :dependencies [[lilactown/helix "0.1.10"] 4 | [com.fbeyer/refx "0.0.49"] 5 | [applied-science/js-interop "0.4.2"] 6 | [cljs-bean "1.9.0"] 7 | [funcool/promesa "11.0.678"] 8 | [com.cognitect/transit-cljs "0.8.280"]] 9 | 10 | :dev-http {1420 "public"} 11 | 12 | :builds {:app {:target :browser 13 | :output-dir "public/js" 14 | :asset-path "/js" 15 | :modules {:main {:init-fn chat-ollama.core/main}} 16 | :release {:output-dir "dist/js"}}}} 17 | -------------------------------------------------------------------------------- /src/cljs/chat_ollama/core.cljs: -------------------------------------------------------------------------------- 1 | (ns ^:dev/once chat-ollama.core 2 | (:require [chat-ollama.lib :refer [defnc]] 3 | [helix.core :refer [$]] 4 | [refx.alpha :as refx :refer [dispatch-sync]] 5 | [chat-ollama.fx] 6 | [chat-ollama.events] 7 | [chat-ollama.subs] 8 | [chat-ollama.views :as views] 9 | ["react-dom/client" :as rdc])) 10 | 11 | (enable-console-print!) 12 | 13 | (dispatch-sync [:initialise-db]) 14 | 15 | (defnc root-view [] 16 | ($ :div {:class ["h-screen" "w-screen" "bg-white" "overflow-hidden" "dark:bg-gray-900" "bg-white" "text-gray-900"]} 17 | ($ views/Main))) 18 | 19 | (defonce root (rdc/createRoot (js/document.getElementById "root"))) 20 | 21 | (defn ^:dev/after-load mount-ui [] 22 | (refx/clear-subscription-cache!) 23 | (.render root ($ root-view))) 24 | 25 | (defn ^:export main [] 26 | (mount-ui)) 27 | -------------------------------------------------------------------------------- /src/cljs/chat_ollama/db.cljs: -------------------------------------------------------------------------------- 1 | (ns chat-ollama.db 2 | (:require [cljs.spec.alpha :as s] 3 | [chat-ollama.db.model :as model] 4 | [chat-ollama.db.dialog :as dialog])) 5 | 6 | (s/def ::ollama-offline? (s/nilable boolean?)) 7 | 8 | (s/def ::db (s/keys :req-un [:chat-ollama.db/ollama-offline? 9 | ::model/models 10 | ::model/selected-model 11 | ::dialog/dialogs 12 | ::dialog/selected-dialog])) 13 | 14 | ;; Default DB 15 | (def default-db 16 | {:models nil 17 | :dialogs nil 18 | :selected-model nil 19 | :selected-dialog nil 20 | :ollama-offline? nil}) 21 | -------------------------------------------------------------------------------- /src/cljs/chat_ollama/db/dialog.cljs: -------------------------------------------------------------------------------- 1 | (ns chat-ollama.db.dialog 2 | (:require [cljs.spec.alpha :as s])) 3 | 4 | (s/def ::prompt string?) 5 | (s/def ::timestamp number?) 6 | (s/def ::uuid string?) 7 | (s/def ::model-name (s/nilable string?)) 8 | (s/def ::answer (s/nilable string?)) 9 | (s/def ::generating? boolean?) 10 | (s/def ::aborted? boolean?) 11 | (s/def ::failed? boolean?) 12 | 13 | 14 | (s/def ::context coll?) 15 | (s/def ::created_at string?) 16 | (s/def ::eval_count int?) 17 | (s/def ::eval_duration int?) 18 | (s/def ::load_duration int?) 19 | (s/def ::prompt_eval_count int?) 20 | (s/def ::prompt_eval_duration int?) 21 | (s/def ::total_duration int?) 22 | (s/def ::response string?) 23 | (s/def ::meta (s/keys :req-un [::context 24 | ::created_at 25 | ::eval_count 26 | ::eval_duration 27 | ::load_duration 28 | ::prompt_eval_count 29 | ::total_duration 30 | ::response] 31 | :opt-un [::prompt_eval_duration])) 32 | 33 | (s/def ::exchange (s/keys :req-un [::timestamp ::prompt] 34 | :opt-un [::answer ::meta ::aborted? ::failed?])) 35 | (s/def ::exchanges (s/nilable (s/map-of ::uuid ::exchange))) 36 | (s/def ::dialog (s/keys :req-un [::uuid 37 | ::model-name 38 | ::timestamp 39 | ::generating?] 40 | :opt-un [::exchanges ::prompt])) 41 | (s/def ::dialogs (s/nilable (s/map-of ::uuid ::dialog))) 42 | (s/def ::selected-dialog (s/nilable ::uuid)) 43 | -------------------------------------------------------------------------------- /src/cljs/chat_ollama/db/model.cljs: -------------------------------------------------------------------------------- 1 | (ns chat-ollama.db.model 2 | (:require [cljs.spec.alpha :as s])) 3 | 4 | (s/def ::digest string?) 5 | (s/def ::size int?) 6 | (s/def ::name string?) 7 | 8 | (s/def ::model (s/keys :req-un [::digest ::name ::size])) 9 | (s/def ::models (s/nilable (s/coll-of ::model :kind coll?))) 10 | (s/def ::selected-model (s/nilable ::name)) 11 | -------------------------------------------------------------------------------- /src/cljs/chat_ollama/events.cljs: -------------------------------------------------------------------------------- 1 | (ns chat-ollama.events 2 | (:require [cljs.spec.alpha :as s] 3 | [chat-ollama.db :refer [default-db]] 4 | [refx.alpha :refer [->interceptor reg-event-db reg-event-fx]] 5 | [refx.interceptors :refer [after]] 6 | ["date-fns" :refer (getUnixTime)])) 7 | 8 | (defonce api-base "http://127.0.0.1:11434") 9 | (defonce wait-multiplier 1.25) 10 | (defonce wait-max (* 1000 60 5)) 11 | 12 | (defn check-and-throw 13 | "Throws an exception if `db` doesn't match the Spec `a-spec`." 14 | [a-spec db] 15 | (when-not (s/valid? a-spec db) 16 | (throw (ex-info (str "spec check failed: " (s/explain-str a-spec db)) {})))) 17 | 18 | (def check-spec-interceptor (after (partial check-and-throw :chat-ollama.db/db))) 19 | 20 | (def offline-interceptor 21 | (->interceptor 22 | :id :offline? 23 | :after (fn [{:keys [coeffects] :as context}] 24 | (let [{:keys [event db]} coeffects 25 | offline? (:ollama-offline? db) 26 | [_ wait {:keys [url status]}] event] 27 | (if (and (some? url) 28 | (zero? status)) 29 | (let [new-wait (when (number? wait) 30 | (* wait wait-multiplier))] 31 | (cond-> context 32 | true 33 | (assoc-in [:coeffects :db :ollama-offline?] true) 34 | 35 | (and (not (false? offline?)) 36 | (number? new-wait) 37 | (< new-wait wait-max)) 38 | (update-in [:effects :fx] 39 | conj 40 | [:dispatch-later {:ms new-wait 41 | :dispatch [:get-models new-wait]}]))) 42 | context))))) 43 | 44 | (def ollama-interceptors 45 | [offline-interceptor 46 | check-spec-interceptor]) 47 | 48 | (reg-event-fx 49 | :initialise-db 50 | ollama-interceptors 51 | (fn [_ _] 52 | {:db default-db 53 | :dispatch [:new-dialog]})) 54 | 55 | (reg-event-db 56 | :set-selected-model 57 | ollama-interceptors 58 | (fn [db [_ model-name]] 59 | (assoc db :selected-model model-name))) 60 | 61 | ;; GET MODELS 62 | (reg-event-fx 63 | :get-models-success 64 | ollama-interceptors 65 | (fn [{:keys [db]} [_ {:keys [models]}]] 66 | {:db (assoc db 67 | :models models 68 | :ollama-offline? false)})) 69 | 70 | (reg-event-db 71 | :get-models-failure 72 | ollama-interceptors 73 | (fn [db [_ _wait {:keys [status]}]] 74 | (assoc db :ollama-offline? (zero? status)))) 75 | 76 | (reg-event-fx 77 | :get-models 78 | ollama-interceptors 79 | (fn [_ [_ wait]] 80 | {:fetch {:url (str api-base "/api/tags") 81 | :method :get 82 | :on-success [:get-models-success] 83 | :on-failure [:get-models-failure wait]}})) 84 | 85 | (reg-event-db 86 | :warm-model-success 87 | identity) 88 | 89 | (reg-event-db 90 | :warm-model-failure 91 | ollama-interceptors 92 | (fn [db [_ _wait {:keys [status]}]] 93 | (assoc db :ollama-offline? (zero? status)))) 94 | 95 | (reg-event-fx 96 | :warm-model 97 | ollama-interceptors 98 | (fn [_ [_ model-name]] 99 | (if model-name 100 | {:fetch {:url (str api-base "/api/generate") 101 | :method :post 102 | :body {:model model-name} 103 | :on-success [:warm-model-success] 104 | :on-failure [:warm-model-failure]}} 105 | {}))) 106 | 107 | ;; DIALOGS 108 | 109 | (reg-event-fx 110 | :set-selected-dialog 111 | ollama-interceptors 112 | (fn [{:keys [db]} [_ dialog-uuid]] 113 | (let [selected-dialog (get-in db [:dialogs dialog-uuid])] 114 | {:db (assoc db :selected-dialog dialog-uuid) 115 | :dispatch [:warm-model (:model-name selected-dialog)]}))) 116 | 117 | (reg-event-db 118 | :set-dialog-model 119 | ollama-interceptors 120 | (fn [db [_ dialog-uuid model-name]] 121 | (-> db 122 | (assoc-in [:dialogs dialog-uuid :model-name] model-name) 123 | (assoc :selected-model model-name)))) 124 | 125 | (reg-event-fx 126 | :new-dialog 127 | ollama-interceptors 128 | (fn [{:keys [db]} [_ model-name]] 129 | (let [new-uuid (str (random-uuid)) 130 | timestamp (getUnixTime (new js/Date))] 131 | {:db (-> db 132 | (assoc-in [:dialogs new-uuid] 133 | {:uuid new-uuid 134 | :generating? false 135 | :model-name model-name 136 | :timestamp timestamp}) 137 | (assoc :selected-model model-name 138 | :selected-dialog new-uuid)) 139 | :dispatch [:warm-model model-name]}))) 140 | 141 | (reg-event-fx 142 | :delete-dialog 143 | ollama-interceptors 144 | (fn [{:keys [db]} [_ dialog-uuid]] 145 | (let [purged (update db :dialogs dissoc dialog-uuid) 146 | next-selected (->> purged 147 | :dialogs 148 | vals 149 | (sort-by :timestamp) 150 | first) 151 | model-name (:model-name next-selected)] 152 | (cond-> {:db (-> purged 153 | (assoc :selected-model model-name 154 | :selected-dialog (:uuid next-selected)))} 155 | (seq? model-name) 156 | (assoc :dispatch [:warm-model model-name]))))) 157 | 158 | ;; PROMPTS 159 | (reg-event-fx 160 | :send-prompt 161 | ollama-interceptors 162 | (fn [{:keys [db]} [_ {:keys [selected-dialog prompt set-abort!]}]] 163 | (let [new-uuid (str (random-uuid)) 164 | timestamp (getUnixTime (new js/Date)) 165 | context (->> (get-in db [:dialogs selected-dialog :exchanges]) 166 | vals 167 | (sort-by > :timestamp) 168 | first 169 | :meta 170 | :context)] 171 | {:db (cond-> db 172 | :always 173 | (assoc-in [:dialogs selected-dialog :generating?] true) 174 | :always 175 | (assoc-in [:dialogs selected-dialog :exchanges new-uuid] 176 | {:prompt prompt 177 | :timestamp timestamp}) 178 | (nil? context) 179 | (assoc-in [:dialogs selected-dialog :title] prompt)) 180 | :dispatch [:get-answer {:prompt prompt 181 | :context context 182 | :set-abort! set-abort! 183 | :dialog-uuid selected-dialog 184 | :exchange-uuid new-uuid}]}))) 185 | 186 | (reg-event-fx 187 | :get-answer-success 188 | ollama-interceptors 189 | (fn [{:keys [db]} [_ {:keys [dialog-uuid exchange-uuid]} response]] 190 | {:db (-> db 191 | (assoc-in [:dialogs dialog-uuid :exchanges exchange-uuid :meta] response) 192 | (assoc-in [:dialogs dialog-uuid :generating?] false))})) 193 | 194 | (reg-event-fx 195 | :get-answer-progress 196 | ollama-interceptors 197 | (fn [{:keys [db]} 198 | [_ 199 | {:keys [dialog-uuid exchange-uuid]} 200 | {:keys [text _idx _new-line?]}]] 201 | {:db (update-in db [:dialogs dialog-uuid :exchanges exchange-uuid :answer] 202 | #(str % text))})) 203 | 204 | (reg-event-db 205 | :get-answer-failure 206 | ollama-interceptors 207 | (fn [db [_ {:keys [dialog-uuid exchange-uuid]} {:keys [status]}]] 208 | (-> db 209 | (assoc :ollama-offline? (zero? status)) 210 | (assoc-in [:dialogs dialog-uuid :generating?] false) 211 | (assoc-in [:dialogs dialog-uuid :exchanges exchange-uuid :failed?] true)))) 212 | 213 | (reg-event-db 214 | :get-answer-abort 215 | ollama-interceptors 216 | (fn [db [_ {:keys [dialog-uuid exchange-uuid]} _]] 217 | (-> db 218 | (assoc-in [:dialogs dialog-uuid :generating?] false) 219 | (assoc-in [:dialogs dialog-uuid :exchanges exchange-uuid :aborted?] true)))) 220 | 221 | (reg-event-fx 222 | :get-answer 223 | ollama-interceptors 224 | (fn [{:keys [db]} [_ {:keys [prompt context set-abort!] :as payload}]] 225 | {:fetch-stream {:url (str api-base "/api/generate") 226 | :method :post 227 | :body (cond-> {:model (:selected-model db) 228 | :prompt prompt} 229 | (some? context) 230 | (assoc :context context)) 231 | :set-abort! set-abort! 232 | :on-progress [:get-answer-progress payload] 233 | :on-success [:get-answer-success payload] 234 | :on-abort [:get-answer-abort payload] 235 | :on-failure [:get-answer-failure payload]}})) 236 | -------------------------------------------------------------------------------- /src/cljs/chat_ollama/fx.cljs: -------------------------------------------------------------------------------- 1 | (ns chat-ollama.fx 2 | (:require [promesa.core :as p] 3 | [applied-science.js-interop :as j] 4 | [cljs-bean.core :refer [->js ->clj]] 5 | [refx.alpha :refer [dispatch reg-fx]] 6 | [clojure.string :as str])) 7 | 8 | (defn request->fetch 9 | [{:as request 10 | :keys [url body on-success on-failure] 11 | :or {on-success [:http-no-on-success] 12 | on-failure [:http-no-on-failure]}}] 13 | (let [success-> #(dispatch (conj on-success (js->clj % :keywordize-keys true))) 14 | pruned (dissoc request :on-success :on-failure :url) 15 | options (cond-> pruned 16 | (some? body) 17 | (update :body #(as-> % $ 18 | (->js $) 19 | (j/call js/JSON :stringify $))))] 20 | (-> (js/fetch url (->js options)) 21 | (.then (fn [response] 22 | (if (j/get response :ok) 23 | (let [data (j/get response :data)] 24 | (if (some? data) 25 | (success-> data) 26 | (-> (j/call response :json) 27 | (.then success->)))) 28 | (dispatch (conj on-failure request))))) 29 | (.catch #(dispatch (conj on-failure (assoc request :status 0))))))) 30 | 31 | (defn fetch-effect [request] 32 | (let [seq-request-maps (if (sequential? request) request [request])] 33 | (doseq [request seq-request-maps] 34 | (request->fetch request)))) 35 | 36 | (reg-fx :fetch fetch-effect) 37 | 38 | (defn- parse-chunk [chunk] 39 | (try 40 | (js/JSON.parse chunk) 41 | (catch js/Error _ 42 | (prn "parse error!" chunk)))) 43 | 44 | (defn request->fetch-stream 45 | [{:keys [url body on-success on-progress on-failure signal] 46 | :or {on-success [:http-no-on-success] 47 | on-progress [:http-no-on-progress] 48 | on-failure [:http-no-on-failure]}}] 49 | (-> (js/fetch url #js{:method "post" 50 | :body (j/call js/JSON :stringify (->js body)) 51 | :signal signal}) 52 | (j/call :then 53 | (fn [response] 54 | (if-not (j/get response :ok) 55 | (dispatch (conj on-failure {:status 0})) 56 | (let [reader (-> response 57 | (j/get :body) 58 | (j/call :getReader))] 59 | #_{:clj-kondo/ignore [:unresolved-symbol]} 60 | (p/loop [data {:response ""}] 61 | (p/let [read (j/call reader :read) 62 | {:keys [done value]} (j/lookup read)] 63 | (if (and done (nil? value)) 64 | (dispatch (conj on-success data)) 65 | (let [chunk (-> (new js/TextDecoder) 66 | (j/call :decode value)) 67 | lines (str/split-lines chunk) 68 | buffer (atom "") 69 | final-result (atom nil)] 70 | 71 | (doseq [line lines] 72 | (let [{:keys [response] :as result} (->clj (parse-chunk line)) 73 | has-value? (seq response)] 74 | (if has-value? 75 | (do 76 | (swap! buffer str response) 77 | (dispatch (conj on-progress {:text response}))) 78 | (reset! final-result result)))) 79 | 80 | (p/recur (if (some? @final-result) 81 | (merge data @final-result) 82 | (update data :response #(str % @buffer)))))))))))) 83 | (j/call :catch 84 | #(when (re-find #"network error" (j/get % :message)) 85 | (dispatch (conj on-failure {:status 0})))))) 86 | 87 | (reg-fx :fetch-stream 88 | (fn [{:keys [set-abort! on-abort] :as request}] 89 | (if (fn? set-abort!) 90 | (let [controller (new js/AbortController) 91 | signal (j/get controller :signal)] 92 | (set-abort! (fn [] 93 | #(do 94 | (j/call controller :abort) 95 | (dispatch (conj on-abort request)) 96 | (set-abort! nil)))) 97 | (request->fetch-stream (assoc request :signal signal))) 98 | (request->fetch-stream request)))) 99 | -------------------------------------------------------------------------------- /src/cljs/chat_ollama/hooks.cljs: -------------------------------------------------------------------------------- 1 | (ns chat-ollama.hooks 2 | (:require [applied-science.js-interop :as j] 3 | [helix.hooks :refer [use-effect use-state]])) 4 | 5 | 6 | (defn use-copy-to-clipboard [] 7 | (let [[copied set-copied!] (use-state false) 8 | clipboard (j/get js/navigator :clipboard) 9 | copy! #(do 10 | (j/call clipboard :writeText %) 11 | (set-copied! true))] 12 | (use-effect 13 | [copied] 14 | (when copied 15 | (let [wait (js/setTimeout #(set-copied! false) 1500)] 16 | #(js/clearTimeout wait)))) 17 | 18 | (when clipboard 19 | [copied copy!]))) 20 | -------------------------------------------------------------------------------- /src/cljs/chat_ollama/lib.cljc: -------------------------------------------------------------------------------- 1 | (ns chat-ollama.lib 2 | #?(:clj (:require [helix.core]) 3 | :cljs (:require-macros [chat-ollama.lib]))) 4 | 5 | #?(:clj 6 | (defmacro defnc [type & form-body] 7 | (let [[docstring form-body] (if (string? (first form-body)) 8 | [(first form-body) (rest form-body)] 9 | [nil form-body]) 10 | [fn-meta form-body] (if (map? (first form-body)) 11 | [(first form-body) (rest form-body)] 12 | [nil form-body]) 13 | params (first form-body) 14 | body (rest form-body) 15 | opts-map? (map? (first body)) 16 | opts (cond-> (if opts-map? 17 | (first body) 18 | {}) 19 | (:wrap fn-meta) (assoc :wrap (:wrap fn-meta))) 20 | ;; feature flags to enable by default 21 | default-opts {:helix/features {:fast-refresh true}}] 22 | `(helix.core/defnc ~type 23 | ~@(when docstring [docstring]) 24 | ~@(when fn-meta [fn-meta]) 25 | ~params 26 | ;; we use `merge` here to allow indidivual consumers to override feature 27 | ;; flags in special cases 28 | ~(merge default-opts opts) 29 | ~@body)))) 30 | -------------------------------------------------------------------------------- /src/cljs/chat_ollama/subs.cljs: -------------------------------------------------------------------------------- 1 | (ns chat-ollama.subs 2 | (:require [refx.alpha :refer [reg-sub]])) 3 | 4 | ;; Misc 5 | (reg-sub 6 | :ollama-offline? 7 | (fn [db _] 8 | (:ollama-offline? db))) 9 | 10 | (reg-sub 11 | :models 12 | (fn [db _] 13 | (:models db))) 14 | 15 | (reg-sub 16 | :selected-model 17 | (fn [db _] 18 | (:selected-model db))) 19 | 20 | (reg-sub 21 | :dialogs 22 | (fn [db _] 23 | (:dialogs db))) 24 | 25 | (reg-sub 26 | :dialog-list 27 | :<- [:dialogs] 28 | (fn [dialogs] 29 | (->> dialogs 30 | (vals) 31 | (map #(dissoc % :exchanges)) 32 | (sort-by :timestamp) 33 | (reverse)))) 34 | 35 | (reg-sub 36 | :selected-dialog 37 | (fn [db _] 38 | (:selected-dialog db))) 39 | 40 | (reg-sub 41 | :dialog 42 | :<- [:dialogs] 43 | (fn [dialogs [_ dialog-uuid]] 44 | (get dialogs dialog-uuid))) 45 | 46 | (reg-sub 47 | :dialog-meta 48 | :<- [:dialogs] 49 | (fn [dialogs [_ dialog-uuid]] 50 | (-> (get dialogs dialog-uuid) 51 | (select-keys [:title :model-name])))) 52 | 53 | (reg-sub 54 | :dialog-exchanges 55 | :<- [:dialogs] 56 | (fn [dialogs [_ dialog-uuid]] 57 | (->> (get-in dialogs [dialog-uuid :exchanges]) 58 | (map (fn [[k v]] 59 | (assoc v :uuid k))) 60 | (sort-by :timestamp) 61 | (mapv :uuid)))) 62 | 63 | (reg-sub 64 | :dialog-exchange 65 | (fn [db [_ {:keys [dialog-uuid exchange-uuid]}]] 66 | (get-in db [:dialogs dialog-uuid :exchanges exchange-uuid]))) 67 | -------------------------------------------------------------------------------- /src/cljs/chat_ollama/utils.cljs: -------------------------------------------------------------------------------- 1 | (ns chat-ollama.utils 2 | (:require [cognitect.transit :as t] 3 | [applied-science.js-interop :as j])) 4 | 5 | 6 | (defn debounce [f delay-ms] 7 | (let [timer (atom nil)] 8 | (fn [& args] 9 | (when @timer (js/clearTimeout @timer)) 10 | (reset! timer (js/setTimeout #(apply f args) delay-ms))))) 11 | 12 | (defn throttle [f interval-ms] 13 | (let [timeout (atom nil) 14 | fire? (atom false) 15 | stored-args (atom []) 16 | fire (fn fire [] 17 | (reset! timeout 18 | (js/setTimeout #(do 19 | (reset! timeout nil) 20 | (when @fire? 21 | (reset! fire? false) 22 | (fire))) 23 | interval-ms)) 24 | (apply f @stored-args))] 25 | (fn [& args] 26 | (reset! stored-args args) 27 | (if @timeout 28 | (reset! fire? true) 29 | (fire))))) 30 | 31 | (defn local-storage-set! [k v] 32 | (try 33 | (let [w (t/writer :json) 34 | tv (t/write w v)] 35 | (-> (j/get js/window :localStorage) 36 | (j/call :setItem k tv))) 37 | (catch js/Error e 38 | (js/console.error e) 39 | (throw e)))) 40 | 41 | (defn local-storage-get [k] 42 | (try 43 | (let [r (t/reader :json)] 44 | (as-> (j/get js/window :localStorage) $ 45 | (j/call $ :getItem k) 46 | (t/read r $))) 47 | (catch js/Error e 48 | (js/console.error e) 49 | (throw e)))) 50 | -------------------------------------------------------------------------------- /src/cljs/chat_ollama/views.cljs: -------------------------------------------------------------------------------- 1 | (ns chat-ollama.views 2 | (:require [applied-science.js-interop :as j] 3 | [clojure.string :as str] 4 | [clojure.set :refer [union]] 5 | [chat-ollama.lib :refer [defnc]] 6 | [chat-ollama.utils :refer [debounce throttle local-storage-set! local-storage-get]] 7 | [chat-ollama.hooks :refer [use-copy-to-clipboard]] 8 | [helix.core :refer [$ <>]] 9 | [helix.hooks :refer [use-effect use-state use-ref]] 10 | [refx.alpha :refer [use-sub dispatch]] 11 | ["react-markdown$default" :as ReactMarkdown] 12 | ["react-syntax-highlighter/dist/esm/styles/hljs" :as p :refer [nord githubGist]] 13 | ["react-syntax-highlighter" :refer [Light]] 14 | ["react-hotkeys-hook" :refer [useHotkeys]] 15 | ["date-fns" :refer [formatDistance fromUnixTime parseISO]] 16 | ["@react-spring/web" :refer [useSpring animated]] 17 | ["lucide-react" :refer [Clipboard Check Plus User MessagesSquare Trash2 18 | PanelLeftClose PanelLeftOpen SendHorizontal XOctagon 19 | ArrowUpToLine ArrowDownToLine RefreshCw]])) 20 | 21 | (defonce dark-mode? (j/get (js/matchMedia "(prefers-color-scheme: dark)") :matches)) 22 | (defonce max-textarea-height 500) 23 | (defonce min-textarea-height 48) 24 | (defonce line-height 48) 25 | (defonce sidebar-width 325) 26 | (defonce ls-chat-ollama-prefs "chat-ollama:prefs:") 27 | 28 | (defn- b->gb [bytes] 29 | (j/call (/ bytes 1024 1024 1024) :toFixed 2)) 30 | 31 | (defnc Ollama [] 32 | ($ :svg {:class ["dark:fill-gray-900" "fill-white" "w-[21px]" "h-[27px]" "scale-125"] 33 | :xmlns "http://www.w3.org/2000/svg"} 34 | ($ :path {:d "M19.642 27h-1.498c.315-1.119.308-2.208-.022-3.266-.177-.568-.915-1.363-.497-1.933 1.421-1.94 1.16-4.045.06-5.995-.133-.234-.148-.542.014-.74 1.088-1.333 1.29-2.789.606-4.369-.56-1.293-1.861-2.349-3.327-2.3-.253.007-.495.016-.726.027a.29.29 0 0 1-.28-.177c-.498-1.168-1.373-1.928-2.624-2.281-1.737-.49-3.658.459-4.423 2.072-.116.244-.147.388-.468.377-.422-.015-.859-.056-1.255.025-2.717.554-3.876 3.896-2.47 6.136.333.528.816.613.353 1.378-1.063 1.762-1.203 4.146.12 5.822.453.576-.384 1.567-.547 2.18-.26.983-.24 1.998.058 3.044H1.211c-.417-1.445-.269-3.32.508-4.648a.081.081 0 0 0-.002-.092C.424 20.28.52 17.66 1.567 15.603a.092.092 0 0 0-.006-.096c-1.279-1.93-1.228-4.524.15-6.385.304-.41.775-.836 1.173-1.236a.102.102 0 0 0 .029-.093 9.956 9.956 0 0 1 .172-4.504c.262-.967.991-2.224 2.099-2.177 1.7.072 2.336 2.658 2.426 3.966a.045.045 0 0 0 .066.036c1.822-1.041 3.643-1.037 5.463.012a.07.07 0 0 0 .104-.056c.073-1.126.441-2.537 1.234-3.384.534-.57 1.306-.75 1.97-.378 1.819 1.018 1.803 4.83 1.494 6.509a.09.09 0 0 0 .028.087c.4.374.659.622.777.745 1.713 1.775 1.845 4.76.526 6.818a.088.088 0 0 0-.004.094c1.053 2.066 1.175 4.724-.145 6.715a.1.1 0 0 0 0 .108c.248.374.428.785.54 1.234a6.65 6.65 0 0 1-.02 3.382ZM5.197 2.62a.07.07 0 0 0-.048-.018.066.066 0 0 0-.047.02c-.93.929-.984 3.236-.81 4.435.006.046.031.063.075.052a8.11 8.11 0 0 1 1.576-.222.114.114 0 0 0 .083-.04c.113-.13.17-.23.174-.301.044-1.116-.128-3.116-1.003-3.926Zm10.602.046a.165.165 0 0 0-.25.023c-.76 1.06-.933 2.549-.904 3.815.002.087.058.2.168.34.022.029.05.043.086.044a6.516 6.516 0 0 1 1.6.24.045.045 0 0 0 .051-.018.046.046 0 0 0 .007-.018c.154-1.116.127-3.574-.758-4.426Z"}) 35 | ($ :path {:d "M13.48 13.144c2.105 2.046.448 4.854-2.154 5.035-.502.035-1.099.037-1.789.006-1.834-.08-3.609-1.734-2.989-3.708.894-2.843 4.981-3.23 6.932-1.333Zm-.323 1.199c-.874-1.46-2.958-1.69-4.342-1.008-.75.369-1.446 1.142-1.387 2.025.148 2.264 3.936 2.163 5.141 1.372.85-.56 1.109-1.518.588-2.39ZM4.607 12.684c-.29.5-.154 1.121.301 1.386.455.265 1.059.075 1.348-.426.289-.5.154-1.12-.302-1.386-.455-.265-1.058-.074-1.347.427ZM14.596 13.65c.293.498.898.683 1.351.414.454-.27.583-.89.29-1.388-.293-.497-.898-.682-1.35-.413-.454.269-.584.89-.29 1.387Z"}) 36 | ($ :path {:d "M9.954 15.208c-.297-.103-.445-.31-.444-.622 0-.034.012-.065.033-.09.261-.31.536-.223.812-.034a.085.085 0 0 0 .103-.004c.206-.165.525-.253.728-.033.34.37-.113.64-.37.83a.08.08 0 0 0-.032.073l.06.572a.12.12 0 0 1-.028.091c-.155.195-.359.25-.612.168-.389-.126-.196-.58-.187-.86 0-.046-.02-.077-.063-.091Z"}))) 37 | 38 | (defnc OllamaAsleep [] 39 | ($ :svg {:class ["fill-gray-900" "dark:fill-white" "w-[77px]" "h-[101px]"] 40 | :xmlns "http://www.w3.org/2000/svg"} 41 | ($ :path {:d "M74.203 100.5h-5.787c1.216-4.322 1.189-8.527-.083-12.616-.684-2.193-3.535-5.262-1.921-7.464 5.49-7.497 4.483-15.626.23-23.157-.511-.905-.57-2.096.055-2.862 4.205-5.145 4.985-10.769 2.34-16.873-2.16-4.992-7.187-9.071-12.848-8.886-.979.03-1.914.066-2.806.105a1.12 1.12 0 0 1-1.082-.682c-1.923-4.51-5.301-7.447-10.135-8.81-6.71-1.895-14.127 1.772-17.084 8.002-.448.943-.566 1.499-1.807 1.456-1.631-.058-3.317-.214-4.848.097-10.496 2.139-14.97 15.05-9.54 23.7 1.284 2.042 3.15 2.37 1.363 5.326-4.105 6.802-4.646 16.013.462 22.487 1.752 2.223-1.48 6.05-2.11 8.42-1.006 3.797-.932 7.716.223 11.757H3.013c-1.61-5.582-1.04-12.823 1.962-17.954a.314.314 0 0 0-.008-.354C-.028 74.54.34 64.42 4.384 56.48a.355.355 0 0 0-.021-.375c-4.94-7.45-4.743-17.474.579-24.66 1.174-1.582 2.994-3.228 4.533-4.773a.393.393 0 0 0 .109-.362c-1.107-5.854-.885-11.652.666-17.394C11.261 5.178 14.08.324 18.356.505c6.571.278 9.024 10.267 9.372 15.319a.172.172 0 0 0 .256.139c7.036-4.022 14.07-4.006 21.101.046a.268.268 0 0 0 .403-.215c.28-4.348 1.702-9.8 4.763-13.07 2.063-2.206 5.045-2.897 7.611-1.461 7.024 3.931 6.965 18.657 5.77 25.14a.344.344 0 0 0 .11.336c1.542 1.445 2.543 2.405 3.002 2.88 6.613 6.853 7.124 18.383 2.03 26.335a.34.34 0 0 0-.017.362c4.067 7.981 4.54 18.249-.558 25.935a.385.385 0 0 0 0 .421 15.113 15.113 0 0 1 2.084 4.766c1.099 4.434 1.072 8.788-.08 13.062ZM18.406 6.331a.272.272 0 0 0-.185-.071.256.256 0 0 0-.18.075c-3.589 3.591-3.803 12.503-3.132 17.133.025.177.123.244.294.202a31.325 31.325 0 0 1 6.084-.858.44.44 0 0 0 .323-.156c.436-.5.66-.887.671-1.162.172-4.31-.495-12.035-3.874-15.163Zm40.953.177a.641.641 0 0 0-.965.088c-2.935 4.096-3.606 9.846-3.493 14.738.009.334.225.772.65 1.314a.41.41 0 0 0 .331.168c2.1.053 4.16.363 6.181.93a.176.176 0 0 0 .223-.143c.595-4.31.49-13.803-2.927-17.095Z"}) 42 | ($ :path {:d "M50.402 46.979c8.13 7.906 1.732 18.75-8.32 19.448-1.94.135-4.243.143-6.91.026-7.083-.312-13.94-6.698-11.545-14.326 3.451-10.978 19.24-12.477 26.775-5.148Zm-1.25 4.63c-3.376-5.64-11.423-6.524-16.77-3.893-2.897 1.427-5.585 4.411-5.358 7.825.574 8.744 15.205 8.352 19.86 5.296 3.283-2.16 4.281-5.86 2.268-9.227Z"}) 43 | ($ :path {:d "M36.782 54.952c-1.146-.398-1.718-1.2-1.715-2.404 0-.127.044-.25.126-.345 1.01-1.2 2.071-.863 3.136-.13a.33.33 0 0 0 .398-.017c.797-.636 2.03-.977 2.814-.127 1.313 1.428-.436 2.472-1.43 3.208a.312.312 0 0 0-.121.278l.226 2.21a.465.465 0 0 1-.105.354c-.598.749-1.386.965-2.365.648-1.501-.488-.755-2.24-.721-3.326.003-.176-.078-.293-.243-.349Z"}) 44 | ($ :path {:d "M13.399 43.411a1.726 1.726 0 0 1 2.42-.322l-1.05 1.37 1.05-1.37-.003-.002.001.001.011.009a6.796 6.796 0 0 0 .293.207c.207.142.497.33.826.514.745.419 1.375.642 1.706.642.508 0 1.155-.246 1.79-.622a7.522 7.522 0 0 0 .904-.632l.041-.035.005-.005a1.726 1.726 0 0 1 2.29 2.584l-1.147-1.29 1.146 1.29-.003.002-.003.003-.01.009-.027.023a9.187 9.187 0 0 1-.391.315c-.253.192-.612.448-1.046.705-.822.487-2.116 1.104-3.55 1.104-1.295 0-2.606-.64-3.397-1.084a14.834 14.834 0 0 1-1.492-.965l-.028-.02-.008-.007-.005-.004 1.048-1.371-1.049 1.37a1.726 1.726 0 0 1-.322-2.419Zm42.974-.322.011.008.057.041a11.403 11.403 0 0 0 1.062.68c.744.419 1.374.642 1.706.642.508 0 1.155-.246 1.79-.622a7.522 7.522 0 0 0 .903-.632l.042-.035.002-.002.003-.003a1.726 1.726 0 0 1 2.289 2.584l-1.147-1.29 1.146 1.29-.005.005-.01.009-.027.023a9.187 9.187 0 0 1-.391.315c-.253.192-.612.448-1.046.705-.822.487-2.116 1.104-3.55 1.104-1.295 0-2.607-.64-3.397-1.084a14.834 14.834 0 0 1-1.493-.965l-.027-.02-.009-.007-.003-.003h-.001s-.001-.002 1.047-1.372l-1.048 1.37a1.726 1.726 0 0 1 2.096-2.741Z"}))) 45 | 46 | (defnc IconButton 47 | [{:keys [icon on-click disabled? class] 48 | :or {class []}}] 49 | ($ :button {:class (into class conj ["disabled:opacity-50" "p-2.5" "rounded" 50 | "dark:enabled:hover:bg-gray-800/40" 51 | "dark:enabled:hover:text-gray-100" 52 | "dark:text-gray-300/80" 53 | "enabled:hover:bg-gray-200/30" 54 | "enabled:hover:text-gray-700" 55 | "text-gray-600/80"]) 56 | :on-click on-click 57 | :disabled disabled?} 58 | ($ icon {:size 20}))) 59 | 60 | (defnc Footer [] 61 | (let [selected-dialog (use-sub [:selected-dialog]) 62 | selected-model (use-sub [:selected-model]) 63 | {:keys [generating?]} (use-sub [:dialog selected-dialog]) 64 | [prompt set-prompt!] (use-state nil) 65 | [abort set-abort!] (use-state nil) 66 | slowly-set-prompt (debounce set-prompt! 150) 67 | ref! (use-ref nil) 68 | set-height! #(do 69 | (j/assoc-in! @ref! [:style :height] "auto") 70 | (j/assoc-in! @ref! 71 | [:style :height] 72 | (str (min max-textarea-height 73 | (max min-textarea-height 74 | (j/get @ref! :scrollHeight))) "px"))) 75 | send! #(do 76 | (dispatch [:send-prompt {:selected-dialog selected-dialog 77 | :prompt % 78 | :set-abort! set-abort!}]) 79 | (j/assoc! @ref! :value "") 80 | (set-prompt! nil) 81 | (set-height!)) 82 | on-key-press #(when (and (= (j/get % :key) "Enter") 83 | (not (j/get % :shiftKey))) 84 | (j/call % :preventDefault) 85 | (send! (j/get-in % [:target :value])))] 86 | 87 | (use-effect 88 | [@ref! generating?] 89 | (when (and @ref! (not generating?)) 90 | (j/call @ref! :focus))) 91 | 92 | ($ :div {:class ["absolute" "bottom-0" "inset-x-0"]} 93 | (when (some? selected-model) 94 | ($ :div {:class ["dark:bg-gray-900" "bg-white" "z-10" "max-w-5xl" "mx-auto" "absolute" "bottom-0" "pb-6" "inset-x-16"]} 95 | (when (and generating? (fn? abort)) 96 | ($ :button {:class ["absolute" "right-0" "bottom-20" "mb-3.5" 97 | "z-20" "dark:text-white" "text-gray-700" "text-sm" "flex" "items-center" "p-2" "gap-2" 98 | "bg-gray-300/40" "hover:bg-gray-300/50" 99 | "dark:bg-white/10" "dark:hover:bg-white/20" "rounded" "shadow" "backdrop-blur"] 100 | :on-click abort} 101 | ($ XOctagon {:size 16}) 102 | ($ :span {} "Stop"))) 103 | ($ :div {:class ["z-0" "absolute" "top-0" "-translate-y-full" "inset-x-0" "h-9" 104 | "bg-gradient-to-t" "dark:from-gray-900" "from-white" "to-transparent" "pointer-events-none"]}) 105 | ($ :textarea {:ref ref! 106 | :key selected-dialog 107 | :autoFocus true 108 | :placeholder (str "Send message to " selected-model) 109 | :onChange #(do 110 | (slowly-set-prompt (j/get-in % [:target :value])) 111 | (set-height!)) 112 | :onKeyPress on-key-press 113 | :disabled generating? 114 | :rows 1 115 | :class ["w-full" "resize-none" "rounded" "relative" "z-10" "h-12" 116 | "pl-3.5" "pr-10" "py-2.5" "text-base" "font-normal" 117 | "dark:bg-gray-950" "border" "placeholder-gray-400/75" 118 | "dark:border-gray-200/10" "dark:placeholder-gray-300/40" "border-gray-300/60" 119 | "focus:outline-none" "focus:border-cyan-600" "focus:ring-1" "focus:ring-cyan-600" 120 | "disabled:opacity-75"]}) 121 | 122 | ($ :button {:class ["absolute" "right-3.5" "bottom-9" "mb-1.5" "z-20" "dark:text-white" "text-gray-700" 123 | (when-not (seq prompt) "opacity-20")] 124 | :on-click #(send! prompt)} 125 | ($ SendHorizontal))))))) 126 | 127 | (defnc Message [{:keys [user? children copy->clipboard]}] 128 | (let [[copied copy!] (use-copy-to-clipboard)] 129 | ($ :div {:class ["max-w-full" "lg:max-w-[85%]" "flex" "gap-3" 130 | (if user? "place-self-end flex-row-reverse" "place-self-start")]} 131 | ($ :div {:class ["shrink-0" "flex" "flex-col"]} 132 | ($ :div {:class ["rounded" "w-10" "h-10" "flex" "justify-center" 133 | (if user? 134 | "items-center dark:bg-gray-700/75 bg-gray-200" 135 | "items-end dark:bg-white bg-gray-800") 136 | (when copy->clipboard "mb-3")]} 137 | (if user? 138 | ($ User) 139 | ($ Ollama))) 140 | (when copy->clipboard 141 | ($ IconButton {:icon (if copied Check Clipboard) 142 | :on-click #(copy! copy->clipboard)}))) 143 | ($ :div {:class ["h-fit" "w-full" "rounded-md" "p-4" "flex" "flex-col" "gap-2.5" "overflow-scroll" 144 | (if user? 145 | "dark:bg-black/20 border dark:border-none border-gray-300/50" 146 | "dark:bg-gray-800/50 bg-gray-50 dark:text-white")]} 147 | children)))) 148 | 149 | (defnc Markdown [{:keys [children user?]}] 150 | ($ ReactMarkdown 151 | {:children children 152 | :className "markdown-body" 153 | :components 154 | #js{:code 155 | (fn [props] 156 | (let [{:keys [inline className children]} (j/lookup props) 157 | language (second (str/split className #"-")) 158 | [copied copy!] (use-copy-to-clipboard)] 159 | (if (and (not inline) 160 | (seq language)) 161 | (<> 162 | ($ :div {:class ["absolute" "right-1.5" "top-1.5" "z-10"]} 163 | ($ IconButton {:icon (if copied Check Clipboard) 164 | :on-click #(copy! (first children))})) 165 | ($ Light {:children (or (first children) "") 166 | :language language 167 | :style (if dark-mode? nord githubGist) 168 | :customStyle #js {:borderRadius "4px" 169 | :padding "16px"} 170 | :className (when user? "border dark:border-none border-gray-300/50")})) 171 | ($ :code {} children))))}})) 172 | 173 | (defnc Exchange [{:keys [dialog-uuid exchange-uuid]}] 174 | (let [{:keys [prompt answer aborted? failed? meta]} 175 | (use-sub [:dialog-exchange {:dialog-uuid dialog-uuid 176 | :exchange-uuid exchange-uuid}])] 177 | ($ :div {:class ["flex" "flex-col" "gap-6" "mt-6"]} 178 | ($ Message {:user? true} 179 | ($ Markdown {:user? true} prompt)) 180 | ($ Message {:user? false 181 | :copy->clipboard (:response meta)} 182 | (if answer 183 | ($ Markdown {} answer) 184 | (when-not (or failed? aborted?) 185 | ($ :div {:class ["flex" "flex-col" "gap-2" "animate-pulse" "min-w-[250px]"]} 186 | ($ :div {:class ["h-2" "dark:bg-white/10" "bg-gray-200/75" "rounded"]}) 187 | ($ :div {:class ["h-2" "dark:bg-white/10" "bg-gray-200/75" "rounded" "w-[75%]"]})))) 188 | (when aborted? 189 | ($ :p {:class ["dark:text-white/20" "text-sm" "italic"]} 190 | "The answer was stopped before finishing")) 191 | (when failed? 192 | ($ :p {:class ["dark:text-white/20" "text-sm" "italic"]} 193 | "There was an issue finishing this response.")) 194 | (when (some? meta) 195 | ($ :p {:class ["dark:text-white/20" "text-gray-300" "text-sm" "mt-2" "italic"]} 196 | (str "Took ~" (j/call js/Math :round (/ (:total_duration meta) 1e+9)) 197 | " seconds, at " (j/call js/Math :round (/ (:eval_count meta) (/ (:eval_duration meta) 1e+9))) 198 | " tokens per second."))))))) 199 | 200 | (defnc StartDialog [{:keys [dialog-uuid]}] 201 | (let [models (use-sub [:models])] 202 | ($ :div {:class ["flex" "flex-col" "grow" "w-full" 203 | "justify-center" "items-center" 204 | "py-12" "px-[4.5rem]" "h-full"]} 205 | ($ :h1 {:class ["dark:text-white" "text-2xl"]} 206 | "Start a new Chat") 207 | ($ :h2 {:class ["text-lg" "dark:text-white/40" "text-gray-800/60" "mb-4"]} 208 | "Choose a model to begin your conversation") 209 | ($ :ul {:class ["dark:text-white" "dark:bg-gray-950/30" "bg-white/75" "w-full" "max-w-2xl" "overflow-scroll" 210 | "rounded" "border" "dark:border-gray-700/40" "border-gray-400/50" 211 | "divide-y" "dark:divide-gray-700/50" "divide-gray-400/50"]} 212 | (for [model models] 213 | (let [[model-name model-version] (str/split (:name model) #":")] 214 | ($ :li {:key (:digest model) 215 | :class ["hover:text-white"]} 216 | ($ :button {:class ["text-left" "w-full" "hover:bg-cyan-700" "pl-3" "pr-3.5" "py-2" 217 | "flex" "items-center" "justify-between" "group"] 218 | :on-click #(dispatch [:set-dialog-model dialog-uuid (:name model)])} 219 | ($ :div 220 | ($ :p {:class ["text-lg"]} 221 | model-name 222 | ($ :span {:class ["opacity-50"]} ":" model-version)) 223 | ($ :p {:class ["flex" "text-sm" "gap-3"]} 224 | ($ :span {:class ["opacity-60"]} 225 | (formatDistance 226 | (parseISO (:modified_at model)) 227 | (new js/Date) 228 | #js {:addSuffix true})) 229 | ($ :span {:class ["opacity-40"]} 230 | (b->gb (:size model)) "GB") 231 | ($ :span {:class ["opacity-20"]} 232 | (subs (:digest model) 0 7)))) 233 | ($ :div {:class ["p-1.5" "rounded" "bg-cyan-600" "text-white" 234 | "group-hover:bg-white" "group-hover:text-cyan-600"]} 235 | ($ Plus)))))))))) 236 | 237 | (defnc Dialog [] 238 | (let [ref! (use-ref nil) 239 | selected-model (use-sub [:selected-model]) 240 | selected-dialog (use-sub [:selected-dialog]) 241 | exchanges (use-sub [:dialog-exchanges selected-dialog]) 242 | {:keys [title]} (use-sub [:dialog-meta selected-dialog]) 243 | [model-name model-version] (str/split selected-model #":") 244 | [->top-disabled? set->top-disabled] (use-state true) 245 | [->bottom-disabled? set->bottom-disabled] (use-state true) 246 | ->top #(j/call @ref! 247 | :scrollTo 248 | #js{:top 0 :behavior "smooth"}) 249 | ->bottom #(j/call @ref! 250 | :scrollTo 251 | #js{:top (j/get @ref! :scrollHeight) 252 | :behavior "smooth"}) 253 | get-scroll-info #(when @ref! 254 | (let [height (-> @ref! 255 | (j/call :getBoundingClientRect) 256 | (j/get :height)) 257 | scroll-height (j/get @ref! :scrollHeight) 258 | scroll-top (j/get @ref! :scrollTop)] 259 | {:scroll-top scroll-top 260 | :scroll-bottom (- scroll-height (+ height scroll-top))})) 261 | slow-on-scroll 262 | (debounce (fn [] 263 | (let [{:keys [scroll-bottom scroll-top]} 264 | (get-scroll-info)] 265 | (set->top-disabled (not (pos? scroll-top))) 266 | (set->bottom-disabled (not (pos? scroll-bottom))))) 267 | 500)] 268 | 269 | (use-effect 270 | [title model-name] 271 | (j/assoc! js/document :title 272 | (if (seq model-name) 273 | (str model-name " : " (or title "New Chat") " — Chat Ollama") 274 | "Chat Ollama"))) 275 | 276 | (use-effect 277 | [@ref!] 278 | (let [slow-change (throttle #(let [{:keys [scroll-bottom]} (get-scroll-info)] 279 | (when (<= scroll-bottom line-height) 280 | (->bottom))) 281 | 250) 282 | observer (new js/MutationObserver slow-change)] 283 | (when (some? @ref!) 284 | (j/call observer :observe @ref! #js{:childList true :subtree true :characterData true})) 285 | #(j/call observer :disconnect))) 286 | 287 | (use-effect 288 | [(count exchanges)] 289 | (when (some? @ref!) 290 | (j/assoc! @ref! 291 | :scrollTop 292 | (j/get @ref! :scrollHeight)))) 293 | 294 | (useHotkeys "ctrl+shift+up" ->top) 295 | (useHotkeys "ctrl+shift+down" ->bottom) 296 | 297 | ($ :div {:class ["flex" "flex-col" "relative" "w-full" "h-screen"]} 298 | ($ :div {:class ["dark:block" "hidden" "z-20" "absolute" "top-0" "inset-x-0" "h-9" 299 | "bg-gradient-to-t" "dark:to-gray-900" "to-white" "from-transparent" "pointer-events-none"]}) 300 | ($ :div {:class ["absolute" "top-4" "right-4" "z-30" "flex" "flex-col"]} 301 | ($ IconButton {:on-click #(dispatch [:delete-dialog selected-dialog]) 302 | :icon Trash2}) 303 | ($ IconButton {:on-click ->top 304 | :disabled? ->top-disabled? 305 | :icon ArrowUpToLine}) 306 | ($ IconButton {:on-click ->bottom 307 | :disabled? ->bottom-disabled? 308 | :icon ArrowDownToLine})) 309 | ($ :div {:ref ref! 310 | :class ["relative" "grow" "flex" "flex-col" "w-full" "overflow-scroll"] 311 | :on-scroll slow-on-scroll} 312 | (when (some? selected-model) 313 | ($ :p {:class ["text-sm" "dark:text-gray-100" "text-gray-600" "text-center" "p-6"]} 314 | model-name 315 | ($ :span {:class ["opacity-50"]} ":" model-version))) 316 | (if (some? selected-model) 317 | ($ :div {:class ["flex" "flex-col" "w-full" "grow" "max-w-6xl" "mx-auto" "justify-end" "pt-6" "px-20" "pb-28"]} 318 | (for [exchange-uuid exchanges] 319 | ($ Exchange {:key exchange-uuid 320 | :dialog-uuid selected-dialog 321 | :exchange-uuid exchange-uuid}))) 322 | ($ StartDialog {:dialog-uuid selected-dialog}))) 323 | ($ Footer)))) 324 | 325 | (defnc SidebarItem [{:keys [selected? on-click children]}] 326 | (let [class #{"border-transparent"} 327 | selected-class #{"dark:text-white" "cursor-default" "bg-gray-300/20" "dark:bg-gray-800/50" "border-cyan-600"}] 328 | ($ :button {:class (vec (union #{"px-3" "py-1.5" "text-sm" "w-full" "text-left" "rounded" 329 | "dark:hover:bg-gray-800/60" "hover:bg-gray-300/20" "border-l-4"} 330 | (if selected? selected-class class))) 331 | :on-click on-click} 332 | children))) 333 | 334 | (defnc Sidebar [{:keys [toggle-sidebar! style]}] 335 | (let [selected-dialog (use-sub [:selected-dialog]) 336 | dialogs (use-sub [:dialog-list])] 337 | ($ :div {:style (j/assoc! style :width sidebar-width) 338 | :class ["dark:bg-gray-950" "bg-gray-50" 339 | "flex" "flex-col" "shrink-0" "p-6"]} 340 | ($ :div {:class ["flex" "items-center" "justify-between" "mb-4"]} 341 | ($ :div {:class ["flex" "items-center" "gap-3"]} 342 | ($ MessagesSquare) 343 | ($ :p {:class ["text-lg"]} 344 | "Chats" 345 | ($ :span {:class ["opacity-50" "ml-2"]} 346 | (count dialogs)))) 347 | ($ IconButton {:class ["-m-2"] 348 | :on-click toggle-sidebar! 349 | :icon PanelLeftClose})) 350 | ($ :div {:class ["grow" "overflow-scroll"]} 351 | ($ :ul {:class ["flex" "flex-col" "gap-y-1"]} 352 | (if (seq dialogs) 353 | (for [{:keys [uuid] :as dialog} dialogs] 354 | (let [selected? (= selected-dialog uuid) 355 | [model-name model-version] (str/split (:model-name dialog) #":")] 356 | ($ :li {:key uuid} 357 | ($ SidebarItem {:selected? selected? 358 | :on-click #(do 359 | (dispatch [:set-selected-dialog uuid]) 360 | (dispatch [:set-selected-model (:model-name dialog)]))} 361 | ($ :p {:class ["truncate" (when-not (:title dialog) "italic opacity-75")]} (or (:title dialog) "New Chat")) 362 | ($ :div {:class ["flex" "items-center" "justify-between"]} 363 | (when (:model-name dialog) 364 | ($ :p {:class ["text-xs" "dark:text-gray-100" "text-gray-600/80"]} 365 | model-name 366 | ($ :span {:class ["opacity-60" "grow"]} ":" model-version))) 367 | ($ :p {:class ["text-xs" "dark:text-gray-300/50" "text-gray-400"]} 368 | (formatDistance 369 | (fromUnixTime (:timestamp dialog)) 370 | (new js/Date) 371 | #js {:addSuffix true}))))))) 372 | ($ :p {:class ["dark:text-white/40"]} 373 | (str "No chats found"))))) 374 | ($ :button {:on-click #(dispatch [:new-dialog]) 375 | :class ["bg-cyan-600" "hover:bg-cyan-700" "text-white" "flex" "px-4" "py-2.5" 376 | "items-center" "rounded" "justify-between"]} 377 | "New Chat" 378 | ($ Plus))))) 379 | 380 | (defnc Offline [] 381 | (let [[copied copy!] (use-copy-to-clipboard) 382 | command (if (= "localhost" (j/get js/location :hostname)) 383 | "ollama serve" 384 | (str "OLLAMA_ORIGINS=" (j/get js/location :origin) " ollama serve"))] 385 | ($ :div {:class ["flex" "flex-col" "grow" "w-full" 386 | "justify-center" "items-center" 387 | "py-16" "h-full"]} 388 | ($ :div {:class ["flex" "flex-col" "grow" "w-full" "justify-center" "items-center"]} 389 | ($ OllamaAsleep) 390 | ($ :h1 {:class ["dark:text-white" "text-3xl" "mt-6"]} 391 | "Looks like Ollama is asleep!") 392 | ($ :h2 {:class ["text-lg" "dark:text-white/40" "text-gray-800/60" "mb-10"]} 393 | "Ollama Chat requires an active Ollama server to work") 394 | ($ :div {:class ["flex" "items-center" "rounded-md" "bg-gray-100" "dark:bg-white/5" "py-2" "pr-2" "pl-4" 395 | "dark:text-white" "font-mono" "text-sm"]} 396 | ($ :span {:class ["dark:text-white" "opacity-25" "mr-3" "select-none"]} "$") 397 | command 398 | ($ :button {:class ["ml-6" "p-2" "rounded-sm" "dark:hover:bg-gray-900" "hover:bg-gray-200"] 399 | :on-click #(copy! command)} 400 | (if copied 401 | ($ Check {:size 16}) 402 | ($ Clipboard {:size 16})))) 403 | ($ IconButton {:class ["mt-12"] 404 | :icon RefreshCw 405 | :on-click #(dispatch [:get-models])}))))) 406 | 407 | (defnc Dialogs [] 408 | (let [ollama-offline? (use-sub [:ollama-offline?]) 409 | selected-dialog (use-sub [:selected-dialog]) 410 | ls-sidebar? (local-storage-get (str ls-chat-ollama-prefs "sidebar?")) 411 | [show-sidebar? set-show-sidebar!] 412 | (use-state (if (some? ls-sidebar?) 413 | ls-sidebar? 414 | true)) 415 | toggle-sidebar! #(set-show-sidebar! not) 416 | sidebar-props (useSpring #js {:marginLeft 417 | (if show-sidebar? 418 | "0" 419 | (str (- sidebar-width) "px"))}) 420 | sidebar-icon-props (useSpring #js {:transform 421 | (if show-sidebar? 422 | (str "translateX(-300%)") 423 | (str "translateX(0%)"))}) 424 | AnimatedSidebar (animated Sidebar)] 425 | 426 | (useHotkeys "ctrl+n" #(dispatch [:new-dialog])) 427 | (useHotkeys "ctrl+d" toggle-sidebar!) 428 | 429 | (use-effect 430 | [show-sidebar?] 431 | (local-storage-set! 432 | (str ls-chat-ollama-prefs "sidebar?") 433 | show-sidebar?)) 434 | 435 | (if (some? ollama-offline?) 436 | ($ :div {:class ["flex" "dark:text-white" "relative" "w-full"]} 437 | (when ollama-offline? 438 | ($ :div {:class ["absolute" "inset-0" "dark:bg-gray-900/75" "bg-white/30" "backdrop-blur-md" "z-50" "w-full" "h-full"]} 439 | ($ Offline))) 440 | 441 | ($ AnimatedSidebar {:toggle-sidebar! toggle-sidebar! 442 | :style sidebar-props}) 443 | 444 | ($ (j/get animated :div) 445 | {:className "absolute top-0 left-0 p-4 z-30" 446 | :style sidebar-icon-props} 447 | ($ IconButton {:on-click toggle-sidebar! 448 | :icon PanelLeftOpen})) 449 | (when (some? selected-dialog) 450 | ($ Dialog))) 451 | (<>)))) 452 | 453 | (defnc Main [] 454 | 455 | (use-effect 456 | :once 457 | (dispatch [:get-models 2000])) 458 | 459 | ($ :div {:class ["flex" "w-full" "h-full" "relative"]} 460 | ($ Dialogs))) 461 | -------------------------------------------------------------------------------- /src/css/style.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @media (prefers-color-scheme: dark) { 6 | .markdown-body { 7 | color-scheme: dark; 8 | --color-prettylights-syntax-comment: #8b949e; 9 | --color-prettylights-syntax-constant: #79c0ff; 10 | --color-prettylights-syntax-entity: #d2a8ff; 11 | --color-prettylights-syntax-storage-modifier-import: #c9d1d9; 12 | --color-prettylights-syntax-entity-tag: #7ee787; 13 | --color-prettylights-syntax-keyword: #ff7b72; 14 | --color-prettylights-syntax-string: #a5d6ff; 15 | --color-prettylights-syntax-variable: #ffa657; 16 | --color-prettylights-syntax-brackethighlighter-unmatched: #f85149; 17 | --color-prettylights-syntax-invalid-illegal-text: #f0f6fc; 18 | --color-prettylights-syntax-invalid-illegal-bg: #8e1519; 19 | --color-prettylights-syntax-carriage-return-text: #f0f6fc; 20 | --color-prettylights-syntax-carriage-return-bg: #b62324; 21 | --color-prettylights-syntax-string-regexp: #7ee787; 22 | --color-prettylights-syntax-markup-list: #f2cc60; 23 | --color-prettylights-syntax-markup-heading: #1f6feb; 24 | --color-prettylights-syntax-markup-italic: #c9d1d9; 25 | --color-prettylights-syntax-markup-bold: #c9d1d9; 26 | --color-prettylights-syntax-markup-deleted-text: #ffdcd7; 27 | --color-prettylights-syntax-markup-deleted-bg: #67060c; 28 | --color-prettylights-syntax-markup-inserted-text: #aff5b4; 29 | --color-prettylights-syntax-markup-inserted-bg: #033a16; 30 | --color-prettylights-syntax-markup-changed-text: #ffdfb6; 31 | --color-prettylights-syntax-markup-changed-bg: #5a1e02; 32 | --color-prettylights-syntax-markup-ignored-text: #c9d1d9; 33 | --color-prettylights-syntax-markup-ignored-bg: #1158c7; 34 | --color-prettylights-syntax-meta-diff-range: #d2a8ff; 35 | --color-prettylights-syntax-brackethighlighter-angle: #8b949e; 36 | --color-prettylights-syntax-sublimelinter-gutter-mark: #484f58; 37 | --color-prettylights-syntax-constant-other-reference-link: #a5d6ff; 38 | --color-fg-default: #c9d1d9; 39 | --color-fg-muted: #8b949e; 40 | --color-fg-subtle: #6e7681; 41 | --color-canvas-default: #0d1117; 42 | --color-canvas-subtle: #161b22; 43 | --color-border-default: #30363d; 44 | --color-border-muted: #21262d; 45 | --color-neutral-muted: rgba(110, 118, 129, 0.4); 46 | --color-accent-fg: #58a6ff; 47 | --color-accent-emphasis: #1f6feb; 48 | --color-attention-subtle: rgba(187, 128, 9, 0.15); 49 | --color-danger-fg: #f85149; 50 | } 51 | } 52 | 53 | @media (prefers-color-scheme: light) { 54 | .markdown-body { 55 | color-scheme: light; 56 | --color-prettylights-syntax-comment: #6e7781; 57 | --color-prettylights-syntax-constant: #0550ae; 58 | --color-prettylights-syntax-entity: #8250df; 59 | --color-prettylights-syntax-storage-modifier-import: #24292f; 60 | --color-prettylights-syntax-entity-tag: #116329; 61 | --color-prettylights-syntax-keyword: #cf222e; 62 | --color-prettylights-syntax-string: #0a3069; 63 | --color-prettylights-syntax-variable: #953800; 64 | --color-prettylights-syntax-brackethighlighter-unmatched: #82071e; 65 | --color-prettylights-syntax-invalid-illegal-text: #f6f8fa; 66 | --color-prettylights-syntax-invalid-illegal-bg: #82071e; 67 | --color-prettylights-syntax-carriage-return-text: #f6f8fa; 68 | --color-prettylights-syntax-carriage-return-bg: #cf222e; 69 | --color-prettylights-syntax-string-regexp: #116329; 70 | --color-prettylights-syntax-markup-list: #3b2300; 71 | --color-prettylights-syntax-markup-heading: #0550ae; 72 | --color-prettylights-syntax-markup-italic: #24292f; 73 | --color-prettylights-syntax-markup-bold: #24292f; 74 | --color-prettylights-syntax-markup-deleted-text: #82071e; 75 | --color-prettylights-syntax-markup-deleted-bg: #ffebe9; 76 | --color-prettylights-syntax-markup-inserted-text: #116329; 77 | --color-prettylights-syntax-markup-inserted-bg: #dafbe1; 78 | --color-prettylights-syntax-markup-changed-text: #953800; 79 | --color-prettylights-syntax-markup-changed-bg: #ffd8b5; 80 | --color-prettylights-syntax-markup-ignored-text: #eaeef2; 81 | --color-prettylights-syntax-markup-ignored-bg: #0550ae; 82 | --color-prettylights-syntax-meta-diff-range: #8250df; 83 | --color-prettylights-syntax-brackethighlighter-angle: #57606a; 84 | --color-prettylights-syntax-sublimelinter-gutter-mark: #8c959f; 85 | --color-prettylights-syntax-constant-other-reference-link: #0a3069; 86 | --color-fg-default: #24292f; 87 | --color-fg-muted: #57606a; 88 | --color-fg-subtle: #6e7781; 89 | --color-canvas-default: #ffffff; 90 | --color-canvas-subtle: #f6f8fa; 91 | --color-border-default: #d0d7de; 92 | --color-border-muted: hsla(210, 18%, 87%, 1); 93 | --color-neutral-muted: rgba(175, 184, 193, 0.2); 94 | --color-accent-fg: #0969da; 95 | --color-accent-emphasis: #0969da; 96 | --color-attention-subtle: #fff8c5; 97 | --color-danger-fg: #cf222e; 98 | } 99 | } 100 | 101 | .markdown-body { 102 | -ms-text-size-adjust: 100%; 103 | -webkit-text-size-adjust: 100%; 104 | margin: 0; 105 | color: var(--color-fg-default); 106 | /* background-color: var(--color-canvas-default); */ 107 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"; 108 | font-size: 16px; 109 | line-height: 1.5; 110 | word-wrap: break-word; 111 | } 112 | 113 | .markdown-body .octicon { 114 | display: inline-block; 115 | fill: currentColor; 116 | vertical-align: text-bottom; 117 | } 118 | 119 | .markdown-body h1:hover .anchor .octicon-link:before, 120 | .markdown-body h2:hover .anchor .octicon-link:before, 121 | .markdown-body h3:hover .anchor .octicon-link:before, 122 | .markdown-body h4:hover .anchor .octicon-link:before, 123 | .markdown-body h5:hover .anchor .octicon-link:before, 124 | .markdown-body h6:hover .anchor .octicon-link:before { 125 | width: 16px; 126 | height: 16px; 127 | content: ' '; 128 | display: inline-block; 129 | background-color: currentColor; 130 | -webkit-mask-image: url("data:image/svg+xml,"); 131 | mask-image: url("data:image/svg+xml,"); 132 | } 133 | 134 | .markdown-body details, 135 | .markdown-body figcaption, 136 | .markdown-body figure { 137 | display: block; 138 | } 139 | 140 | .markdown-body summary { 141 | display: list-item; 142 | } 143 | 144 | .markdown-body [hidden] { 145 | display: none !important; 146 | } 147 | 148 | .markdown-body a { 149 | background-color: transparent; 150 | color: var(--color-accent-fg); 151 | text-decoration: none; 152 | } 153 | 154 | .markdown-body abbr[title] { 155 | border-bottom: none; 156 | text-decoration: underline dotted; 157 | } 158 | 159 | .markdown-body b, 160 | .markdown-body strong { 161 | font-weight: var(--base-text-weight-semibold, 600); 162 | } 163 | 164 | .markdown-body dfn { 165 | font-style: italic; 166 | } 167 | 168 | .markdown-body h1 { 169 | margin: .67em 0; 170 | font-weight: var(--base-text-weight-semibold, 600); 171 | padding-bottom: .3em; 172 | font-size: 2em; 173 | border-bottom: 1px solid var(--color-border-muted); 174 | } 175 | 176 | .markdown-body mark { 177 | background-color: var(--color-attention-subtle); 178 | color: var(--color-fg-default); 179 | } 180 | 181 | .markdown-body small { 182 | font-size: 90%; 183 | } 184 | 185 | .markdown-body sub, 186 | .markdown-body sup { 187 | font-size: 75%; 188 | line-height: 0; 189 | position: relative; 190 | vertical-align: baseline; 191 | } 192 | 193 | .markdown-body sub { 194 | bottom: -0.25em; 195 | } 196 | 197 | .markdown-body sup { 198 | top: -0.5em; 199 | } 200 | 201 | .markdown-body img { 202 | border-style: none; 203 | max-width: 100%; 204 | box-sizing: content-box; 205 | background-color: var(--color-canvas-default); 206 | } 207 | 208 | .markdown-body code, 209 | .markdown-body kbd, 210 | .markdown-body pre, 211 | .markdown-body samp { 212 | font-family: monospace; 213 | font-size: 1em; 214 | } 215 | 216 | .markdown-body figure { 217 | margin: 1em 40px; 218 | } 219 | 220 | .markdown-body hr { 221 | box-sizing: content-box; 222 | overflow: hidden; 223 | background: transparent; 224 | border-bottom: 1px solid var(--color-border-muted); 225 | height: .25em; 226 | padding: 0; 227 | margin: 24px 0; 228 | background-color: var(--color-border-default); 229 | border: 0; 230 | } 231 | 232 | .markdown-body input { 233 | font: inherit; 234 | margin: 0; 235 | overflow: visible; 236 | font-family: inherit; 237 | font-size: inherit; 238 | line-height: inherit; 239 | } 240 | 241 | .markdown-body [type=button], 242 | .markdown-body [type=reset], 243 | .markdown-body [type=submit] { 244 | -webkit-appearance: button; 245 | } 246 | 247 | .markdown-body [type=checkbox], 248 | .markdown-body [type=radio] { 249 | box-sizing: border-box; 250 | padding: 0; 251 | } 252 | 253 | .markdown-body [type=number]::-webkit-inner-spin-button, 254 | .markdown-body [type=number]::-webkit-outer-spin-button { 255 | height: auto; 256 | } 257 | 258 | .markdown-body [type=search]::-webkit-search-cancel-button, 259 | .markdown-body [type=search]::-webkit-search-decoration { 260 | -webkit-appearance: none; 261 | } 262 | 263 | .markdown-body ::-webkit-input-placeholder { 264 | color: inherit; 265 | opacity: .54; 266 | } 267 | 268 | .markdown-body ::-webkit-file-upload-button { 269 | -webkit-appearance: button; 270 | font: inherit; 271 | } 272 | 273 | .markdown-body a:hover { 274 | text-decoration: underline; 275 | } 276 | 277 | .markdown-body ::placeholder { 278 | color: var(--color-fg-subtle); 279 | opacity: 1; 280 | } 281 | 282 | .markdown-body hr::before { 283 | display: table; 284 | content: ""; 285 | } 286 | 287 | .markdown-body hr::after { 288 | display: table; 289 | clear: both; 290 | content: ""; 291 | } 292 | 293 | .markdown-body table { 294 | border-spacing: 0; 295 | border-collapse: collapse; 296 | display: block; 297 | width: max-content; 298 | max-width: 100%; 299 | overflow: auto; 300 | } 301 | 302 | .markdown-body td, 303 | .markdown-body th { 304 | padding: 0; 305 | } 306 | 307 | .markdown-body details summary { 308 | cursor: pointer; 309 | } 310 | 311 | .markdown-body details:not([open])>*:not(summary) { 312 | display: none !important; 313 | } 314 | 315 | .markdown-body a:focus, 316 | .markdown-body [role=button]:focus, 317 | .markdown-body input[type=radio]:focus, 318 | .markdown-body input[type=checkbox]:focus { 319 | outline: 2px solid var(--color-accent-fg); 320 | outline-offset: -2px; 321 | box-shadow: none; 322 | } 323 | 324 | .markdown-body a:focus:not(:focus-visible), 325 | .markdown-body [role=button]:focus:not(:focus-visible), 326 | .markdown-body input[type=radio]:focus:not(:focus-visible), 327 | .markdown-body input[type=checkbox]:focus:not(:focus-visible) { 328 | outline: solid 1px transparent; 329 | } 330 | 331 | .markdown-body a:focus-visible, 332 | .markdown-body [role=button]:focus-visible, 333 | .markdown-body input[type=radio]:focus-visible, 334 | .markdown-body input[type=checkbox]:focus-visible { 335 | outline: 2px solid var(--color-accent-fg); 336 | outline-offset: -2px; 337 | box-shadow: none; 338 | } 339 | 340 | .markdown-body a:not([class]):focus, 341 | .markdown-body a:not([class]):focus-visible, 342 | .markdown-body input[type=radio]:focus, 343 | .markdown-body input[type=radio]:focus-visible, 344 | .markdown-body input[type=checkbox]:focus, 345 | .markdown-body input[type=checkbox]:focus-visible { 346 | outline-offset: 0; 347 | } 348 | 349 | .markdown-body kbd { 350 | display: inline-block; 351 | padding: 3px 5px; 352 | font: 11px ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace; 353 | line-height: 10px; 354 | color: var(--color-fg-default); 355 | vertical-align: middle; 356 | background-color: var(--color-canvas-subtle); 357 | border: solid 1px var(--color-neutral-muted); 358 | border-bottom-color: var(--color-neutral-muted); 359 | border-radius: 6px; 360 | box-shadow: inset 0 -1px 0 var(--color-neutral-muted); 361 | } 362 | 363 | .markdown-body h1, 364 | .markdown-body h2, 365 | .markdown-body h3, 366 | .markdown-body h4, 367 | .markdown-body h5, 368 | .markdown-body h6 { 369 | margin-top: 24px; 370 | margin-bottom: 16px; 371 | font-weight: var(--base-text-weight-semibold, 600); 372 | line-height: 1.25; 373 | } 374 | 375 | .markdown-body h2 { 376 | font-weight: var(--base-text-weight-semibold, 600); 377 | padding-bottom: .3em; 378 | font-size: 1.5em; 379 | border-bottom: 1px solid var(--color-border-muted); 380 | } 381 | 382 | .markdown-body h3 { 383 | font-weight: var(--base-text-weight-semibold, 600); 384 | font-size: 1.25em; 385 | } 386 | 387 | .markdown-body h4 { 388 | font-weight: var(--base-text-weight-semibold, 600); 389 | font-size: 1em; 390 | } 391 | 392 | .markdown-body h5 { 393 | font-weight: var(--base-text-weight-semibold, 600); 394 | font-size: .875em; 395 | } 396 | 397 | .markdown-body h6 { 398 | font-weight: var(--base-text-weight-semibold, 600); 399 | font-size: .85em; 400 | color: var(--color-fg-muted); 401 | } 402 | 403 | .markdown-body p { 404 | margin-top: 0; 405 | margin-bottom: 10px; 406 | } 407 | 408 | .markdown-body blockquote { 409 | margin: 0; 410 | padding: 0 1em; 411 | color: var(--color-fg-muted); 412 | border-left: .25em solid var(--color-border-default); 413 | } 414 | 415 | .markdown-body ul, 416 | .markdown-body ol { 417 | margin-top: 0; 418 | margin-bottom: 0; 419 | padding-left: 1rem; 420 | } 421 | 422 | .markdown-body ul { 423 | list-style: disc; 424 | } 425 | 426 | .markdown-body ol { 427 | list-style: decimal; 428 | } 429 | 430 | .markdown-body ol ol, 431 | .markdown-body ul ol { 432 | list-style: lower-roman; 433 | } 434 | 435 | .markdown-body ul ul ol, 436 | .markdown-body ul ol ol, 437 | .markdown-body ol ul ol, 438 | .markdown-body ol ol ol { 439 | list-style: lower-alpha; 440 | } 441 | 442 | .markdown-body dd { 443 | margin-left: 0; 444 | } 445 | 446 | .markdown-body tt, 447 | .markdown-body code, 448 | .markdown-body samp { 449 | font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace; 450 | font-size: 12px; 451 | } 452 | 453 | .markdown-body .octicon { 454 | display: inline-block; 455 | overflow: visible !important; 456 | vertical-align: text-bottom; 457 | fill: currentColor; 458 | } 459 | 460 | .markdown-body input::-webkit-outer-spin-button, 461 | .markdown-body input::-webkit-inner-spin-button { 462 | margin: 0; 463 | -webkit-appearance: none; 464 | appearance: none; 465 | } 466 | 467 | .markdown-body::before { 468 | display: table; 469 | content: ""; 470 | } 471 | 472 | .markdown-body::after { 473 | display: table; 474 | clear: both; 475 | content: ""; 476 | } 477 | 478 | .markdown-body>*:first-child { 479 | margin-top: 0 !important; 480 | } 481 | 482 | .markdown-body>*:last-child { 483 | margin-bottom: 0 !important; 484 | } 485 | 486 | .markdown-body a:not([href]) { 487 | color: inherit; 488 | text-decoration: none; 489 | } 490 | 491 | .markdown-body .absent { 492 | color: var(--color-danger-fg); 493 | } 494 | 495 | .markdown-body .anchor { 496 | float: left; 497 | padding-right: 4px; 498 | margin-left: -20px; 499 | line-height: 1; 500 | } 501 | 502 | .markdown-body .anchor:focus { 503 | outline: none; 504 | } 505 | 506 | .markdown-body p, 507 | .markdown-body blockquote, 508 | .markdown-body ul, 509 | .markdown-body ol, 510 | .markdown-body dl, 511 | .markdown-body table, 512 | .markdown-body>pre, 513 | .markdown-body details { 514 | margin-top: 0; 515 | margin-bottom: 16px; 516 | } 517 | 518 | .markdown-body blockquote>:first-child { 519 | margin-top: 0; 520 | } 521 | 522 | .markdown-body blockquote>:last-child { 523 | margin-bottom: 0; 524 | } 525 | 526 | .markdown-body h1 .octicon-link, 527 | .markdown-body h2 .octicon-link, 528 | .markdown-body h3 .octicon-link, 529 | .markdown-body h4 .octicon-link, 530 | .markdown-body h5 .octicon-link, 531 | .markdown-body h6 .octicon-link { 532 | color: var(--color-fg-default); 533 | vertical-align: middle; 534 | visibility: hidden; 535 | } 536 | 537 | .markdown-body h1:hover .anchor, 538 | .markdown-body h2:hover .anchor, 539 | .markdown-body h3:hover .anchor, 540 | .markdown-body h4:hover .anchor, 541 | .markdown-body h5:hover .anchor, 542 | .markdown-body h6:hover .anchor { 543 | text-decoration: none; 544 | } 545 | 546 | .markdown-body h1:hover .anchor .octicon-link, 547 | .markdown-body h2:hover .anchor .octicon-link, 548 | .markdown-body h3:hover .anchor .octicon-link, 549 | .markdown-body h4:hover .anchor .octicon-link, 550 | .markdown-body h5:hover .anchor .octicon-link, 551 | .markdown-body h6:hover .anchor .octicon-link { 552 | visibility: visible; 553 | } 554 | 555 | .markdown-body h1 tt, 556 | .markdown-body h1 code, 557 | .markdown-body h2 tt, 558 | .markdown-body h2 code, 559 | .markdown-body h3 tt, 560 | .markdown-body h3 code, 561 | .markdown-body h4 tt, 562 | .markdown-body h4 code, 563 | .markdown-body h5 tt, 564 | .markdown-body h5 code, 565 | .markdown-body h6 tt, 566 | .markdown-body h6 code { 567 | padding: 0 .2em; 568 | font-size: inherit; 569 | } 570 | 571 | .markdown-body summary h1, 572 | .markdown-body summary h2, 573 | .markdown-body summary h3, 574 | .markdown-body summary h4, 575 | .markdown-body summary h5, 576 | .markdown-body summary h6 { 577 | display: inline-block; 578 | } 579 | 580 | .markdown-body summary h1 .anchor, 581 | .markdown-body summary h2 .anchor, 582 | .markdown-body summary h3 .anchor, 583 | .markdown-body summary h4 .anchor, 584 | .markdown-body summary h5 .anchor, 585 | .markdown-body summary h6 .anchor { 586 | margin-left: -40px; 587 | } 588 | 589 | .markdown-body summary h1, 590 | .markdown-body summary h2 { 591 | padding-bottom: 0; 592 | border-bottom: 0; 593 | } 594 | 595 | .markdown-body ul.no-list, 596 | .markdown-body ol.no-list { 597 | padding: 0; 598 | list-style: none; 599 | } 600 | 601 | .markdown-body ol[type=a] { 602 | list-style: lower-alpha; 603 | } 604 | 605 | .markdown-body ol[type=A] { 606 | list-style: upper-alpha; 607 | } 608 | 609 | .markdown-body ol[type=i] { 610 | list-style: lower-roman; 611 | } 612 | 613 | .markdown-body ol[type=I] { 614 | list-style: upper-roman; 615 | } 616 | 617 | .markdown-body ol[type="1"] { 618 | list-style: decimal; 619 | } 620 | 621 | .markdown-body div>ol:not([type]) { 622 | list-style: decimal; 623 | } 624 | 625 | .markdown-body ul ul, 626 | .markdown-body ul ol, 627 | .markdown-body ol ol, 628 | .markdown-body ol ul { 629 | margin-top: 0; 630 | margin-bottom: 0; 631 | } 632 | 633 | .markdown-body li>p { 634 | margin-top: 16px; 635 | } 636 | 637 | .markdown-body li+li { 638 | margin-top: .25em; 639 | } 640 | 641 | .markdown-body dl { 642 | padding: 0; 643 | } 644 | 645 | .markdown-body dl dt { 646 | padding: 0; 647 | margin-top: 16px; 648 | font-size: 1em; 649 | font-style: italic; 650 | font-weight: var(--base-text-weight-semibold, 600); 651 | } 652 | 653 | .markdown-body dl dd { 654 | padding: 0 16px; 655 | margin-bottom: 16px; 656 | } 657 | 658 | .markdown-body table th { 659 | font-weight: var(--base-text-weight-semibold, 600); 660 | } 661 | 662 | .markdown-body table th, 663 | .markdown-body table td { 664 | padding: 6px 13px; 665 | border: 1px solid var(--color-border-default); 666 | } 667 | 668 | .markdown-body table tr { 669 | background-color: var(--color-canvas-default); 670 | border-top: 1px solid var(--color-border-muted); 671 | } 672 | 673 | .markdown-body table tr:nth-child(2n) { 674 | background-color: var(--color-canvas-subtle); 675 | } 676 | 677 | .markdown-body table img { 678 | background-color: transparent; 679 | } 680 | 681 | .markdown-body img[align=right] { 682 | padding-left: 20px; 683 | } 684 | 685 | .markdown-body img[align=left] { 686 | padding-right: 20px; 687 | } 688 | 689 | .markdown-body .emoji { 690 | max-width: none; 691 | vertical-align: text-top; 692 | background-color: transparent; 693 | } 694 | 695 | .markdown-body span.frame { 696 | display: block; 697 | overflow: hidden; 698 | } 699 | 700 | .markdown-body span.frame>span { 701 | display: block; 702 | float: left; 703 | width: auto; 704 | padding: 7px; 705 | margin: 13px 0 0; 706 | overflow: hidden; 707 | border: 1px solid var(--color-border-default); 708 | } 709 | 710 | .markdown-body span.frame span img { 711 | display: block; 712 | float: left; 713 | } 714 | 715 | .markdown-body span.frame span span { 716 | display: block; 717 | padding: 5px 0 0; 718 | clear: both; 719 | color: var(--color-fg-default); 720 | } 721 | 722 | .markdown-body span.align-center { 723 | display: block; 724 | overflow: hidden; 725 | clear: both; 726 | } 727 | 728 | .markdown-body span.align-center>span { 729 | display: block; 730 | margin: 13px auto 0; 731 | overflow: hidden; 732 | text-align: center; 733 | } 734 | 735 | .markdown-body span.align-center span img { 736 | margin: 0 auto; 737 | text-align: center; 738 | } 739 | 740 | .markdown-body span.align-right { 741 | display: block; 742 | overflow: hidden; 743 | clear: both; 744 | } 745 | 746 | .markdown-body span.align-right>span { 747 | display: block; 748 | margin: 13px 0 0; 749 | overflow: hidden; 750 | text-align: right; 751 | } 752 | 753 | .markdown-body span.align-right span img { 754 | margin: 0; 755 | text-align: right; 756 | } 757 | 758 | .markdown-body span.float-left { 759 | display: block; 760 | float: left; 761 | margin-right: 13px; 762 | overflow: hidden; 763 | } 764 | 765 | .markdown-body span.float-left span { 766 | margin: 13px 0 0; 767 | } 768 | 769 | .markdown-body span.float-right { 770 | display: block; 771 | float: right; 772 | margin-left: 13px; 773 | overflow: hidden; 774 | } 775 | 776 | .markdown-body span.float-right>span { 777 | display: block; 778 | margin: 13px auto 0; 779 | overflow: hidden; 780 | text-align: right; 781 | } 782 | 783 | .markdown-body code, 784 | .markdown-body tt { 785 | padding: .2em .4em; 786 | margin: 0; 787 | font-size: 85%; 788 | white-space: break-spaces; 789 | background-color: var(--color-neutral-muted); 790 | border-radius: 6px; 791 | } 792 | 793 | .markdown-body code br, 794 | .markdown-body tt br { 795 | display: none; 796 | } 797 | 798 | .markdown-body del code { 799 | text-decoration: inherit; 800 | } 801 | 802 | .markdown-body samp { 803 | font-size: 85%; 804 | } 805 | 806 | .markdown-body pre code { 807 | font-size: 100%; 808 | } 809 | 810 | .markdown-body pre>code { 811 | padding: 0; 812 | margin: 0; 813 | word-break: normal; 814 | white-space: pre; 815 | background: transparent; 816 | border: 0; 817 | } 818 | 819 | .markdown-body .highlight { 820 | margin-bottom: 16px; 821 | } 822 | 823 | .markdown-body .highlight pre { 824 | margin-bottom: 0; 825 | word-break: normal; 826 | } 827 | 828 | .markdown-body .highlight pre, 829 | .markdown-body pre { 830 | font-size: 0.875rem; 831 | position: relative; 832 | } 833 | 834 | .markdown-body pre code, 835 | .markdown-body pre tt { 836 | display: inline; 837 | max-width: auto; 838 | padding: 0; 839 | margin: 0; 840 | overflow: visible; 841 | line-height: inherit; 842 | word-wrap: normal; 843 | background-color: transparent; 844 | border: 0; 845 | } 846 | 847 | .markdown-body .csv-data td, 848 | .markdown-body .csv-data th { 849 | padding: 5px; 850 | overflow: hidden; 851 | font-size: 12px; 852 | line-height: 1; 853 | text-align: left; 854 | white-space: nowrap; 855 | } 856 | 857 | .markdown-body .csv-data .blob-num { 858 | padding: 10px 8px 9px; 859 | text-align: right; 860 | background: var(--color-canvas-default); 861 | border: 0; 862 | } 863 | 864 | .markdown-body .csv-data tr { 865 | border-top: 0; 866 | } 867 | 868 | .markdown-body .csv-data th { 869 | font-weight: var(--base-text-weight-semibold, 600); 870 | background: var(--color-canvas-subtle); 871 | border-top: 0; 872 | } 873 | 874 | .markdown-body [data-footnote-ref]::before { 875 | content: "["; 876 | } 877 | 878 | .markdown-body [data-footnote-ref]::after { 879 | content: "]"; 880 | } 881 | 882 | .markdown-body .footnotes { 883 | font-size: 12px; 884 | color: var(--color-fg-muted); 885 | border-top: 1px solid var(--color-border-default); 886 | } 887 | 888 | .markdown-body .footnotes ol { 889 | padding-left: 16px; 890 | } 891 | 892 | .markdown-body .footnotes ol ul { 893 | display: inline-block; 894 | padding-left: 16px; 895 | margin-top: 16px; 896 | } 897 | 898 | .markdown-body .footnotes li { 899 | position: relative; 900 | } 901 | 902 | .markdown-body .footnotes li:target::before { 903 | position: absolute; 904 | top: -8px; 905 | right: -8px; 906 | bottom: -8px; 907 | left: -24px; 908 | pointer-events: none; 909 | content: ""; 910 | border: 2px solid var(--color-accent-emphasis); 911 | border-radius: 6px; 912 | } 913 | 914 | .markdown-body .footnotes li:target { 915 | color: var(--color-fg-default); 916 | } 917 | 918 | .markdown-body .footnotes .data-footnote-backref g-emoji { 919 | font-family: monospace; 920 | } 921 | 922 | .markdown-body .pl-c { 923 | color: var(--color-prettylights-syntax-comment); 924 | } 925 | 926 | .markdown-body .pl-c1, 927 | .markdown-body .pl-s .pl-v { 928 | color: var(--color-prettylights-syntax-constant); 929 | } 930 | 931 | .markdown-body .pl-e, 932 | .markdown-body .pl-en { 933 | color: var(--color-prettylights-syntax-entity); 934 | } 935 | 936 | .markdown-body .pl-smi, 937 | .markdown-body .pl-s .pl-s1 { 938 | color: var(--color-prettylights-syntax-storage-modifier-import); 939 | } 940 | 941 | .markdown-body .pl-ent { 942 | color: var(--color-prettylights-syntax-entity-tag); 943 | } 944 | 945 | .markdown-body .pl-k { 946 | color: var(--color-prettylights-syntax-keyword); 947 | } 948 | 949 | .markdown-body .pl-s, 950 | .markdown-body .pl-pds, 951 | .markdown-body .pl-s .pl-pse .pl-s1, 952 | .markdown-body .pl-sr, 953 | .markdown-body .pl-sr .pl-cce, 954 | .markdown-body .pl-sr .pl-sre, 955 | .markdown-body .pl-sr .pl-sra { 956 | color: var(--color-prettylights-syntax-string); 957 | } 958 | 959 | .markdown-body .pl-v, 960 | .markdown-body .pl-smw { 961 | color: var(--color-prettylights-syntax-variable); 962 | } 963 | 964 | .markdown-body .pl-bu { 965 | color: var(--color-prettylights-syntax-brackethighlighter-unmatched); 966 | } 967 | 968 | .markdown-body .pl-ii { 969 | color: var(--color-prettylights-syntax-invalid-illegal-text); 970 | background-color: var(--color-prettylights-syntax-invalid-illegal-bg); 971 | } 972 | 973 | .markdown-body .pl-c2 { 974 | color: var(--color-prettylights-syntax-carriage-return-text); 975 | background-color: var(--color-prettylights-syntax-carriage-return-bg); 976 | } 977 | 978 | .markdown-body .pl-sr .pl-cce { 979 | font-weight: bold; 980 | color: var(--color-prettylights-syntax-string-regexp); 981 | } 982 | 983 | .markdown-body .pl-ml { 984 | color: var(--color-prettylights-syntax-markup-list); 985 | } 986 | 987 | .markdown-body .pl-mh, 988 | .markdown-body .pl-mh .pl-en, 989 | .markdown-body .pl-ms { 990 | font-weight: bold; 991 | color: var(--color-prettylights-syntax-markup-heading); 992 | } 993 | 994 | .markdown-body .pl-mi { 995 | font-style: italic; 996 | color: var(--color-prettylights-syntax-markup-italic); 997 | } 998 | 999 | .markdown-body .pl-mb { 1000 | font-weight: bold; 1001 | color: var(--color-prettylights-syntax-markup-bold); 1002 | } 1003 | 1004 | .markdown-body .pl-md { 1005 | color: var(--color-prettylights-syntax-markup-deleted-text); 1006 | background-color: var(--color-prettylights-syntax-markup-deleted-bg); 1007 | } 1008 | 1009 | .markdown-body .pl-mi1 { 1010 | color: var(--color-prettylights-syntax-markup-inserted-text); 1011 | background-color: var(--color-prettylights-syntax-markup-inserted-bg); 1012 | } 1013 | 1014 | .markdown-body .pl-mc { 1015 | color: var(--color-prettylights-syntax-markup-changed-text); 1016 | background-color: var(--color-prettylights-syntax-markup-changed-bg); 1017 | } 1018 | 1019 | .markdown-body .pl-mi2 { 1020 | color: var(--color-prettylights-syntax-markup-ignored-text); 1021 | background-color: var(--color-prettylights-syntax-markup-ignored-bg); 1022 | } 1023 | 1024 | .markdown-body .pl-mdr { 1025 | font-weight: bold; 1026 | color: var(--color-prettylights-syntax-meta-diff-range); 1027 | } 1028 | 1029 | .markdown-body .pl-ba { 1030 | color: var(--color-prettylights-syntax-brackethighlighter-angle); 1031 | } 1032 | 1033 | .markdown-body .pl-sg { 1034 | color: var(--color-prettylights-syntax-sublimelinter-gutter-mark); 1035 | } 1036 | 1037 | .markdown-body .pl-corl { 1038 | text-decoration: underline; 1039 | color: var(--color-prettylights-syntax-constant-other-reference-link); 1040 | } 1041 | 1042 | .markdown-body g-emoji { 1043 | display: inline-block; 1044 | min-width: 1ch; 1045 | font-family: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; 1046 | font-size: 1em; 1047 | font-style: normal !important; 1048 | font-weight: var(--base-text-weight-normal, 400); 1049 | line-height: 1; 1050 | vertical-align: -0.075em; 1051 | } 1052 | 1053 | .markdown-body g-emoji img { 1054 | width: 1em; 1055 | height: 1em; 1056 | } 1057 | 1058 | .markdown-body .task-list-item { 1059 | list-style: none; 1060 | } 1061 | 1062 | .markdown-body .task-list-item label { 1063 | font-weight: var(--base-text-weight-normal, 400); 1064 | } 1065 | 1066 | .markdown-body .task-list-item.enabled label { 1067 | cursor: pointer; 1068 | } 1069 | 1070 | .markdown-body .task-list-item+.task-list-item { 1071 | margin-top: 4px; 1072 | } 1073 | 1074 | .markdown-body .task-list-item .handle { 1075 | display: none; 1076 | } 1077 | 1078 | .markdown-body .task-list-item-checkbox { 1079 | margin: 0 .2em .25em -1.4em; 1080 | vertical-align: middle; 1081 | } 1082 | 1083 | .markdown-body .contains-task-list:dir(rtl) .task-list-item-checkbox { 1084 | margin: 0 -1.6em .25em .2em; 1085 | } 1086 | 1087 | .markdown-body .contains-task-list { 1088 | position: relative; 1089 | } 1090 | 1091 | .markdown-body .contains-task-list:hover .task-list-item-convert-container, 1092 | .markdown-body .contains-task-list:focus-within .task-list-item-convert-container { 1093 | display: block; 1094 | width: auto; 1095 | height: 24px; 1096 | overflow: visible; 1097 | clip: auto; 1098 | } 1099 | 1100 | .markdown-body ::-webkit-calendar-picker-indicator { 1101 | filter: invert(50%); 1102 | } 1103 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: ["./src/**/*.{html,js,cljs}"], 4 | theme: { 5 | extend: { 6 | "colors": { 7 | "gray": { 8 | 50: "#F3F5F7", 9 | 100: "#F0F1F5", 10 | 200: "#CED3DE", 11 | 300: "#AFB7CA", 12 | 400: "#919CB6", 13 | 500: "#7280A1", 14 | 600: "#586584", 15 | 700: "#434D65", 16 | 800: "#2F3646", 17 | 900: "#1A1E27", 18 | 950: "#161A22" 19 | } 20 | } 21 | } 22 | }, 23 | plugins: [], 24 | } 25 | --------------------------------------------------------------------------------