├── LICENSE ├── README.md ├── assets ├── logo.png ├── long_logo.png └── ui.png ├── docs └── iphone.md ├── index.css ├── index.html └── index.js /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TinySwallow ChatUI 2 | 📚 [Paper](https://arxiv.org/abs/2501.16937) | 🤗 [Hugging Face](https://huggingface.co/collections/SakanaAI/tinyswallow-676cf5e57fff9075b5ddb7ec) | 📝 [Blog](https://sakana.ai/taid-jp) 3 | 4 |
5 | ui 6 |
7 | 8 | TinySwallow ChatUIは、ブラウザ上で動作するローカルな言語モデルチャットアプリケーションです。 9 | 10 | **特徴:** 11 | 12 | - 📱外部APIなどを介さず、最初のモデルのダウンロードが完了すれば完全オフラインで会話することも可能です。 13 | - 🔐 すべての処理がローカルで実行されるため、データが外部に送信されることはありません。 14 | - ⚡️ 複雑なセットアップ不要で、ブラウザから直接チャットを始めることができます。 15 | 16 | すぐにチャットを始めたい場合は、[こちら](https://pub.sakana.ai/tinyswallow/)からお試しください。なお、このデモにおいても、チャットはお使いのPCで動作され、データがSakana AIに送られることはありません。 17 | 18 | ## 使い方 19 | 20 | ### 1. リポジトリのクローン 21 | 22 | ```bash 23 | git clone https://github.com/SakanaAI/TinySwallow-ChatUI.git 24 | cd TinySwallow-ChatUI 25 | ``` 26 | 27 | ### 2. HTTPサーバーを起動 28 | 29 | ターミナルを開き、HTTPサーバーを起動します。 30 | 例えば、Pythonを用いる場合は以下のコマンドを実行します。 31 | 32 | ```bash 33 | python -m http.server 8000 34 | ``` 35 | 36 | ⚠️ Windowsの場合、pythonを事前にインストールする必要があります。 37 | 38 | ### 3. ブラウザでアクセス 39 | 40 | 任意のブラウザで以下のURLにアクセスします。 41 | 42 | ``` 43 | http://localhost:8000 44 | ``` 45 | 46 | ## 完全なローカル実行が必要な場合 47 | 48 | このTinySwallow ChatUIでは、最初のモデルダウンロード時に、ネットワークが必要です。 49 | 50 | 完全にローカル環境でモデルを実行したい場合は、[TinySwallow-ChatUI-Local](https://github.com/SakanaAI/TinySwallow-ChatUI-Local)をご利用ください。このレポジトリでは、モデルファイルも含まれており、ネットワークを必要なく、ローカルでチャットが可能です。 51 | 52 | ## 利用上の注意 53 | 54 | 本アプリケーションは実験段階のプロトタイプであり、研究開発の目的でのみ提供されています。商用利用や、障害が重大な影響を及ぼす可能性のある環境(ミッションクリティカルな環境)での使用には適していません。 本アプリケーションの使用は、利用者の自己責任で行われ、その性能や結果については何ら保証されません。 Sakana AIは、本アプリケーションの使用によって生じた直接的または間接的な損失に対して、結果に関わらず、一切の責任を負いません。 利用者は、本アプリケーションの使用に伴うリスクを十分に理解し、自身の判断で使用することが必要です。 55 | -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SakanaAI/TinySwallow-ChatUI/913f0e25bc8ad691b5fe8ada5dcab9067cfc8424/assets/logo.png -------------------------------------------------------------------------------- /assets/long_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SakanaAI/TinySwallow-ChatUI/913f0e25bc8ad691b5fe8ada5dcab9067cfc8424/assets/long_logo.png -------------------------------------------------------------------------------- /assets/ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SakanaAI/TinySwallow-ChatUI/913f0e25bc8ad691b5fe8ada5dcab9067cfc8424/assets/ui.png -------------------------------------------------------------------------------- /docs/iphone.md: -------------------------------------------------------------------------------- 1 | # TinySwallowをiPhoneで使う 2 | 3 | このガイドでは、[小規模言語モデルTinySwallow](https://sakana.ai/taid-jp/)をiPhoneで動かす方法を説明します。 4 | 5 | ## セットアップ手順 6 | 7 | ### 1. アプリのダウンロード 8 | 9 | まずは「LLMFarm」というアプリをApp Storeからダウンロードしてください。 10 | [LLMFarmをダウンロード](https://apps.apple.com/ru/app/llm-farm/id6461209867?l=en-GB&platform=iphone) 11 | 12 | ### 2. モデルファイルのダウンロード 13 | 14 | TinySwallowには2つのバージョンがあります: 15 | 16 | - Q8バージョン:より正確な応答が可能ですが、やや遅めです(14.75トークン/秒) 17 | - [Q8をダウンロード](https://huggingface.co/SakanaAI/TinySwallow-1.5B-Instruct-GGUF/resolve/main/tinyswallow-1.5b-instruct-q8_0.gguf?download=true) 18 | 19 | - Q5バージョン:Q8より精度は落ちますが、より速く動作します(19.89トークン/秒) 20 | - [Q5をダウンロード](https://huggingface.co/SakanaAI/TinySwallow-1.5B-Instruct-GGUF/resolve/main/tinyswallow-1.5b-instruct-q5_k_m.gguf?download=true) 21 | 22 | ※ブログのデモではQ8バージョンを使用しています 23 | 24 | ### 3. LLMFarmでの設定 25 | 26 | 1. ダウンロードしたLLMFarmアプリを開きます 27 | 2. 画面右上の+ボタンをタップします 28 | 3. 「Basic」タブの「Model」セクションで「Select model」をタップします 29 | 4. 「import from file」から、先ほどダウンロードしたファイルを選択します 30 | 5. タイトルに「TinySwallow」と入力します 31 | 6. 「Settings template」をタップし、「ChatML」を選択します 32 | 33 | ### 4. 追加設定(任意) 34 | 35 | 以下の設定を変更することで、より良い結果が得られる場合があります: 36 | 37 | 「Sampling」タブで: 38 | - Temperature: 0.7 39 | - Top_k: 20 40 | 41 | 「Prompt」タブで以下のシステムプロンプトを追加できます: 42 | 43 | ``` 44 | [system](あなたは、Sakana AI株式会社が開発したTinySwallowです。小型ながら、誠実で優秀なアシスタントです。) 45 | <|im_start|>user 46 | {{prompt}}<|im_end|> 47 | <|im_start|>assistant 48 | 49 | ``` 50 | 51 | ### 5. 完了 52 | 53 | 「Save」をタップして保存すれば、チャットを開始できます! 54 | -------------------------------------------------------------------------------- /index.css: -------------------------------------------------------------------------------- 1 | body, html { 2 | font-family: 'Noto Sans JP', 'Inter', system-ui, sans-serif; 3 | margin: 0; 4 | padding: 0; 5 | background-color: #f8fafc; 6 | height: 100%; 7 | } 8 | 9 | .wrapper { 10 | display: flex; 11 | flex-direction: column; 12 | min-height: 100vh; 13 | height: 100vh; 14 | overflow: hidden; 15 | } 16 | 17 | .wrapper:not(.chat-active) .footer-notice { 18 | display: block; 19 | } 20 | 21 | .wrapper.chat-active footer .footer-notice { 22 | display: none; 23 | } 24 | 25 | .wrapper.chat-active footer { 26 | padding: 0.5rem; 27 | } 28 | 29 | .wrapper.chat-active footer .footer-main { 30 | margin: 0; 31 | } 32 | 33 | header { 34 | display: flex; 35 | flex-direction: column; 36 | background: #ffffff; 37 | box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.1); 38 | margin-bottom: 1rem; 39 | } 40 | 41 | .header-main { 42 | display: flex; 43 | align-items: center; 44 | justify-content: space-between; 45 | padding: 4px 20px; 46 | } 47 | 48 | .header-content { 49 | flex: 1; 50 | text-align: center; 51 | } 52 | 53 | header h1 { 54 | margin: 0; 55 | font-size: 1.8rem; 56 | text-align: left; 57 | padding-left: 20px; 58 | flex: 1; 59 | background: linear-gradient(135deg, #2563eb, #7c3aed); 60 | -webkit-background-clip: text; 61 | -webkit-text-fill-color: transparent; 62 | } 63 | header h1 a { 64 | text-decoration: none; 65 | background: linear-gradient(135deg, #2563eb, #7c3aed); 66 | -webkit-background-clip: text; 67 | -webkit-text-fill-color: transparent; 68 | } 69 | 70 | 71 | header .model-description { 72 | text-align: left; 73 | margin: 0; 74 | padding: 4px 60px; 75 | font-size: 0.9rem; 76 | color: #64748b; 77 | max-height: 200px; 78 | overflow: hidden; 79 | transition: all 0.3s ease-out; 80 | opacity: 1; 81 | line-height: 2.0; 82 | } 83 | 84 | .header-collapsed .model-description { 85 | max-height: 0; 86 | padding-top: 0; 87 | padding-bottom: 0; 88 | opacity: 0; 89 | } 90 | 91 | .collapse-section { 92 | border-top: 1px solid #e2e8f0; 93 | border-bottom: 1px solid #e2e8f0; 94 | margin-bottom: 1rem; 95 | } 96 | .header-toggle { 97 | width: 100%; 98 | background: #ffffff; 99 | border: none; 100 | height: 24px; 101 | cursor: pointer; 102 | display: none; 103 | align-items: center; 104 | justify-content: center; 105 | border-top: 1px solid #e2e8f0; 106 | padding: 0; 107 | margin-top: -1px; 108 | } 109 | .header-toggle:hover { 110 | background: #f8fafc; 111 | } 112 | 113 | 114 | .wrapper.chat-active .header-toggle { 115 | display: flex; 116 | } 117 | .toggle-icon { 118 | width: 36px; 119 | height: 4px; 120 | position: relative; 121 | } 122 | 123 | .toggle-icon::before { 124 | content: ''; 125 | position: absolute; 126 | width: 12px; 127 | height: 12px; 128 | border: 2px solid #e2e8f0; 129 | border-left: 0; 130 | border-top: 0; 131 | left: 50%; 132 | top: -4px; 133 | transform: translateX(-50%) rotate(45deg); 134 | transition: transform 0.3s ease; 135 | } 136 | .header-collapsed .toggle-icon::before { 137 | transform: translateX(-50%) rotate(-135deg); 138 | top: 0; 139 | } 140 | 141 | .model-description a { 142 | color: #2563eb; 143 | text-decoration: none; 144 | } 145 | 146 | .model-description a:hover { 147 | text-decoration: underline; 148 | } 149 | 150 | header .logo { 151 | height: 50px; 152 | margin-left: auto; 153 | background-color: white; 154 | padding: 5px; 155 | border-radius: 5px; 156 | } 157 | 158 | main { 159 | flex: 1; 160 | display: flex; 161 | flex-direction: column; 162 | overflow-y: auto; 163 | min-height: 0; 164 | height: 100%; 165 | } 166 | 167 | main:not(.chat-active) { 168 | justify-content: center; 169 | } 170 | 171 | .download-container { 172 | display: flex; 173 | flex-direction: column; 174 | align-items: center; 175 | justify-content: center; 176 | flex: 1; 177 | padding: 2rem 0; 178 | margin: 0; 179 | height: auto; 180 | } 181 | 182 | #download { 183 | margin-bottom: 10px; 184 | } 185 | 186 | #download-status { 187 | padding: 10px; 188 | background-color: #ffffff; 189 | border: 1px solid #e2e8f0; 190 | border-radius: 5px; 191 | font-size: 0.9rem; 192 | color: #333; 193 | box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.1); 194 | text-align: center; 195 | max-width: 400px; 196 | } 197 | 198 | button { 199 | padding: 15px 30px; 200 | font-size: 1.2rem; 201 | border: none; 202 | border-radius: 5px; 203 | background: linear-gradient(135deg, #2563eb, #7c3aed); 204 | color: white; 205 | cursor: pointer; 206 | transition: all 0.2s ease; 207 | box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); 208 | } 209 | 210 | button:hover:not(:disabled) { 211 | transform: translateY(-2px); 212 | box-shadow: 0 6px 8px rgba(0, 0, 0, 0.15); 213 | } 214 | 215 | button:disabled { 216 | background: #94a3b8; 217 | cursor: not-allowed; 218 | } 219 | 220 | .chat-container { 221 | flex: 1; 222 | display: none; 223 | flex-direction: column; 224 | max-width: 1200px; 225 | width: 100%; 226 | margin: 0 auto; 227 | padding: 0 1rem; 228 | box-sizing: border-box; 229 | overflow: hidden; 230 | position: relative; 231 | height: 100%; 232 | } 233 | 234 | 235 | .chat-box { 236 | flex: 1; 237 | overflow-y: auto; 238 | padding: 10px; 239 | background-color: #ffffff; 240 | border-radius: 5px; 241 | box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.1); 242 | margin-bottom: 0.5rem; 243 | position: relative; 244 | height: 0; 245 | } 246 | 247 | .chat-stats { 248 | background-color: #f1f5f9; 249 | padding: 10px; 250 | font-size: 0.9rem; 251 | } 252 | 253 | .chat-input-container { 254 | position: sticky; 255 | bottom: 0; 256 | display: flex; 257 | gap: 8px; 258 | padding: 1rem; 259 | align-items: flex-end; 260 | background-color: #ffffff; 261 | border-top: 1px solid #e2e8f0; 262 | padding-top: 0.5rem; 263 | margin-top: auto; 264 | } 265 | 266 | #user-input { 267 | flex: 1; 268 | resize: none; 269 | min-height: 24px; 270 | max-height: 200px; 271 | padding: 0.75rem; 272 | border: 1px solid #e2e8f0; 273 | border-radius: 6px; 274 | font-family: inherit; 275 | line-height: 1.5; 276 | overflow-y: auto; 277 | } 278 | 279 | .input-actions { 280 | display: flex; 281 | gap: 4px; 282 | } 283 | 284 | 285 | #reset-chat { 286 | display: none; 287 | padding: 0.75rem; 288 | background: white; 289 | border: 1px solid #e2e8f0; 290 | border-radius: 6px; 291 | color: #64748b; 292 | font-size: 0.9rem; 293 | } 294 | #reset-chat:hover { 295 | background: #f8fafc; 296 | } 297 | 298 | #reset-chat:disabled { 299 | cursor: not-allowed; 300 | opacity: 0.5; 301 | background: #f1f5f9; 302 | transform: none; 303 | box-shadow: none; 304 | } 305 | 306 | #reset-chat:disabled:hover { 307 | background: #f1f5f9; 308 | transform: none; 309 | box-shadow: none; 310 | } 311 | 312 | #user-input:focus { 313 | outline: none; 314 | border-color: #2563eb; 315 | } 316 | 317 | #send { 318 | padding: 10px 12px; 319 | font-size: 0.9rem; 320 | background: linear-gradient(135deg, #2563eb, #7c3aed); 321 | color: white; 322 | border: none; 323 | border-radius: 5px; 324 | cursor: pointer; 325 | } 326 | 327 | #send:hover { 328 | box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); 329 | } 330 | 331 | footer { 332 | text-align: center; 333 | padding: 1rem; 334 | background-color: #ffffff; 335 | color: #64748b; 336 | border-top: 1px solid #e2e8f0; 337 | width: 100%; 338 | } 339 | 340 | footer p { 341 | margin: 0; 342 | } 343 | 344 | footer a { 345 | color: #2563eb; 346 | text-decoration: none; 347 | } 348 | 349 | footer a:hover { 350 | text-decoration: underline; 351 | } 352 | 353 | .footer-main { 354 | margin-bottom: 1rem; 355 | font-size: 0.9rem; 356 | } 357 | .footer-notice { 358 | max-width: 800px; 359 | margin: 0 auto; 360 | font-size: 0.8rem; 361 | color: #64748b; 362 | text-align: left; 363 | } 364 | 365 | .notice-text { 366 | margin: 0.5rem 0; 367 | line-height: 1.5; 368 | } 369 | 370 | .hidden { 371 | display: none; 372 | } 373 | 374 | .message-container { 375 | display: flex; 376 | margin: 10px 0; 377 | } 378 | 379 | .message-container.user { 380 | justify-content: flex-end; 381 | } 382 | 383 | .message-container.assistant { 384 | justify-content: flex-start; 385 | } 386 | 387 | .message { 388 | white-space: pre-wrap; 389 | word-wrap: break-word; 390 | max-width: 80%; 391 | padding: 10px; 392 | border-radius: 10px; 393 | font-size: 1rem; 394 | box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); 395 | } 396 | 397 | .message pre { 398 | background-color: #f8fafc; 399 | padding: 1em; 400 | border-radius: 5px; 401 | overflow-x: auto; 402 | } 403 | 404 | .message-container.user .message { 405 | background: linear-gradient(135deg, #2563eb, #7c3aed); 406 | color: white; 407 | } 408 | 409 | .message-container.assistant .message { 410 | background-color: #f1f5f9; 411 | color: #334155; 412 | } 413 | 414 | 415 | 416 | .spinner { 417 | width: 16px; 418 | height: 16px; 419 | border: 2px solid transparent; 420 | border-top-color: white; 421 | border-radius: 50%; 422 | animation: spin 0.6s linear infinite; 423 | position: absolute; 424 | } 425 | 426 | .button-content { 427 | position: relative; 428 | display: flex; 429 | align-items: center; 430 | justify-content: center; 431 | min-width: 50px; 432 | } 433 | 434 | @keyframes spin { 435 | to { transform: rotate(360deg); } 436 | } 437 | 438 | .progress-bar { 439 | width: 100%; 440 | max-width: 400px; 441 | height: 4px; 442 | background-color: #e2e8f0; 443 | border-radius: 2px; 444 | margin-top: 10px; 445 | } 446 | 447 | .progress-fill { 448 | height: 100%; 449 | background: linear-gradient(135deg, #2563eb, #7c3aed); 450 | border-radius: 2px; 451 | transition: width 0.3s ease; 452 | } 453 | 454 | .suggestions-container { 455 | display: none; 456 | flex-direction: column; 457 | align-items: center; 458 | margin: 0 auto 1rem; 459 | gap: 1rem; 460 | max-width: 800px; 461 | width: 100%; 462 | } 463 | 464 | .suggestions-title { 465 | color: #334155; 466 | font-size: 1.2rem; 467 | font-weight: 500; 468 | margin: 0; 469 | } 470 | 471 | .suggestions { 472 | display: grid; 473 | grid-template-columns: repeat(2, 1fr); 474 | gap: 12px; 475 | width: 100%; 476 | padding: 0 20px; 477 | } 478 | 479 | .suggestion-btn { 480 | background: white; 481 | border: 2px solid #e2e8f0; 482 | padding: 1rem; 483 | border-radius: 12px; 484 | box-shadow: 0 2px 4px rgba(0,0,0,0.05); 485 | transition: all 0.2s; 486 | font-size: 1rem; 487 | text-align: left; 488 | color: #1e293b; 489 | } 490 | 491 | .suggestion-btn:hover { 492 | border-color: #2563eb; 493 | transform: translateY(-1px); 494 | } 495 | 496 | .info-button { 497 | position: fixed; 498 | right: max(20px, calc((100% - 1000px) / 2 - 60px)); 499 | top: 50%; 500 | transform: translateY(-50%); 501 | width: 40px; 502 | height: 40px; 503 | border-radius: 8px; 504 | background: white; 505 | border: 1px solid #e2e8f0; 506 | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); 507 | display: none; 508 | align-items: center; 509 | justify-content: center; 510 | cursor: pointer; 511 | transition: all 0.2s ease; 512 | color: #64748b; 513 | z-index: 10; 514 | padding: 0; 515 | } 516 | 517 | .info-button:hover { 518 | color: #2563eb; 519 | border-color: #2563eb; 520 | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); 521 | } 522 | 523 | 524 | .info-button { 525 | display: none; 526 | } 527 | 528 | .wrapper.chat-active .info-button { 529 | display: flex; 530 | position: fixed; 531 | right: max(20px, calc((100% - 1000px) / 2 - 60px)); 532 | bottom: 80px; 533 | top: auto; 534 | transform: none; 535 | width: 30px; 536 | height: 30px; 537 | border-radius: 8px; 538 | background: white; 539 | border: 1px solid #e2e8f0; 540 | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); 541 | align-items: center; 542 | justify-content: center; 543 | cursor: pointer; 544 | transition: all 0.2s ease; 545 | color: #64748b; 546 | z-index: 10; 547 | padding: 0; 548 | } 549 | 550 | .wrapper.chat-active .info-button:hover { 551 | color: #2563eb; 552 | border-color: #2563eb; 553 | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); 554 | } 555 | 556 | .wrapper.chat-active .modal:not(.hidden) { 557 | display: flex; 558 | position: fixed; 559 | top: 0; 560 | left: 0; 561 | width: 100%; 562 | height: 100%; 563 | background: rgba(0, 0, 0, 0.5); 564 | align-items: center; 565 | justify-content: center; 566 | z-index: 1000; 567 | } 568 | 569 | .modal { 570 | position: fixed; 571 | top: 0; 572 | left: 0; 573 | width: 100%; 574 | height: 100%; 575 | background: rgba(0, 0, 0, 0.5); 576 | display: none; 577 | align-items: center; 578 | justify-content: center; 579 | z-index: 1000; 580 | } 581 | 582 | .modal-content { 583 | background: white; 584 | border-radius: 8px; 585 | width: 90%; 586 | max-width: 800px; 587 | max-height: 90vh; 588 | overflow-y: auto; 589 | box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); 590 | } 591 | 592 | 593 | .modal-header { 594 | padding: 1rem; 595 | border-bottom: 1px solid #e2e8f0; 596 | display: flex; 597 | align-items: center; 598 | justify-content: space-between; 599 | background: white; 600 | position: sticky; 601 | top: 0; 602 | z-index: 1; 603 | } 604 | 605 | .modal-header h2 { 606 | margin: 0; 607 | font-size: 1.25rem; 608 | color: #1e293b; 609 | font-weight: 500; 610 | } 611 | 612 | .close-modal { 613 | background: none; 614 | border: none; 615 | font-size: 1.5rem; 616 | color: #64748b; 617 | cursor: pointer; 618 | padding: 0.5rem; 619 | width: 40px; 620 | height: 40px; 621 | display: flex; 622 | align-items: center; 623 | justify-content: center; 624 | border-radius: 6px; 625 | transition: all 0.2s ease; 626 | } 627 | 628 | .close-modal:hover { 629 | background: #f1f5f9; 630 | color: #1e293b; 631 | } 632 | 633 | .modal-body { 634 | padding: 1.5rem; 635 | } 636 | 637 | .modal-body .notice-text { 638 | color: #1e293b; 639 | line-height: 1.6; 640 | margin-bottom: 1rem; 641 | } 642 | 643 | .modal-body .notice-text:last-child { 644 | margin-bottom: 0; 645 | } 646 | 647 | @media (max-width: 1200px) { 648 | .wrapper.chat-active .info-button { 649 | right: 20px; 650 | } 651 | } 652 | 653 | @media (max-width: 768px) { 654 | .header-main { 655 | padding: 8px 12px; 656 | } 657 | 658 | header h1 { 659 | font-size: 1.3rem; 660 | padding-left: 0; 661 | } 662 | 663 | .header-content { 664 | flex: 1; 665 | } 666 | 667 | header .model-description { 668 | font-size: 0.85rem; 669 | padding: 8px 40px; 670 | line-height: 1.8; 671 | } 672 | 673 | .logo { 674 | height: 35px; 675 | } 676 | 677 | .suggestions { 678 | grid-template-columns: 1fr; 679 | gap: 8px; 680 | padding: 0 12px; 681 | } 682 | 683 | .suggestions-title { 684 | font-size: 1.1rem; 685 | padding: 0 12px; 686 | } 687 | 688 | .suggestion-btn { 689 | padding: 12px; 690 | font-size: 0.9rem; 691 | } 692 | 693 | .chat-input-container { 694 | padding: 8px; 695 | } 696 | 697 | #user-input { 698 | font-size: 16px; 699 | padding: 8px; 700 | } 701 | 702 | .message { 703 | max-width: 85%; 704 | font-size: 0.9rem; 705 | } 706 | 707 | .footer-notice { 708 | padding: 0 12px; 709 | font-size: 0.75rem; 710 | } 711 | 712 | #send { 713 | padding: 8px 16px; 714 | min-width: 60px; 715 | } 716 | } 717 | 718 | @media (max-width: 640px) { 719 | .wrapper.chat-active .info-button { 720 | bottom: 70px; 721 | } 722 | 723 | .modal-content { 724 | width: 95%; 725 | max-height: 80vh; 726 | } 727 | 728 | .modal-header h2 { 729 | font-size: 1.1rem; 730 | } 731 | 732 | .modal-body { 733 | padding: 1rem; 734 | } 735 | 736 | .close-modal { 737 | padding: 0.25rem; 738 | width: 32px; 739 | height: 32px; 740 | } 741 | } 742 | 743 | @media (max-width: 480px) { 744 | header h1 { 745 | font-size: 1.2rem; 746 | } 747 | 748 | header .model-description { 749 | font-size: 0.8rem; 750 | padding: 8px 20px; 751 | line-height: 1.7; 752 | } 753 | 754 | .logo { 755 | height: 30px; 756 | } 757 | 758 | .chat-input-container { 759 | padding: 8px; 760 | } 761 | 762 | #user-input { 763 | font-size: 16px; 764 | padding: 8px; 765 | } 766 | 767 | #send { 768 | padding: 8px 12px; 769 | font-size: 0.85rem; 770 | } 771 | 772 | #reset-chat { 773 | padding: 8px; 774 | } 775 | 776 | .message { 777 | max-width: 90%; 778 | font-size: 0.9rem; 779 | padding: 8px; 780 | } 781 | 782 | .suggestions-container { 783 | gap: 0.5rem; 784 | } 785 | 786 | .suggestions { 787 | grid-template-columns: 1fr; 788 | } 789 | } 790 | 791 | @supports (padding: max(0px)) { 792 | .chat-input-container { 793 | padding-bottom: max(8px, env(safe-area-inset-bottom)); 794 | } 795 | 796 | .model-description { 797 | padding-left: max(16px, env(safe-area-inset-left)); 798 | padding-right: max(16px, env(safe-area-inset-right)); 799 | } 800 | 801 | footer { 802 | padding-bottom: max(1rem, env(safe-area-inset-bottom)); 803 | } 804 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | TinySwallow-1.5B Chat 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 |
18 |
19 |

TinySwallow ChatUI

20 |

21 | 📚 Paper | 22 | 🤗 Hugging Face | 23 | 📝 Blog
24 | このChatUIでは、小規模日本語言語モデルTinySwallow-1.5Bと会話することができます。 25 |
26 | このデモはTinySwallow-1.5Bをブラウザ上で動作させており、外部APIなどを介しておらず、最初のモデルのダウンロードが完了すれば完全オフラインで会話することも可能です。 27 |
28 | PCでの利用を想定しており、iPhone上でのチャットはこちらをお試しください。 29 |

30 |
31 | 32 | 33 | 34 | 35 |
36 |
37 |
38 | 39 | 42 | 43 |
44 |
45 |

例文をクリックして会話を始めてみましょう

46 |
47 | 48 | 49 | 50 | 51 |
52 |
53 |
54 |
55 | 56 |
57 | 58 |
59 | 65 | 68 |
69 |
70 | 77 |
78 |
79 | 80 | 98 | 111 |
112 | 113 | 114 | 115 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import * as webllm from "https://esm.run/@mlc-ai/web-llm"; 2 | 3 | console.log("WebLLM loaded successfully!"); 4 | const messages = [ 5 | { 6 | content: "あなたは、Sakana AI株式会社が開発したTinySwallowです。小型ながら、誠実で優秀なアシスタントです。", 7 | role: "system", 8 | }, 9 | ]; 10 | const appConfig = { 11 | model_list: [ 12 | { 13 | model: "https://huggingface.co/SakanaAI/TinySwallow-1.5B-Instruct-q4f32_1-MLC", 14 | model_id: "TinySwallow-1.5B", 15 | model_lib: 16 | // https://github.com/mlc-ai/binary-mlc-llm-libs/tree/main/web-llm-models/v0_2_48 17 | webllm.modelLibURLPrefix + 18 | webllm.modelVersion + 19 | "/Qwen2-1.5B-Instruct-q4f32_1-ctx4k_cs1k-webgpu.wasm", 20 | }, 21 | ], 22 | }; 23 | console.log(appConfig); 24 | // Callback function for initializing progress 25 | function updateProgress(report) { 26 | const progressBar = document.querySelector('.progress-fill'); 27 | progressBar.style.width = `${report.progress * 100}%`; 28 | } 29 | 30 | function updateEngineInitProgressCallback(report) { 31 | console.log("initialize", report.progress); 32 | document.getElementById("download-status").textContent = report.text; 33 | updateProgress(report); 34 | } 35 | 36 | let engine; 37 | 38 | async function initializeWebLLMEngine() { 39 | try { 40 | const button = document.getElementById("download"); 41 | const downloadStatus = document.getElementById("download-status"); 42 | const downloadProgress = document.getElementById("download-progress"); 43 | 44 | button.disabled = true; 45 | downloadStatus.style.display = "block"; 46 | downloadProgress.style.display = "block"; 47 | 48 | engine = await webllm.CreateMLCEngine("TinySwallow-1.5B", { 49 | appConfig: appConfig, 50 | initProgressCallback: updateEngineInitProgressCallback, 51 | }); 52 | console.log("Model successfully loaded!"); 53 | 54 | button.style.display = "none"; 55 | downloadStatus.style.display = "none"; 56 | downloadProgress.style.display = "none"; 57 | document.querySelector(".download-container").style.display = "none"; 58 | 59 | document.querySelector(".suggestions-container").style.display = "flex"; 60 | document.querySelector(".chat-container").style.display = "flex"; 61 | document.getElementById('reset-chat').style.display = 'flex'; 62 | document.getElementById("send").disabled = false; 63 | document.getElementById("user-input").disabled = false; 64 | document.querySelector('.wrapper').classList.add('chat-active'); 65 | 66 | } catch (error) { 67 | console.error("Error loading model:", error); 68 | button.disabled = false; 69 | downloadStatus.style.display = "none"; 70 | downloadProgress.style.display = "none"; 71 | } 72 | } 73 | 74 | function setLoading(isLoading) { 75 | const sendButton = document.getElementById("send"); 76 | const resetButton = document.getElementById("reset-chat"); 77 | const spinner = sendButton.querySelector(".spinner"); 78 | const buttonText = sendButton.querySelector(".button-text"); 79 | const userInput = document.getElementById("user-input"); 80 | 81 | sendButton.disabled = isLoading; 82 | spinner.classList.toggle("hidden", !isLoading); 83 | buttonText.classList.toggle("hidden", isLoading); 84 | 85 | resetButton.disabled = isLoading; 86 | resetButton.style.opacity = isLoading ? "0.5" : "1"; 87 | 88 | userInput.disabled = isLoading; 89 | userInput.setAttribute("placeholder", isLoading ? "生成中..." : "メッセージを入力してください。"); 90 | } 91 | 92 | function resetUIState() { 93 | const sendButton = document.getElementById("send"); 94 | const spinner = sendButton.querySelector(".spinner"); 95 | const buttonText = sendButton.querySelector(".button-text"); 96 | 97 | sendButton.disabled = false; 98 | spinner.classList.add("hidden"); 99 | buttonText.classList.remove("hidden"); 100 | 101 | const userInput = document.getElementById("user-input"); 102 | userInput.value = ""; 103 | userInput.setAttribute("placeholder", "メッセージを入力してください。"); 104 | userInput.style.height = "24px"; 105 | userInput.disabled = false; 106 | } 107 | 108 | let currentGenerationController = null; 109 | 110 | function cancelGeneration() { 111 | if (currentGenerationController) { 112 | currentGenerationController.abort(); 113 | currentGenerationController = null; 114 | } 115 | } 116 | 117 | async function streamingGenerating(messages, onUpdate, onFinish, onError) { 118 | try { 119 | currentGenerationController = new AbortController(); 120 | 121 | let curMessage = ""; 122 | let usage; 123 | console.log(messages); 124 | const completion = await engine.chat.completions.create({ 125 | stream: true, 126 | messages, 127 | stream_options: { include_usage: true }, 128 | temperature: 0.7, 129 | top_p: 0.95, 130 | logit_bias: {"14444": -100}, 131 | repetition_penalty: 1.2, 132 | frequency_penalty: 0.5, 133 | }); 134 | for await (const chunk of completion) { 135 | if (currentGenerationController === null) { 136 | return; 137 | } 138 | const curDelta = chunk.choices[0]?.delta.content; 139 | if (curDelta) { 140 | curMessage += curDelta; 141 | } 142 | if (chunk.usage) { 143 | usage = chunk.usage; 144 | } 145 | onUpdate(curMessage); 146 | } 147 | if (currentGenerationController !== null) { 148 | const finalMessage = await engine.getMessage(); 149 | messages.push({ role: "assistant", content: finalMessage }); 150 | onFinish(finalMessage, usage); 151 | } 152 | } catch (err) { 153 | if (err.name === 'AbortError') { 154 | console.log('Generation was cancelled'); 155 | return; 156 | } 157 | onError(err); 158 | } finally { 159 | currentGenerationController = null; 160 | } 161 | } 162 | 163 | function onMessageSend() { 164 | const input = document.getElementById("user-input").value.trim(); 165 | const message = { content: input, role: "user" }; 166 | if (!input) return; 167 | document.querySelector('.suggestions-container').style.display = 'none'; 168 | document.querySelector('.chat-container').classList.add('no-suggestions'); 169 | setLoading(true); 170 | 171 | messages.push(message); 172 | appendMessage(message); 173 | document.getElementById("user-input").value = ""; 174 | document.getElementById("user-input").style.height = "24px"; 175 | appendMessage({ content: "お待ちください...", role: "assistant" }); 176 | 177 | streamingGenerating( 178 | messages, 179 | updateLastMessage, 180 | (finalMessage, usage) => { 181 | updateLastMessage(finalMessage); 182 | setLoading(false); 183 | }, 184 | (error) => { 185 | console.error(error); 186 | setLoading(false); 187 | } 188 | ); 189 | } 190 | 191 | function appendMessage(message) { 192 | const chatBox = document.getElementById("chat-box"); 193 | const container = document.createElement("div"); 194 | container.classList.add("message-container", message.role); 195 | const newMessage = document.createElement("div"); 196 | newMessage.classList.add("message"); 197 | marked.setOptions({ 198 | breaks: true, 199 | gfm: true 200 | }); 201 | const formattedMessage = marked.parse(message.content); 202 | newMessage.innerHTML = formattedMessage; 203 | 204 | container.appendChild(newMessage); 205 | chatBox.appendChild(container); 206 | chatBox.scrollTo({ 207 | top: chatBox.scrollHeight, 208 | behavior: "smooth", 209 | }); 210 | } 211 | 212 | function updateLastMessage(content) { 213 | const messageDoms = document.getElementById("chat-box").querySelectorAll(".message"); 214 | const lastMessageDom = messageDoms[messageDoms.length - 1]; 215 | marked.setOptions({ 216 | breaks: true, 217 | gfm: true 218 | }); 219 | const formattedContent = marked.parse(content); 220 | lastMessageDom.innerHTML = formattedContent; 221 | const chatBox = document.getElementById("chat-box"); 222 | chatBox.scrollTo({ 223 | top: chatBox.scrollHeight, 224 | behavior: "smooth", 225 | }); 226 | } 227 | 228 | let isComposing = false; 229 | 230 | const textarea = document.getElementById("user-input"); 231 | textarea.addEventListener("input", () => { 232 | textarea.style.height = "24px"; 233 | textarea.style.height = textarea.scrollHeight + "px"; 234 | }); 235 | 236 | textarea.addEventListener("compositionstart", () => { 237 | isComposing = true; 238 | }); 239 | 240 | textarea.addEventListener("compositionend", () => { 241 | isComposing = false; 242 | }); 243 | 244 | textarea.addEventListener("keydown", (event) => { 245 | if (event.key === "Enter" && !event.shiftKey && !isComposing) { 246 | event.preventDefault(); 247 | onMessageSend(); 248 | } 249 | }); 250 | 251 | document.getElementById("user-input").addEventListener("compositionstart", () => { 252 | isComposing = true; 253 | }); 254 | 255 | document.getElementById("user-input").addEventListener("compositionend", () => { 256 | isComposing = false; 257 | }); 258 | 259 | document.getElementById("user-input").addEventListener("keydown", (event) => { 260 | if (event.key === "Enter" && !isComposing) { 261 | event.preventDefault(); 262 | onMessageSend(); 263 | } 264 | }); 265 | 266 | 267 | document.getElementById("download").addEventListener("click", initializeWebLLMEngine); 268 | document.getElementById("send").addEventListener("click", onMessageSend); 269 | 270 | document.querySelectorAll('.suggestion-btn').forEach(btn => { 271 | btn.addEventListener('click', () => { 272 | const input = document.getElementById('user-input'); 273 | input.value = btn.textContent; 274 | onMessageSend(); 275 | document.querySelector('.suggestions-container').style.display = 'none'; 276 | }); 277 | }); 278 | 279 | document.getElementById('title-link').addEventListener('click', (e) => { 280 | e.preventDefault(); 281 | if (document.querySelector('.chat-container').style.display === 'flex') { 282 | cancelGeneration(); 283 | 284 | messages.length = 1; 285 | document.getElementById('chat-box').innerHTML = ''; 286 | 287 | document.querySelector('.chat-container').style.display = 'none'; 288 | document.querySelector('.suggestions-container').style.display = 'none'; 289 | document.getElementById('reset-chat').style.display = 'none'; 290 | 291 | const downloadContainer = document.querySelector('.download-container'); 292 | downloadContainer.style.display = 'flex'; 293 | document.getElementById('download').style.display = 'block'; 294 | document.getElementById('download').disabled = false; 295 | document.getElementById('download-status').style.display = 'none'; 296 | document.getElementById('download-progress').style.display = 'none'; 297 | 298 | document.querySelector('.wrapper').classList.remove('chat-active'); 299 | 300 | resetUIState(); 301 | if (engine) { 302 | engine = null; 303 | } 304 | } 305 | }); 306 | 307 | document.getElementById('reset-chat').addEventListener('click', () => { 308 | cancelGeneration(); 309 | messages.length = 1; 310 | const chatBox = document.getElementById('chat-box'); 311 | while (chatBox.firstChild) { 312 | chatBox.removeChild(chatBox.firstChild); 313 | } 314 | 315 | document.querySelector('.suggestions-container').style.display = 'flex'; 316 | resetUIState(); 317 | 318 | const userInput = document.getElementById("user-input"); 319 | userInput.disabled = false; 320 | const sendButton = document.getElementById("send"); 321 | sendButton.disabled = false; 322 | }); 323 | 324 | const infoButton = document.getElementById('info-button'); 325 | const modal = document.getElementById('info-modal'); 326 | 327 | infoButton.addEventListener('click', () => { 328 | modal.classList.remove('hidden'); 329 | }); 330 | 331 | modal.addEventListener('click', (e) => { 332 | if (e.target === modal) { 333 | modal.classList.add('hidden'); 334 | } 335 | }); 336 | 337 | const closeModalButton = modal.querySelector('.close-modal'); 338 | closeModalButton.addEventListener('click', () => { 339 | modal.classList.add('hidden'); 340 | }); 341 | 342 | document.addEventListener('DOMContentLoaded', () => { 343 | // Get existing elements 344 | const header = document.querySelector('header'); 345 | const headerContent = header.querySelector('.header-content'); 346 | const logo = header.querySelector('.logo').parentElement; 347 | const modelDescription = document.querySelector('.model-description'); 348 | 349 | // Create and setup header main 350 | const headerMain = document.createElement('div'); 351 | headerMain.className = 'header-main'; 352 | 353 | // Move elements to new structure 354 | headerMain.appendChild(headerContent); 355 | headerMain.appendChild(logo); 356 | 357 | // Clear and rebuild header 358 | header.innerHTML = ''; 359 | header.appendChild(headerMain); 360 | 361 | if (modelDescription) { 362 | header.appendChild(modelDescription); 363 | } 364 | 365 | // Create and add toggle button 366 | const headerToggle = document.createElement('button'); 367 | headerToggle.className = 'header-toggle'; 368 | headerToggle.setAttribute('aria-label', '詳細の表示/非表示'); 369 | headerToggle.innerHTML = ''; 370 | 371 | header.appendChild(headerToggle); 372 | 373 | // Add click event 374 | // Add initial collapsed state when chat is active 375 | const setHeaderState = () => { 376 | if (document.querySelector('.wrapper').classList.contains('chat-active')) { 377 | header.classList.add('header-collapsed'); 378 | } else { 379 | header.classList.remove('header-collapsed'); 380 | } 381 | }; 382 | 383 | // Set initial state 384 | setHeaderState(); 385 | 386 | // Add click event listener 387 | headerToggle.addEventListener('click', () => { 388 | header.classList.toggle('header-collapsed'); 389 | }); 390 | 391 | // Watch for chat-active class changes 392 | const observer = new MutationObserver((mutations) => { 393 | mutations.forEach((mutation) => { 394 | if (mutation.attributeName === 'class') { 395 | setHeaderState(); 396 | } 397 | }); 398 | }); 399 | 400 | observer.observe(document.querySelector('.wrapper'), { 401 | attributes: true 402 | }); 403 | }); --------------------------------------------------------------------------------