lauro1 commited on
Commit
faca43f
1 Parent(s): aed7200
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. Dockerfile +32 -0
  2. LICENSE +201 -0
  3. PRIVACY.md +18 -0
  4. README.md +233 -10
  5. assets/on-device-dark.png +0 -0
  6. assets/on-device-light.png +0 -0
  7. assets/zero-trust-dark-border.png +0 -0
  8. assets/zero-trust-dark.png +0 -0
  9. assets/zero-trust-light-border.png +0 -0
  10. assets/zero-trust-light.png +0 -0
  11. package-lock.json +0 -0
  12. package.json +67 -0
  13. postcss.config.js +6 -0
  14. src/app.d.ts +20 -0
  15. src/app.html +72 -0
  16. src/hooks.server.ts +107 -0
  17. src/lib/actions/snapScrollToBottom.ts +54 -0
  18. src/lib/buildPrompt.ts +34 -0
  19. src/lib/components/AnnouncementBanner.svelte +15 -0
  20. src/lib/components/CodeBlock.svelte +28 -0
  21. src/lib/components/CopyToClipBoardBtn.svelte +50 -0
  22. src/lib/components/LoadingModal.svelte +40 -0
  23. src/lib/components/LoadingModalWritable.js +5 -0
  24. src/lib/components/LoginModal.svelte +76 -0
  25. src/lib/components/MobileNav.svelte +62 -0
  26. src/lib/components/MobileWarningModal.svelte +25 -0
  27. src/lib/components/Modal.svelte +62 -0
  28. src/lib/components/ModelCardMetadata.svelte +48 -0
  29. src/lib/components/ModelsModal.svelte +149 -0
  30. src/lib/components/NavConversationItem.svelte +89 -0
  31. src/lib/components/NavMenu.svelte +105 -0
  32. src/lib/components/OpenWebSearchResults.svelte +114 -0
  33. src/lib/components/Portal.svelte +19 -0
  34. src/lib/components/ScrollToBottomBtn.svelte +46 -0
  35. src/lib/components/SettingsModal.svelte +80 -0
  36. src/lib/components/StopGeneratingBtn.svelte +13 -0
  37. src/lib/components/Switch.svelte +13 -0
  38. src/lib/components/Toast.svelte +19 -0
  39. src/lib/components/Tooltip.svelte +22 -0
  40. src/lib/components/WebSearchToggle.svelte +27 -0
  41. src/lib/components/chat/ChatInput.svelte +65 -0
  42. src/lib/components/chat/ChatIntroduction.svelte +86 -0
  43. src/lib/components/chat/ChatMessage.svelte +212 -0
  44. src/lib/components/chat/ChatMessages.svelte +83 -0
  45. src/lib/components/chat/ChatWindow.svelte +147 -0
  46. src/lib/components/icons/IconChevron.svelte +20 -0
  47. src/lib/components/icons/IconCopy.svelte +26 -0
  48. src/lib/components/icons/IconDazzled.svelte +36 -0
  49. src/lib/components/icons/IconLoading.svelte +18 -0
  50. src/lib/components/icons/Logo.svelte +28 -0
Dockerfile ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # syntax=docker/dockerfile:1
2
+ # read the doc: https://huggingface.co/docs/hub/spaces-sdks-docker
3
+ # you will also find guides on how best to write your Dockerfile
4
+ FROM node:19 as builder-production
5
+
6
+ WORKDIR /app
7
+
8
+ COPY --link --chown=1000 package-lock.json package.json ./
9
+ RUN --mount=type=cache,target=/app/.npm \
10
+ npm set cache /app/.npm && \
11
+ npm ci --omit=dev
12
+
13
+ FROM builder-production as builder
14
+
15
+ RUN --mount=type=cache,target=/app/.npm \
16
+ npm set cache /app/.npm && \
17
+ npm ci
18
+
19
+ COPY --link --chown=1000 . .
20
+
21
+ RUN --mount=type=secret,id=DOTENV_LOCAL,dst=.env.local \
22
+ npm run build
23
+
24
+ FROM node:19-slim
25
+
26
+ RUN npm install -g pm2
27
+
28
+ COPY --from=builder-production /app/node_modules /app/node_modules
29
+ COPY --link --chown=1000 package.json /app/package.json
30
+ COPY --from=builder /app/build /app/build
31
+
32
+ CMD pm2 start /app/build/index.js -i $CPU_CORES --no-daemon
LICENSE ADDED
@@ -0,0 +1,201 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 2023, Mithril Security SAS
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.
PRIVACY.md ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # About & Privacy - BlindChat
2
+
3
+ ## Privacy
4
+
5
+ <em>Last updated: September 15, 2023</em>
6
+
7
+ No conversations are recorded. All computation happens on your device, and conversations are stored locally in the browser’s cache.
8
+
9
+ We don’t and never will see your data, so we cannot train on your data. Your data remains yours.
10
+
11
+ ## About
12
+
13
+ BlindChat is an open-source project to provide fully in-browser and private Conversational AI.
14
+
15
+ It is currently developed and maintained by [Mithril Security](https://www.mithrilsecurity.io/), a startup aiming to make AI more private.
16
+
17
+ You can find more information on our [Github](https://github.com/mithril-security/blind_chat/), join us on our [Discord](https://discord.com/invite/TxEHagpWd4), or directly [contact us](mailto:contact@mithrilsecurity.io).
18
+
README.md CHANGED
@@ -1,10 +1,233 @@
1
- ---
2
- title: Blind Chat
3
- emoji: 📚
4
- colorFrom: gray
5
- colorTo: pink
6
- sdk: docker
7
- pinned: false
8
- ---
9
-
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <a name="readme-top"></a>
2
+ <br />
3
+
4
+ <div align="center">
5
+ <a href="https://github.com/mithril-security/blind_chat">
6
+ <img src="https://github.com/mithril-security/blindai/raw/main/docs/assets/logo.png" alt="Logo" width="80" height="80">
7
+ </a>
8
+
9
+ <h1 align="center">BlindChat</h1>
10
+
11
+ [![Website][website-shield]][website-url]
12
+ [![Blog][blog-shield]][blog-url]
13
+
14
+ </div>
15
+
16
+ <p align="center">
17
+ <b>Open-source and privacy-by-design alternative to ChatGPT</b><br /><br />
18
+ <!--
19
+ <a href="https://blindllama.mithrilsecurity.io/en/latest"><strong>Explore the docs »</strong></a>
20
+ <br />
21
+ <br />
22
+ <a href="https://aicert.mithrilsecurity.io/en/latest/docs/getting-started/quick-tour/">Get started</a>
23
+ ·
24
+ <a href="https://github.com/mithril-security/aicert/issues">Report Bug</a>
25
+ ·
26
+ <a href="https://github.com/mithril-security/aicert/issues">Request Feature</a>
27
+ </p>
28
+ </div>
29
+
30
+ <!-- TABLE OF CONTENTS -->
31
+ <details>
32
+ <summary>Table of Contents</summary>
33
+ <ol>
34
+ <li><a href="#-about-the-project">About the project</a></li>
35
+ <li><a href="#-roadmap">Roadmap</a></li>
36
+ <li><a href="#-design">Design</a></li>
37
+ <li><a href="#-comparisons">Comparisons</a></li>
38
+ <li><a href="#-get-in-touch">Contact</a></li>
39
+ </ol>
40
+ </details>
41
+
42
+ ## 📜 About the project
43
+
44
+ ### What is BlindChat?
45
+
46
+ 🐱 **BlindChat** is an open-source project to develop **the first fully in-browser and private Conversational AI**.
47
+
48
+ Most conversational AI solutions today require users to send their data to AI providers who serve AI models as a Service. This poses privacy issues for users who **lose control over their data**.
49
+
50
+ ⚠️ Because data is a key asset to improve LLMs, **many solutions more or less implicitly fine-tune users’ data to improve their model**.
51
+
52
+ This creates privacy risks for users as LLMs might learn their data by heart. Carlini et al. [1] showed that LLMs such as GPT-J could learn at least 1% of their training set by heart.
53
+
54
+ 🔐 BlindChat solves this issue as users have guarantees that their data remains private at all times and have full control over it, either by doing local inference or using secure isolated environments called secure enclaves.
55
+
56
+ ### Local conversations
57
+
58
+ ### Demo
59
+
60
+ 👩‍💻 You can try out BlindChat [here](https://chat.mithrilsecurity.io)! We enable users to interact with a [Flan-T5 model](https://huggingface.co/docs/transformers/model_doc/flan-t5) locally through their browser: the model is pulled and used for local inference using [transformers.js](https://huggingface.co/docs/transformers.js/index).
61
+
62
+ ### Who is BlindChat for?
63
+
64
+ BlindChat aims to serve two users:
65
+
66
+ - **End users:** We want to provide privacy-by-design alternatives to change the current status quo. Most users today are forced to give up their data to leverage AI services, and opaque or inexistent privacy controls are the norm.
67
+
68
+ - **Developers:** We want to help developers easily serve privacy-by-design Conversational AI, which is why we are focused on making BlindChat easy to customize and deploy.
69
+
70
+ <p align="right">(<a href="#readme-top">back to top</a>)</p>
71
+
72
+ ## Roadmap
73
+
74
+ You can check out our progress in more detail on our [official roadmap](https://github.com/orgs/mithril-security/projects/2/views/4). We highlight feature on which we would love help from contributors in our [help wanted section](https://github.com/orgs/mithril-security/projects/2/views/3).
75
+
76
+ Roadmap quick summary:
77
+
78
+ - [x] Revamping of Hugging Face Chat UI to make it entirely client-side (removal of telemetry, data sharing, server-side history of conversations, server-side inference, etc.)
79
+ - [x] Integration of privacy-by-design inference with local model
80
+ - [x] Local caching of conversations
81
+ - [ ] Integration of more advanced local models (e.g. [phi-1.5](https://huggingface.co/microsoft/phi-1_5)) and more advanced inference (e.g. [Web LLM](https://github.com/mlc-ai/web-llm))
82
+ - [ ] Integration of privacy-by-design inference with remote enclaves using BlindLlama for powerful models such as [Llama 2 70b](https://huggingface.co/meta-llama/Llama-2-70b-chat-hf) & [Falcon 180b](https://huggingface.co/tiiuae/falcon-180B) ⌛
83
+ - [ ] Integration with [LlamaIndex TS](https://github.com/run-llama/LlamaIndexTS) for local Retrieval Augmented Generation (RAG) ⌛
84
+ - [ ] Internet search ⌛
85
+ - [ ] Connectors to pull data from different sources ⌛
86
+
87
+ <p align="right">(<a href="#readme-top">back to top</a>)</p>
88
+
89
+ ## 🔧 Setup
90
+
91
+ Before going any further, please make sure you have [Node JS 18.0](https://nodejs.org/en) installed on your system.
92
+
93
+ To run the chat user interface in dev/debug mode for testing purposes, execute the following commands in the root folder of your BlindChat code repo.
94
+
95
+ ```bash
96
+ npm install
97
+ npm run dev
98
+ ```
99
+
100
+ This will install the dependencies of the project and launch the dev environment.
101
+
102
+ The chat can be deployed in production mode with the following commands:
103
+
104
+ ```bash
105
+ npm run build
106
+ node build
107
+ ```
108
+
109
+ The chat-ui uses server-side rendering, so building the pages before deploying them is mandatory.
110
+
111
+ > ⚠️ Note that the command `node build` will run the server in `HTTP mode`.
112
+ > If you wish to add TLS, please use a proxy server, such as NGINX.
113
+
114
+ <p align="right">(<a href="#readme-top">back to top</a>)</p>
115
+
116
+ ## 🧑‍🎨 Design
117
+
118
+ ### Principles
119
+
120
+ 🤗 **BlindChat** is a fork from [**Hugging Face Chat UI project**](https://huggingface.co/spaces/huggingchat/chat-ui).
121
+
122
+ We modified the code so that various tasks usually handled by the server are done by the browser. This is to **ensure privacy** as we do not want to send user data to the server/AI provider as our solution **places the AI provider outside of our trust model**.
123
+
124
+ ### Philosophy
125
+
126
+ To make AI transparent and confidential, (almost) all of the logic is transported from the server-side to the client-side browser.
127
+
128
+ This ensures end-users’ privacy and gives them control over what happens to their data. For instance, the inference can be done locally using transformers.js, and conversations can be stored in the user's browser chat. This means the operators of the AI service are blind to the user's data, hence the name BlindChat!
129
+
130
+ Data is only sent server-side where our remote enclave mode is selected. With this mode, the server is deployed within a hardened and verifiable environment called an enclave which provides end-to-end protection and prevents external access. Not even the AI provider admins operating the enclave can read users’ data.
131
+
132
+ Note that while our hardened environments don’t fit in with all definitions of an “enclave”, we will use it for convenience’s sake here to describe an environment that allows a server to process data without exposing its contents to service providers.
133
+
134
+ ### Private inference
135
+
136
+ We offer two modes to ensure users’ data remains private:
137
+
138
+ #### On-device inference
139
+
140
+ ![on-device-mode-dark](./assets/on-device-dark.png#gh-dark-mode-only)
141
+ ![on-device-mode-light](./assets/on-device-light.png#gh-light-mode-only)
142
+
143
+ With the on-device mode, the model is sent locally to the users’ browser, and **inference is performed on-device**.
144
+
145
+ This mode is **generally suitable for smaller models** as large models may require too much bandwidth and computational resources.
146
+
147
+ #### Confidential and transparent AI APis with enclaves
148
+
149
+ ![zero-trust-mode-dark](./assets/zero-trust-dark.png#gh-dark-mode-only)
150
+ ![zero-trust-mode-light](./assets/zero-trust-light.png#gh-light-mode-only)
151
+
152
+ With the Zero-trust AI APIs mode, data is sent to a **secure environment** called an **enclave** containing the model for remote inference.
153
+
154
+ These environments provide **end-to-end protection** through robust **isolation and verification**. User data is **never accessible in clear** to the AI provider admins.
155
+
156
+ > You can find out more about Confidential and transparent AI APIs with enclaves in the [guide](https://blindllama.mithrilsecurity.io/en/latest/docs/concepts/hardened-systems/) we provide with our [BlindLlama project](https://blindllama.mithrilsecurity.io/en/latest/), which is the underlying technology for this mode of BlindChat.
157
+
158
+ <p align="right">(<a href="#readme-top">back to top</a>)</p>
159
+
160
+ ### Architecture
161
+
162
+ The project currently has three major components:
163
+
164
+ - **UI:** This is the Chat interface that the end user interacts with. It contains the Chat box, and will contain plugins and other widgets for more complex interaction, such as loading documents or enabling voice commands.
165
+ - **Private LLM:** Developers can customize which LLM they choose to answer users’ queries. Current options are either local models or remote enclaves to ensure transparent and private inference.
166
+ - **Storage:** Developers can customize what kind of storage is used to save information such as conversation history and, in the future, embeddings for RAG.
167
+
168
+ **\*Coming soon:**
169
+
170
+ - **Connectors:** Connectors will allows users to pull documents from various sources, e.g. PDF upload, and share outputs
171
+ - **Integration with Llama Index TS:** This will allow users to index documents with local models, store them in local storage and use them for RAG (query the LLMs based on the information contained in their documents).
172
+
173
+ ## 📊 Comparisons
174
+
175
+ | | Client-side bandwidth requirements | Client-side computing requirements | Model capabilities | Privacy |
176
+ | -------------------- | ---------------------------------- | ---------------------------------- | ------------------ | ------- |
177
+ | On-device prediction | High | High | Low | High |
178
+ | Regular AI APIs | Low | Low | High | Low |
179
+ | Zero-trust AI APIs | Low | Low | High | High |
180
+
181
+ **On-device predictions and Confidential AI APIs both provide privacy** contrary to most existing Conversational AI solutions that expose data to privacy risks.
182
+
183
+ **On-device prediction** has the advantage of providing the highest level of privacy as data does not leave the device but requires downloading models that are several hundreds of MBs to several GBs and require heavy memory and computing resources. For many users, this option will not be possible with larger, higher-performing models due to these device requirements.
184
+
185
+ **Confidential AI APIs** are deployed remotely, meaning the size of models is not restricted by the specifications of user devices. Users are able to query large models while still having robust privacy guarantees.
186
+
187
+ <p align="right">(<a href="#readme-top">back to top</a>)</p>
188
+
189
+ ## 📇 Get in touch
190
+
191
+ We would love to hear your feedback or suggestions, here are the ways you can reach us:
192
+
193
+ - Found a bug? [Open an issue!](https://github.com/mithril-security/blind_chat/issues)
194
+ - Got a suggestion? [Join our Discord community and let us know!](https://discord.com/invite/TxEHagpWd4)
195
+ - Set up [a one-on-one meeting](https://www.mithrilsecurity.io/contact) with a member of our team
196
+
197
+ Want to hear more about our work on privacy in the field AI?
198
+
199
+ - Check out our [blog](https://blog.mithrilsecurity.io/)
200
+ - Subscribe to our newsletter [here](https://blog.mithrilsecurity.io/)
201
+
202
+ Thank you for your support!
203
+
204
+ <p align="right">(<a href="#readme-top">back to top</a>)</p>
205
+
206
+ ## References
207
+
208
+ [1] Carlini, N., Ippolito, D., Jagielski, M., Lee, K., Tramer, F., & Zhang, C. (2022). Quantifying Memorization Across Neural Language Models. ArXiv. /abs/2202.07646
209
+
210
+ <!-- MARKDOWN LINKS & IMAGES -->
211
+
212
+ [project-url]: https://github.com/mithril-security/blind_chat
213
+ [twitter-url]: https://twitter.com/MithrilSecurity
214
+ [contact-url]: https://www.mithrilsecurity.io/contact
215
+ [docs-shield]: https://img.shields.io/badge/Docs-000000?style=for-the-badge&colorB=555
216
+ [docs-url]: https://blindllama.mithrilsecurity.io/en/latest/
217
+ [license-shield]: https://img.shields.io/github/license/mithril-security/aicert.svg?style=for-the-badge
218
+ [contact]: https://img.shields.io/badge/Contact_us-000000?style=for-the-badge&colorB=555
219
+ [project]: https://img.shields.io/badge/Project-000000?style=for-the-badge&colorB=555
220
+ [linkedin-shield]: https://img.shields.io/badge/LinkedIn-0077B5?style=for-the-badge&logo=linkedin&logoColor=white&colorB=555
221
+ [reddit-shield]: https://img.shields.io/badge/reddit-0077B5?style=for-the-badge&logo=reddit&logoColor=white&colorB=FF4500
222
+ [twitter]: https://img.shields.io/badge/Twitter-1DA1F2?style=for-the-badge&logo=twitter&logoColor=white
223
+ [fb-shield]: https://img.shields.io/badge/Facebook-0077B5?style=for-the-badge&logo=facebook&logoColor=white&colorB=3b5998
224
+ [linkedin-url]: https://www.linkedin.com/company/mithril-security-company/
225
+ [website-url]: https://www.mithrilsecurity.io
226
+ [docs-url]: https://blindllama.mithrilsecurity.io/en/latest/
227
+ [website-shield]: https://img.shields.io/badge/website-000000?style=for-the-badge&colorB=555
228
+ [blog-url]: https://blog.mithrilsecurity.io/
229
+ [blog-shield]: https://img.shields.io/badge/Blog-000?style=for-the-badge&logo=ghost&logoColor=yellow&colorB=555
230
+ [facebook-share]: https://www.facebook.com/sharer/sharer.php?u=https%3A//github.com/mithril-security/blind_chat
231
+ [twitter-share]: https://twitter.com/intent/tweet?url=https://github.com/mithril-security/blind_chat&text=Check%20out%20the%20open-source%20project%20to%20build%20a%20private%20Conversational%20AI%20app%20running%20fully%20in-browser
232
+ [linkedin-share]: https://www.linkedin.com/sharing/share-offsite/?url=https://github.com/mithril-security/blind_chat
233
+ [reddit-share]: https://www.reddit.com/submit?url=github.com%2Fmithril-security%2Fblind_chat&title=Private%20in-browser%20Conversational%20AI%20with%20BlindChat
assets/on-device-dark.png ADDED
assets/on-device-light.png ADDED
assets/zero-trust-dark-border.png ADDED
assets/zero-trust-dark.png ADDED
assets/zero-trust-light-border.png ADDED
assets/zero-trust-light.png ADDED
package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
package.json ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "chat-ui",
3
+ "version": "0.0.1",
4
+ "private": true,
5
+ "packageManager": "npm@9.5.0",
6
+ "scripts": {
7
+ "dev": "vite dev",
8
+ "build": "vite build",
9
+ "preview": "vite preview",
10
+ "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
11
+ "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
12
+ "lint": "prettier --plugin-search-dir . --check . && eslint .",
13
+ "format": "prettier --plugin-search-dir . --write .",
14
+ "test": "MONGODB_URL=mongodb://127.0.0.1:27017/ vitest"
15
+ },
16
+ "devDependencies": {
17
+ "@iconify-json/carbon": "^1.1.16",
18
+ "@iconify-json/eos-icons": "^1.1.6",
19
+ "@sveltejs/adapter-node": "^1.2.4",
20
+ "@sveltejs/kit": "^1.15.10",
21
+ "@tailwindcss/typography": "^0.5.9",
22
+ "@types/jsdom": "^21.1.1",
23
+ "@types/marked": "^4.0.8",
24
+ "@types/parquetjs": "^0.10.3",
25
+ "@typescript-eslint/eslint-plugin": "^5.45.0",
26
+ "@typescript-eslint/parser": "^5.45.0",
27
+ "eslint": "^8.28.0",
28
+ "eslint-config-prettier": "^8.5.0",
29
+ "eslint-plugin-svelte": "^2.27.3",
30
+ "prettier": "^2.8.0",
31
+ "prettier-plugin-svelte": "^2.8.1",
32
+ "prettier-plugin-tailwindcss": "^0.2.7",
33
+ "svelte": "^3.58.0",
34
+ "svelte-check": "^3.2.0",
35
+ "tslib": "^2.4.1",
36
+ "typescript": "^4.9.3",
37
+ "unplugin-icons": "^0.16.1",
38
+ "vite": "^4.3.9",
39
+ "vitest": "^0.31.0"
40
+ },
41
+ "type": "module",
42
+ "dependencies": {
43
+ "@huggingface/hub": "^0.5.1",
44
+ "@huggingface/inference": "^2.2.0",
45
+ "@xenova/transformers": "^2.0.0",
46
+ "autoprefixer": "^10.4.14",
47
+ "aws4fetch": "^1.0.17",
48
+ "date-fns": "^2.29.3",
49
+ "dexie": "^3.2.4",
50
+ "dotenv": "^16.0.3",
51
+ "handlebars": "^4.7.8",
52
+ "highlight.js": "^11.7.0",
53
+ "jsdom": "^22.0.0",
54
+ "marked": "^4.3.0",
55
+ "nanoid": "^4.0.2",
56
+ "openid-client": "^5.4.2",
57
+ "parquetjs": "^0.11.2",
58
+ "postcss": "^8.4.21",
59
+ "save": "^2.9.0",
60
+ "serpapi": "^1.1.1",
61
+ "svelte-device-info": "^1.0.0",
62
+ "tailwind-scrollbar": "^3.0.0",
63
+ "tailwindcss": "^3.3.1",
64
+ "uuid": "^9.0.1",
65
+ "zod": "^3.21.4"
66
+ }
67
+ }
postcss.config.js ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ export default {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ };
src/app.d.ts ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /// <reference types="@sveltejs/kit" />
2
+ /// <reference types="unplugin-icons/types/svelte" />
3
+
4
+ import type { User } from "$lib/types/User";
5
+
6
+ // See https://kit.svelte.dev/docs/types#app
7
+ // for information about these interfaces
8
+ declare global {
9
+ namespace App {
10
+ // interface Error {}
11
+ interface Locals {
12
+ sessionId: string;
13
+ user?: User;
14
+ }
15
+ // interface PageData {}
16
+ // interface Platform {}
17
+ }
18
+ }
19
+
20
+ export {};
src/app.html ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en" class="h-full">
3
+ <link rel="stylesheet" href="https://www.w3schools.com/w3css/4/w3.css" />
4
+ <head>
5
+ <meta charset="utf-8" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" />
7
+ <script>
8
+ if (
9
+ localStorage.theme === "dark" ||
10
+ (!("theme" in localStorage) && window.matchMedia("(prefers-color-scheme: dark)").matches)
11
+ ) {
12
+ document.documentElement.classList.add("dark");
13
+ }
14
+
15
+ // For some reason, Sveltekit doesn't let us load env variables from .env here, so we load it from hooks.server.ts
16
+ window.gaId = "%gaId%";
17
+ window.gaIdDeprecated = "%gaIdDeprecated%";
18
+ </script>
19
+ %sveltekit.head%
20
+ </head>
21
+ <body data-sveltekit-preload-data="hover" class="h-full dark:bg-gray-900">
22
+ <div id="app" class="contents h-full">%sveltekit.body%</div>
23
+
24
+ <!-- Google Tag Manager -->
25
+ <script>
26
+ if (window.gaId) {
27
+ const script = document.createElement("script");
28
+ script.src = "https://www.googletagmanager.com/gtag/js?id=" + window.gaId;
29
+ script.async = true;
30
+ document.head.appendChild(script);
31
+
32
+ window.dataLayer = window.dataLayer || [];
33
+ function gtag() {
34
+ dataLayer.push(arguments);
35
+ }
36
+ gtag("js", new Date());
37
+ /// ^ See https://developers.google.com/tag-platform/gtagjs/install
38
+ gtag("config", window.gaId);
39
+ gtag("consent", "default", { ad_storage: "denied", analytics_storage: "denied" });
40
+ /// ^ See https://developers.google.com/tag-platform/gtagjs/reference#consent
41
+ /// TODO: ask the user for their consent and update this with gtag('consent', 'update')
42
+ }
43
+ </script>
44
+
45
+ <!-- Google Analytics v3 (deprecated on 1 July 2023) -->
46
+ <script>
47
+ if (window.gaIdDeprecated) {
48
+ (function (i, s, o, g, r, a, m) {
49
+ i["GoogleAnalyticsObject"] = r;
50
+ (i[r] =
51
+ i[r] ||
52
+ function () {
53
+ (i[r].q = i[r].q || []).push(arguments);
54
+ }),
55
+ (i[r].l = 1 * new Date());
56
+ (a = s.createElement(o)), (m = s.getElementsByTagName(o)[0]);
57
+ a.async = 1;
58
+ a.src = g;
59
+ m.parentNode.insertBefore(a, m);
60
+ })(
61
+ window,
62
+ document,
63
+ "script",
64
+ "https://www.google-analytics.com/analytics.js",
65
+ "ganalytics"
66
+ );
67
+ ganalytics("create", window.gaIdDeprecated, "auto");
68
+ ganalytics("send", "pageview");
69
+ }
70
+ </script>
71
+ </body>
72
+ </html>
src/hooks.server.ts ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { COOKIE_NAME, MESSAGES_BEFORE_LOGIN } from "$env/static/private";
2
+ import type { Handle } from "@sveltejs/kit";
3
+ import {
4
+ PUBLIC_GOOGLE_ANALYTICS_ID,
5
+ PUBLIC_DEPRECATED_GOOGLE_ANALYTICS_ID,
6
+ PUBLIC_ORIGIN,
7
+ PUBLIC_APP_DISCLAIMER,
8
+ } from "$env/static/public";
9
+ import { collections } from "$lib/server/database";
10
+ import { base } from "$app/paths";
11
+ import { refreshSessionCookie, requiresUser } from "$lib/server/auth";
12
+ import { ERROR_MESSAGES } from "$lib/stores/errors";
13
+
14
+ export const handle: Handle = async ({ event, resolve }) => {
15
+ const token = event.cookies.get(COOKIE_NAME);
16
+
17
+ event.locals.sessionId = token || crypto.randomUUID();
18
+
19
+ function errorResponse(status: number, message: string) {
20
+ const sendJson =
21
+ event.request.headers.get("accept")?.includes("application/json") ||
22
+ event.request.headers.get("content-type")?.includes("application/json");
23
+ return new Response(sendJson ? JSON.stringify({ error: message }) : message, {
24
+ status,
25
+ headers: {
26
+ "content-type": sendJson ? "application/json" : "text/plain",
27
+ },
28
+ });
29
+ }
30
+
31
+ // CSRF protection
32
+ const requestContentType = event.request.headers.get("content-type")?.split(";")[0] ?? "";
33
+ /** https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form#attr-enctype */
34
+ const nativeFormContentTypes = [
35
+ "multipart/form-data",
36
+ "application/x-www-form-urlencoded",
37
+ "text/plain",
38
+ ];
39
+ if (event.request.method === "POST" && nativeFormContentTypes.includes(requestContentType)) {
40
+ const referer = event.request.headers.get("referer");
41
+
42
+ if (!referer) {
43
+ return errorResponse(403, "Non-JSON form requests need to have a referer");
44
+ }
45
+
46
+ const validOrigins = [
47
+ new URL(event.request.url).origin,
48
+ ...(PUBLIC_ORIGIN ? [new URL(PUBLIC_ORIGIN).origin] : []),
49
+ ];
50
+
51
+ if (!validOrigins.includes(new URL(referer).origin)) {
52
+ return errorResponse(403, "Invalid referer for POST request");
53
+ }
54
+ }
55
+
56
+ if (
57
+ !event.url.pathname.startsWith(`${base}/login`) &&
58
+ !event.url.pathname.startsWith(`${base}/admin`) &&
59
+ !["GET", "OPTIONS", "HEAD"].includes(event.request.method)
60
+ ) {
61
+ if (
62
+ !user &&
63
+ requiresUser &&
64
+ !((MESSAGES_BEFORE_LOGIN ? parseInt(MESSAGES_BEFORE_LOGIN) : 0) > 0)
65
+ ) {
66
+ return errorResponse(401, ERROR_MESSAGES.authOnly);
67
+ }
68
+
69
+ // if login is not required and the call is not from /settings and we display the ethics modal with PUBLIC_APP_DISCLAIMER
70
+ // we check if the user has accepted the ethics modal first.
71
+ // If login is required, `ethicsModalAcceptedAt` is already true at this point, so do not pass this condition. This saves a DB call.
72
+ if (
73
+ !requiresUser &&
74
+ !event.url.pathname.startsWith(`${base}/settings`) &&
75
+ !!PUBLIC_APP_DISCLAIMER
76
+ ) {
77
+ const hasAcceptedEthicsModal = await collections.settings.countDocuments({
78
+ sessionId: event.locals.sessionId,
79
+ ethicsModalAcceptedAt: { $exists: true },
80
+ });
81
+
82
+ if (!hasAcceptedEthicsModal) {
83
+ return errorResponse(405, "You need to accept the welcome modal first");
84
+ }
85
+ }
86
+ }
87
+
88
+ refreshSessionCookie(event.cookies, event.locals.sessionId);
89
+
90
+ let replaced = false;
91
+
92
+ const response = await resolve(event, {
93
+ transformPageChunk: (chunk) => {
94
+ // For some reason, Sveltekit doesn't let us load env variables from .env in the app.html template
95
+ if (replaced || !chunk.html.includes("%gaId%") || !chunk.html.includes("%gaIdDeprecated%")) {
96
+ return chunk.html;
97
+ }
98
+ replaced = true;
99
+
100
+ return chunk.html
101
+ .replace("%gaId%", PUBLIC_GOOGLE_ANALYTICS_ID)
102
+ .replace("%gaIdDeprecated%", PUBLIC_DEPRECATED_GOOGLE_ANALYTICS_ID);
103
+ },
104
+ });
105
+
106
+ return response;
107
+ };
src/lib/actions/snapScrollToBottom.ts ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { navigating } from "$app/stores";
2
+ import { tick } from "svelte";
3
+ import { get } from "svelte/store";
4
+
5
+ const detachedOffset = 10;
6
+
7
+ /**
8
+ * @param node element to snap scroll to bottom
9
+ * @param dependency pass in a dependency to update scroll on changes.
10
+ */
11
+ export const snapScrollToBottom = (node: HTMLElement, dependency: unknown) => {
12
+ let prevScrollValue = node.scrollTop;
13
+ let isDetached = false;
14
+
15
+ const handleScroll = () => {
16
+ // if user scrolled up, we detach
17
+ if (node.scrollTop < prevScrollValue) {
18
+ isDetached = true;
19
+ }
20
+
21
+ // if user scrolled back to within 10px of bottom, we reattach
22
+ if (node.scrollTop - (node.scrollHeight - node.clientHeight) >= -detachedOffset) {
23
+ isDetached = false;
24
+ }
25
+
26
+ prevScrollValue = node.scrollTop;
27
+ };
28
+
29
+ const updateScroll = async (_options: { force?: boolean } = {}) => {
30
+ const defaultOptions = { force: false };
31
+ const options = { ...defaultOptions, ..._options };
32
+ const { force } = options;
33
+
34
+ if (!force && isDetached && !get(navigating)) return;
35
+
36
+ // wait for next tick to ensure that the DOM is updated
37
+ await tick();
38
+
39
+ node.scrollTo({ top: node.scrollHeight });
40
+ };
41
+
42
+ node.addEventListener("scroll", handleScroll);
43
+
44
+ if (dependency) {
45
+ updateScroll({ force: true });
46
+ }
47
+
48
+ return {
49
+ update: updateScroll,
50
+ destroy: () => {
51
+ node.removeEventListener("scroll", handleScroll);
52
+ },
53
+ };
54
+ };
src/lib/buildPrompt.ts ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { BackendModel } from "./server/models";
2
+ import type { Message } from "./types/Message";
3
+ import { collections } from "$lib/server/database";
4
+ import { authCondition } from "./server/auth";
5
+ /**
6
+ * Convert [{user: "assistant", content: "hi"}, {user: "user", content: "hello"}] to:
7
+ *
8
+ * <|assistant|>hi<|endoftext|><|prompter|>hello<|endoftext|><|assistant|>
9
+ */
10
+
11
+ interface buildPromptOptions {
12
+ messages: Pick<Message, "from" | "content">[];
13
+ model: BackendModel;
14
+ locals?: App.Locals;
15
+ webSearchId?: string;
16
+ preprompt?: string;
17
+ }
18
+
19
+ export async function buildPrompt({
20
+ messages,
21
+ model,
22
+ locals,
23
+ webSearchId,
24
+ preprompt,
25
+ }: buildPromptOptions): Promise<string> {
26
+ return (
27
+ model
28
+ .chatPromptRender({ messages, preprompt })
29
+ // Not super precise, but it's truncated in the model's backend anyway
30
+ .split(" ")
31
+ .slice(-(model.parameters?.truncate ?? 0))
32
+ .join(" ")
33
+ );
34
+ }
src/lib/components/AnnouncementBanner.svelte ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ export let title = "";
3
+ export let classNames = "";
4
+ </script>
5
+
6
+ <div class="flex items-center rounded-xl bg-gray-100 p-1 text-sm dark:bg-gray-800 {classNames}">
7
+ <span
8
+ class="mr-2 inline-flex items-center rounded-lg bg-gradient-to-br from-primary-300 px-2 py-1 text-xxs font-medium uppercase leading-3 text-primary-700 dark:from-primary-900 dark:text-primary-400"
9
+ >New</span
10
+ >
11
+ {title}
12
+ <div class="ml-auto shrink-0">
13
+ <slot />
14
+ </div>
15
+ </div>
src/lib/components/CodeBlock.svelte ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { afterUpdate } from "svelte";
3
+ import CopyToClipBoardBtn from "./CopyToClipBoardBtn.svelte";
4
+
5
+ export let code = "";
6
+ export let lang = "";
7
+
8
+ $: highlightedCode = "";
9
+
10
+ afterUpdate(async () => {
11
+ const { default: hljs } = await import("highlight.js");
12
+ const language = hljs.getLanguage(lang);
13
+
14
+ highlightedCode = hljs.highlightAuto(code, language?.aliases).value;
15
+ });
16
+ </script>
17
+
18
+ <div class="group relative my-4 rounded-lg">
19
+ <!-- eslint-disable svelte/no-at-html-tags -->
20
+ <pre
21
+ class="scrollbar-custom overflow-auto px-5 scrollbar-thumb-gray-500 hover:scrollbar-thumb-gray-400 dark:scrollbar-thumb-white/10 dark:hover:scrollbar-thumb-white/20"><code
22
+ class="language-{lang}">{@html highlightedCode || code.replaceAll("<", "&lt;")}</code
23
+ ></pre>
24
+ <CopyToClipBoardBtn
25
+ classNames="absolute top-2 right-2 invisible opacity-0 group-hover:visible group-hover:opacity-100"
26
+ value={code}
27
+ />
28
+ </div>
src/lib/components/CopyToClipBoardBtn.svelte ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { onDestroy } from "svelte";
3
+
4
+ import IconCopy from "./icons/IconCopy.svelte";
5
+ import Tooltip from "./Tooltip.svelte";
6
+
7
+ export let classNames = "";
8
+ export let value: string;
9
+
10
+ let isSuccess = false;
11
+ let timeout: ReturnType<typeof setTimeout>;
12
+
13
+ const handleClick = async () => {
14
+ // writeText() can be unavailable or fail in some cases (iframe, etc) so we try/catch
15
+ try {
16
+ await navigator.clipboard.writeText(value);
17
+
18
+ isSuccess = true;
19
+ if (timeout) {
20
+ clearTimeout(timeout);
21
+ }
22
+ timeout = setTimeout(() => {
23
+ isSuccess = false;
24
+ }, 1000);
25
+ } catch (err) {
26
+ console.error(err);
27
+ }
28
+ };
29
+
30
+ onDestroy(() => {
31
+ if (timeout) {
32
+ clearTimeout(timeout);
33
+ }
34
+ });
35
+ </script>
36
+
37
+ <button
38
+ class="btn rounded-lg border border-gray-200 px-2 py-2 text-sm shadow-sm transition-all hover:border-gray-300 active:shadow-inner dark:border-gray-600 dark:hover:border-gray-400 {classNames}
39
+ {!isSuccess && 'text-gray-200 dark:text-gray-200'}
40
+ {isSuccess && 'text-green-500'}
41
+ "
42
+ title={"Copy to clipboard"}
43
+ type="button"
44
+ on:click={handleClick}
45
+ >
46
+ <span class="relative">
47
+ <IconCopy />
48
+ <Tooltip classNames={isSuccess ? "opacity-100" : "opacity-0"} />
49
+ </span>
50
+ </button>
src/lib/components/LoadingModal.svelte ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import Modal from "$lib/components/Modal.svelte";
3
+ import { progress_writable, curr_model_writable, map_writable } from "./LoadingModalWritable.js";
4
+
5
+ const forceUpdate = async (_) => {};
6
+
7
+ let loadingMap = new Map<string, number>();
8
+ let pr = 1;
9
+
10
+ map_writable.subscribe((value) => {
11
+ const [model, percent] = value;
12
+ pr = Number(percent);
13
+ if (model.startsWith("onnx")) {
14
+ loadingMap.set(model, Math.floor(Number(percent)));
15
+ //console.log(loadingMap);
16
+ }
17
+ });
18
+ </script>
19
+
20
+ <Modal>
21
+ <div class="flex w-full flex-col gap-0 p-2">
22
+ <div class="flex items-start text-xl font-bold text-gray-800">
23
+ <h2>Loading the model...</h2>
24
+ <br />
25
+ </div>
26
+ <div class="flex items-start text-s text-gray-800">
27
+ <br>Please wait while we download the model. This has to be done only once.
28
+ </div>
29
+ <br />
30
+ {#await forceUpdate(pr) then _}
31
+ {#each [...loadingMap] as [key, value]}
32
+ <p class="text-s text-gray-800">{key}</p>
33
+ <div class="w3-light-grey">
34
+ <div class="w3-blue" style="width:{value}%">{value}%</div>
35
+ </div>
36
+ <br />
37
+ {/each}
38
+ {/await}
39
+ </div>
40
+ </Modal>
src/lib/components/LoadingModalWritable.js ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ import { writable } from "svelte/store";
2
+
3
+ export const progress_writable = writable(0);
4
+ export const curr_model_writable = writable("");
5
+ export const map_writable = writable(["", ""]);
src/lib/components/LoginModal.svelte ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { browser } from "$app/environment";
3
+ import { base } from "$app/paths";
4
+ import { page } from "$app/stores";
5
+ import { PUBLIC_APP_DATA_SHARING, PUBLIC_APP_NAME, PUBLIC_VERSION } from "$env/static/public";
6
+ import LogoHuggingFaceBorderless from "$lib/components/icons/LogoHuggingFaceBorderless.svelte";
7
+ import Modal from "$lib/components/Modal.svelte";
8
+ import type { LayoutData } from "../../routes/$types";
9
+ import Logo from "./icons/Logo.svelte";
10
+ export let settings: LayoutData["settings"];
11
+
12
+ const isIframe = browser && window.self !== window.parent;
13
+ </script>
14
+
15
+ <Modal>
16
+ <div
17
+ class="flex w-full flex-col items-center gap-6 bg-gradient-to-t from-primary-500/40 via-primary-500/10 to-primary-500/0 px-4 pb-10 pt-9 text-center "
18
+ >
19
+ <h2 class="flex items-center text-2xl font-semibold text-gray-800">
20
+ <Logo classNames="mr-1" />
21
+ {PUBLIC_APP_NAME}
22
+ <div
23
+ class="ml-3 flex h-6 items-center rounded-lg border border-gray-100 bg-gray-50 px-2 text-base text-gray-400"
24
+ >
25
+ v{PUBLIC_VERSION}
26
+ </div>
27
+ </h2>
28
+ {#if $page.data.requiresLogin}
29
+ <p
30
+ class="px-4 text-lg font-semibold leading-snug text-gray-800 sm:px-12"
31
+ style="text-wrap: balance;"
32
+ >
33
+ Please Sign in with Hugging Face to continue
34
+ </p>
35
+ {/if}
36
+ <p class="text-base text-gray-800">
37
+ Disclaimer: AI is an area of active research with known problems such as biased generation and
38
+ misinformation. Do not use this application for high-stakes decisions or advice.
39
+ </p>
40
+ {#if PUBLIC_APP_DATA_SHARING}
41
+ <p class="px-2 text-sm text-gray-500">
42
+ Your conversations will be shared with model authors unless you disable it from your
43
+ settings.
44
+ </p>
45
+ {/if}
46
+ <form
47
+ action="{base}/{$page.data.requiresLogin ? 'login' : 'settings'}"
48
+ target={isIframe ? "_blank" : ""}
49
+ method="POST"
50
+ class="flex w-full flex-col items-center gap-2"
51
+ >
52
+ {#if $page.data.requiresLogin}
53
+ <button
54
+ type="submit"
55
+ class="mt-2 flex items-center whitespace-nowrap rounded-full bg-black px-5 py-2 text-lg font-semibold text-gray-100 transition-colors hover:bg-primary-500"
56
+ >
57
+ Sign in
58
+ {#if PUBLIC_APP_NAME === "HuggingChat"}
59
+ with <LogoHuggingFaceBorderless classNames="text-xl mr-1 ml-1.5" /> Hugging Face
60
+ {/if}
61
+ </button>
62
+ {:else}
63
+ <input type="hidden" name="ethicsModalAccepted" value={true} />
64
+ {#each Object.entries(settings) as [key, val]}
65
+ <input type="hidden" name={key} value={val} />
66
+ {/each}
67
+ <button
68
+ type="submit"
69
+ class="mt-2 rounded-full bg-black px-5 py-2 text-lg font-semibold text-gray-100 transition-colors hover:bg-primary-500"
70
+ >
71
+ Start chatting
72
+ </button>
73
+ {/if}
74
+ </form>
75
+ </div>
76
+ </Modal>
src/lib/components/MobileNav.svelte ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { navigating } from "$app/stores";
3
+ import { createEventDispatcher } from "svelte";
4
+ import { browser } from "$app/environment";
5
+ import { base } from "$app/paths";
6
+
7
+ import CarbonClose from "~icons/carbon/close";
8
+ import CarbonAdd from "~icons/carbon/add";
9
+ import CarbonTextAlignJustify from "~icons/carbon/text-align-justify";
10
+
11
+ export let isOpen = false;
12
+ export let title: string | undefined;
13
+
14
+ $: title = title || "New Chat";
15
+
16
+ let closeEl: HTMLButtonElement;
17
+ let openEl: HTMLButtonElement;
18
+
19
+ const dispatch = createEventDispatcher();
20
+
21
+ $: if ($navigating) {
22
+ dispatch("toggle", false);
23
+ }
24
+
25
+ $: if (isOpen && closeEl) {
26
+ closeEl.focus();
27
+ } else if (!isOpen && browser && document.activeElement === closeEl) {
28
+ openEl.focus();
29
+ }
30
+ </script>
31
+
32
+ <nav
33
+ class="flex h-12 items-center justify-between border-b bg-gray-50 px-4 dark:border-gray-800 dark:bg-gray-800/70 md:hidden"
34
+ >
35
+ <button
36
+ type="button"
37
+ class="-ml-3 flex h-9 w-9 shrink-0 items-center justify-center"
38
+ on:click={() => dispatch("toggle", true)}
39
+ aria-label="Open menu"
40
+ bind:this={openEl}><CarbonTextAlignJustify /></button
41
+ >
42
+ <span class="truncate px-4">{title}</span>
43
+ <a href={`${base}/`} class="-mr-3 flex h-9 w-9 shrink-0 items-center justify-center"
44
+ ><CarbonAdd /></a
45
+ >
46
+ </nav>
47
+ <nav
48
+ class="fixed inset-0 z-30 grid max-h-screen grid-cols-1 grid-rows-[auto,auto,1fr,auto] bg-white bg-gradient-to-l from-gray-50 dark:bg-gray-900 dark:from-gray-800/30 {isOpen
49
+ ? 'block'
50
+ : 'hidden'}"
51
+ >
52
+ <div class="flex h-12 items-center px-4">
53
+ <button
54
+ type="button"
55
+ class="-mr-3 ml-auto flex h-9 w-9 items-center justify-center"
56
+ on:click={() => dispatch("toggle", false)}
57
+ aria-label="Close menu"
58
+ bind:this={closeEl}><CarbonClose /></button
59
+ >
60
+ </div>
61
+ <slot />
62
+ </nav>
src/lib/components/MobileWarningModal.svelte ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { createEventDispatcher } from "svelte";
3
+ import Modal from "$lib/components/Modal.svelte";
4
+ import CarbonClose from "~icons/carbon/close";
5
+ import Switch from "$lib/components/Switch.svelte";
6
+ import { enhance } from "$app/forms";
7
+ import { base } from "$app/paths";
8
+ import { PUBLIC_APP_DATA_SHARING } from "$env/static/public";
9
+ import type { Model } from "$lib/types/Model";
10
+ import type { LayoutData } from "../../routes/$types";
11
+
12
+ const dispatch = createEventDispatcher<{ close: void }>();
13
+ </script>
14
+
15
+ <Modal>
16
+ <div class="flex w-full flex-col gap-5 p-6">
17
+ <div class="flex items-start justify-between text-xl font-semibold text-gray-800">
18
+ <h2>Warning</h2>
19
+ </div>
20
+ <p class="text-gray-800">
21
+ This chat is still a beta. Therefore, it might have some issues on your phone. Use at your own
22
+ risk.
23
+ </p>
24
+ </div>
25
+ </Modal>
src/lib/components/Modal.svelte ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { createEventDispatcher, onDestroy, onMount } from "svelte";
3
+ import { cubicOut } from "svelte/easing";
4
+ import { fade } from "svelte/transition";
5
+ import Portal from "./Portal.svelte";
6
+ import { browser } from "$app/environment";
7
+
8
+ export let width = "max-w-sm";
9
+
10
+ let backdropEl: HTMLDivElement;
11
+ let modalEl: HTMLDivElement;
12
+
13
+ const dispatch = createEventDispatcher<{ close: void }>();
14
+
15
+ function handleKeydown(event: KeyboardEvent) {
16
+ // close on ESC
17
+ if (event.key === "Escape") {
18
+ event.preventDefault();
19
+ dispatch("close");
20
+ }
21
+ }
22
+
23
+ function handleBackdropClick(event: MouseEvent) {
24
+ if (event.target === backdropEl) {
25
+ dispatch("close");
26
+ }
27
+ }
28
+
29
+ onMount(() => {
30
+ document.getElementById("app")?.setAttribute("inert", "true");
31
+ modalEl.focus();
32
+ });
33
+
34
+ onDestroy(() => {
35
+ if (!browser) return;
36
+ // remove inert attribute if this is the last modal
37
+ if (document.querySelectorAll('[role="dialog"]:not(#app *)').length === 1) {
38
+ document.getElementById("app")?.removeAttribute("inert");
39
+ }
40
+ });
41
+ </script>
42
+
43
+ <Portal>
44
+ <div
45
+ role="presentation"
46
+ tabindex="-1"
47
+ bind:this={backdropEl}
48
+ on:click={handleBackdropClick}
49
+ transition:fade={{ easing: cubicOut, duration: 300 }}
50
+ class="fixed inset-0 z-40 flex items-center justify-center bg-black/80 p-8 backdrop-blur-sm dark:bg-black/50"
51
+ >
52
+ <div
53
+ role="dialog"
54
+ tabindex="-1"
55
+ bind:this={modalEl}
56
+ on:keydown={handleKeydown}
57
+ class="-mt-10 overflow-hidden rounded-2xl bg-white shadow-2xl outline-none md:-mt-20 {width}"
58
+ >
59
+ <slot />
60
+ </div>
61
+ </div>
62
+ </Portal>
src/lib/components/ModelCardMetadata.svelte ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import CarbonEarth from "~icons/carbon/earth";
3
+ import CarbonArrowUpRight from "~icons/carbon/arrow-up-right";
4
+ import type { Model } from "$lib/types/Model";
5
+
6
+ export let model: Pick<Model, "name" | "datasetName" | "websiteUrl" | "modelUrl" | "datasetUrl">;
7
+
8
+ export let variant: "light" | "dark" = "light";
9
+ </script>
10
+
11
+ <div
12
+ class="flex items-center gap-5 rounded-xl bg-gray-100 px-3 py-2 text-sm
13
+ {variant === 'dark'
14
+ ? 'text-gray-600 dark:bg-gray-800 dark:text-gray-300'
15
+ : 'text-gray-800 dark:bg-gray-100 dark:text-gray-600'}"
16
+ >
17
+ <a
18
+ href={model.modelUrl || "https://huggingface.co/" + model.name}
19
+ target="_blank"
20
+ rel="noreferrer"
21
+ class="flex items-center hover:underline"
22
+ ><CarbonArrowUpRight class="mr-1.5 shrink-0 text-xs text-gray-400" />
23
+ Model
24
+ <div class="max-sm:hidden">&nbsp;page</div></a
25
+ >
26
+ {#if model.datasetName || model.datasetUrl}
27
+ <a
28
+ href={model.datasetUrl || "https://huggingface.co/datasets/" + model.datasetName}
29
+ target="_blank"
30
+ rel="noreferrer"
31
+ class="flex items-center hover:underline"
32
+ ><CarbonArrowUpRight class="mr-1.5 shrink-0 text-xs text-gray-400" />
33
+ Dataset
34
+ <div class="max-sm:hidden">&nbsp;page</div></a
35
+ >
36
+ {/if}
37
+ {#if model.websiteUrl}
38
+ <a
39
+ href={model.websiteUrl}
40
+ target="_blank"
41
+ class="ml-auto flex items-center hover:underline"
42
+ rel="noreferrer"
43
+ >
44
+ <CarbonEarth class="mr-1.5 shrink-0 text-xs text-gray-400" />
45
+ Website
46
+ </a>
47
+ {/if}
48
+ </div>
src/lib/components/ModelsModal.svelte ADDED
@@ -0,0 +1,149 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { createEventDispatcher } from "svelte";
3
+
4
+ import Modal from "$lib/components/Modal.svelte";
5
+ import CarbonClose from "~icons/carbon/close";
6
+ import CarbonCheckmark from "~icons/carbon/checkmark-filled";
7
+ import ModelCardMetadata from "./ModelCardMetadata.svelte";
8
+ import type { Model } from "$lib/types/Model";
9
+ import type { LayoutData } from "../../routes/$types";
10
+ import { enhance } from "$app/forms";
11
+ import { base } from "$app/paths";
12
+
13
+ import CarbonEdit from "~icons/carbon/edit";
14
+ import CarbonSave from "~icons/carbon/save";
15
+ import CarbonRestart from "~icons/carbon/restart";
16
+
17
+ export let settings: LayoutData["settings"];
18
+ export let models: Array<Model>;
19
+
20
+ let selectedModelId = settings.activeModel;
21
+
22
+ const dispatch = createEventDispatcher<{ close: void }>();
23
+
24
+ let expanded = false;
25
+
26
+ function onToggle() {
27
+ if (expanded) {
28
+ settings.customPrompts[selectedModelId] = value;
29
+ }
30
+ expanded = !expanded;
31
+ }
32
+
33
+ let value = "";
34
+
35
+ function onModelChange() {
36
+ value =
37
+ settings.customPrompts[selectedModelId] ??
38
+ models.filter((el) => el.id === selectedModelId)[0].preprompt ??
39
+ "";
40
+ }
41
+
42
+ $: selectedModelId, onModelChange();
43
+ </script>
44
+
45
+ <Modal width="max-w-lg" on:close>
46
+ <form
47
+ action="{base}/settings"
48
+ method="post"
49
+ on:submit={() => {
50
+ if (expanded) {
51
+ onToggle();
52
+ }
53
+ }}
54
+ use:enhance={() => {
55
+ dispatch("close");
56
+ }}
57
+ class="flex w-full flex-col gap-5 p-6"
58
+ >
59
+ {#each Object.entries(settings).filter(([k]) => !(k == "activeModel" || k === "customPrompts")) as [key, val]}
60
+ <input type="hidden" name={key} value={val} />
61
+ {/each}
62
+ <input type="hidden" name="customPrompts" value={JSON.stringify(settings.customPrompts)} />
63
+ <div class="flex items-start justify-between text-xl font-semibold text-gray-800">
64
+ <h2>Models</h2>
65
+ <button type="button" class="group" on:click={() => dispatch("close")}>
66
+ <CarbonClose class="text-gray-900 group-hover:text-gray-500" />
67
+ </button>
68
+ </div>
69
+
70
+ <div class="space-y-4">
71
+ {#each models as model}
72
+ {@const active = model.id === selectedModelId}
73
+ <div
74
+ class="rounded-xl border border-gray-100 {active
75
+ ? 'bg-gradient-to-r from-primary-200/40 via-primary-500/10'
76
+ : ''}"
77
+ >
78
+ <label class="group flex cursor-pointer p-3" on:change aria-label={model.displayName}>
79
+ <input
80
+ type="radio"
81
+ class="sr-only"
82
+ name="activeModel"
83
+ value={model.id}
84
+ bind:group={selectedModelId}
85
+ />
86
+ <span>
87
+ <span class="text-md block font-semibold leading-tight text-gray-800"
88
+ >{model.displayName}</span
89
+ >
90
+ {#if model.description}
91
+ <span class="text-xs text-[#9FA8B5]">{model.description}</span>
92
+ {/if}
93
+ </span>
94
+ <CarbonCheckmark
95
+ class="-mr-1 -mt-1 ml-auto shrink-0 text-xl {active
96
+ ? 'text-primary-400'
97
+ : 'text-transparent group-hover:text-gray-200'}"
98
+ />
99
+ </label>
100
+ {#if active}
101
+ <div class=" overflow-hidden rounded-xl px-3 pb-2">
102
+ <div class="flex flex-row flex-nowrap gap-2 pb-1">
103
+ <div class="text-xs font-semibold text-gray-500">System Prompt</div>
104
+ {#if expanded}
105
+ <button
106
+ class="text-gray-500 hover:text-gray-900"
107
+ on:click|preventDefault={onToggle}
108
+ >
109
+ <CarbonSave class="text-sm " />
110
+ </button>
111
+ <button
112
+ class="text-gray-500 hover:text-gray-900"
113
+ on:click|preventDefault={() => {
114
+ value = model.preprompt ?? "";
115
+ }}
116
+ >
117
+ <CarbonRestart class="text-sm " />
118
+ </button>
119
+ {:else}
120
+ <button
121
+ class=" text-gray-500 hover:text-gray-900"
122
+ on:click|preventDefault={onToggle}
123
+ >
124
+ <CarbonEdit class="text-sm " />
125
+ </button>
126
+ {/if}
127
+ </div>
128
+ <textarea
129
+ enterkeyhint="send"
130
+ tabindex="0"
131
+ rows="1"
132
+ class="h-20 w-full resize-none scroll-p-3 overflow-x-hidden overflow-y-scroll rounded-md border border-gray-300 bg-transparent p-1 text-xs outline-none focus:ring-0 focus-visible:ring-0"
133
+ bind:value
134
+ hidden={!expanded}
135
+ />
136
+ </div>
137
+ {/if}
138
+ <ModelCardMetadata {model} />
139
+ </div>
140
+ {/each}
141
+ </div>
142
+ <button
143
+ type="submit"
144
+ class="mt-2 rounded-full bg-black px-5 py-2 text-lg font-semibold text-gray-100 ring-gray-400 ring-offset-1 transition-colors hover:ring"
145
+ >
146
+ Apply
147
+ </button>
148
+ </form>
149
+ </Modal>
src/lib/components/NavConversationItem.svelte ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { base } from "$app/paths";
3
+ import { page } from "$app/stores";
4
+ import { createEventDispatcher } from "svelte";
5
+ import { params_writable } from "../../routes/conversation/[id]/ParamsWritable";
6
+
7
+ import CarbonCheckmark from "~icons/carbon/checkmark";
8
+ import CarbonTrashCan from "~icons/carbon/trash-can";
9
+ import CarbonClose from "~icons/carbon/close";
10
+ import CarbonEdit from "~icons/carbon/edit";
11
+
12
+ export let conv: { id: string; title: string };
13
+
14
+ let confirmDelete = false;
15
+
16
+ const dispatch = createEventDispatcher<{
17
+ deleteConversation: string;
18
+ editConversationTitle: { id: string; title: string };
19
+ }>();
20
+ </script>
21
+
22
+ <a
23
+ data-sveltekit-noscroll
24
+ on:mouseleave={() => {
25
+ confirmDelete = false;
26
+ }}
27
+ on:click = {() => {params_writable.set(conv.id)}}
28
+ href="{base}/conversation/{conv.id}"
29
+ class="group flex h-11 flex-none items-center gap-1.5 rounded-lg pl-3 pr-2 text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700 {conv.id ===
30
+ $page.params.id
31
+ ? 'bg-gray-100 dark:bg-gray-700'
32
+ : ''}"
33
+ >
34
+ <div class="flex-1 truncate">
35
+ {#if confirmDelete}
36
+ <span class="font-semibold"> Delete </span>
37
+ {/if}
38
+ {conv.title}
39
+ </div>
40
+
41
+ {#if confirmDelete}
42
+ <button
43
+ type="button"
44
+ class="flex h-5 w-5 items-center justify-center rounded md:hidden md:group-hover:flex"
45
+ title="Confirm delete action"
46
+ on:click|preventDefault={() => dispatch("deleteConversation", conv.id)}
47
+ >
48
+ <CarbonCheckmark class="text-xs text-gray-400 hover:text-gray-500 dark:hover:text-gray-300" />
49
+ </button>
50
+ <button
51
+ type="button"
52
+ class="flex h-5 w-5 items-center justify-center rounded md:hidden md:group-hover:flex"
53
+ title="Cancel delete action"
54
+ on:click|preventDefault={() => {
55
+ confirmDelete = false;
56
+ }}
57
+ >
58
+ <CarbonClose class="text-xs text-gray-400 hover:text-gray-500 dark:hover:text-gray-300" />
59
+ </button>
60
+ {:else}
61
+ <button
62
+ type="button"
63
+ class="flex h-5 w-5 items-center justify-center rounded md:hidden md:group-hover:flex"
64
+ title="Edit conversation title"
65
+ on:click|preventDefault={() => {
66
+ const newTitle = prompt("Edit this conversation title:", conv.title);
67
+ if (!newTitle) return;
68
+ dispatch("editConversationTitle", { id: conv.id, title: newTitle });
69
+ }}
70
+ >
71
+ <CarbonEdit class="text-xs text-gray-400 hover:text-gray-500 dark:hover:text-gray-300" />
72
+ </button>
73
+
74
+ <button
75
+ type="button"
76
+ class="flex h-5 w-5 items-center justify-center rounded md:hidden md:group-hover:flex"
77
+ title="Delete conversation"
78
+ on:click|preventDefault={(event) => {
79
+ if (event.shiftKey) {
80
+ dispatch("deleteConversation", conv.id);
81
+ } else {
82
+ confirmDelete = true;
83
+ }
84
+ }}
85
+ >
86
+ <CarbonTrashCan class="text-xs text-gray-400 hover:text-gray-500 dark:hover:text-gray-300" />
87
+ </button>
88
+ {/if}
89
+ </a>
src/lib/components/NavMenu.svelte ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { base } from "$app/paths";
3
+ import { createEventDispatcher } from "svelte";
4
+
5
+ import Logo from "$lib/components/icons/Logo.svelte";
6
+ import { switchTheme } from "$lib/switchTheme";
7
+ import { PUBLIC_APP_NAME, PUBLIC_ORIGIN } from "$env/static/public";
8
+ import NavConversationItem from "./NavConversationItem.svelte";
9
+ import type { LayoutData } from "../../routes/$types";
10
+
11
+ const dispatch = createEventDispatcher<{
12
+ shareConversation: { id: string; title: string };
13
+ clickSettings: void;
14
+ clickLogout: void;
15
+ }>();
16
+
17
+ export let conversations: Array<{
18
+ id: string;
19
+ title: string;
20
+ }> = [];
21
+
22
+ export let canLogin: boolean;
23
+ export let user: LayoutData["user"];
24
+
25
+ export let loginModalVisible;
26
+ </script>
27
+
28
+ <div class="sticky top-0 flex flex-none items-center justify-between px-3 py-3.5 max-sm:pt-0">
29
+ <a class="flex items-center rounded-xl text-lg font-semibold" href="{PUBLIC_ORIGIN}{base}/">
30
+ <Logo classNames="mr-1" />
31
+ {PUBLIC_APP_NAME}
32
+ </a>
33
+ <a
34
+ href={`${base}/`}
35
+ class="flex rounded-lg border bg-white px-2 py-0.5 text-center shadow-sm hover:shadow-none dark:border-gray-600 dark:bg-gray-700"
36
+ >
37
+ New Chat
38
+ </a>
39
+ </div>
40
+ <div
41
+ class="scrollbar-custom flex flex-col gap-1 overflow-y-auto rounded-r-xl bg-gradient-to-l from-gray-50 px-3 pb-3 pt-2 dark:from-gray-800/30"
42
+ >
43
+ {#each conversations as conv (conv.id)}
44
+ <NavConversationItem on:editConversationTitle on:deleteConversation {conv} />
45
+ {/each}
46
+ </div>
47
+ <div
48
+ class="mt-0.5 flex flex-col gap-1 rounded-r-xl bg-gradient-to-l from-gray-50 p-3 text-sm dark:from-gray-800/30"
49
+ >
50
+ {#if user?.username || user?.email}
51
+ <form
52
+ action="{base}/logout"
53
+ method="post"
54
+ class="group flex items-center gap-1.5 rounded-lg pl-3 pr-2 hover:bg-gray-100 dark:hover:bg-gray-700"
55
+ >
56
+ <span
57
+ class="flex h-9 flex-none shrink items-center gap-1.5 truncate pr-2 text-gray-500 dark:text-gray-400"
58
+ >{user?.username || user?.email}</span
59
+ >
60
+ <button
61
+ type="submit"
62
+ class="ml-auto h-6 flex-none items-center gap-1.5 rounded-md border bg-white px-2 text-gray-700 shadow-sm group-hover:flex hover:shadow-none dark:border-gray-600 dark:bg-gray-600 dark:text-gray-400 dark:hover:text-gray-300 md:hidden"
63
+ >
64
+ Sign Out
65
+ </button>
66
+ </form>
67
+ {/if}
68
+ {#if canLogin}
69
+ <button
70
+ on:click={() => (loginModalVisible = true)}
71
+ type="button"
72
+ class="flex h-9 flex-none items-center gap-1.5 rounded-lg pl-3 pr-2 text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700"
73
+ >
74
+ Login
75
+ </button>
76
+ {/if}
77
+ <button
78
+ on:click={switchTheme}
79
+ type="button"
80
+ class="flex h-9 flex-none items-center gap-1.5 rounded-lg pl-3 pr-2 text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700"
81
+ >
82
+ Theme
83
+ </button>
84
+ <button
85
+ on:click={() => dispatch("clickSettings")}
86
+ type="button"
87
+ class="flex h-9 flex-none items-center gap-1.5 rounded-lg pl-3 pr-2 text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700"
88
+ >
89
+ Settings
90
+ </button>
91
+ <a
92
+ href="https://github.com/mithril-security/blind_chat/issues"
93
+ target="_blank"
94
+ rel="noreferrer"
95
+ class="flex h-9 flex-none items-center gap-1.5 rounded-lg pl-3 pr-2 text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700"
96
+ >
97
+ Feedback
98
+ </a>
99
+ <a
100
+ href="{base}/privacy"
101
+ class="flex h-9 flex-none items-center gap-1.5 rounded-lg pl-3 pr-2 text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700"
102
+ >
103
+ About & Privacy
104
+ </a>
105
+ </div>
src/lib/components/OpenWebSearchResults.svelte ADDED
@@ -0,0 +1,114 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import type { WebSearchMessage } from "$lib/types/WebSearch";
3
+ import CarbonCaretRight from "~icons/carbon/caret-right";
4
+
5
+ import CarbonCheckmark from "~icons/carbon/checkmark-filled";
6
+ import CarbonError from "~icons/carbon/error-filled";
7
+
8
+ import EosIconsLoading from "~icons/eos-icons/loading";
9
+
10
+ export let loading = false;
11
+ export let classNames = "";
12
+ export let webSearchMessages: WebSearchMessage[] = [];
13
+
14
+ let detailsOpen: boolean;
15
+ let error: boolean;
16
+ $: error = webSearchMessages[webSearchMessages.length - 2]?.type === "error";
17
+ </script>
18
+
19
+ <details
20
+ class="flex w-fit rounded-xl border border-gray-200 bg-white shadow-sm dark:border-gray-800 dark:bg-gray-900 {classNames} max-w-full"
21
+ bind:open={detailsOpen}
22
+ >
23
+ <summary
24
+ class="align-center flex cursor-pointer select-none list-none py-1 pl-2.5 pr-2 align-text-top transition-all"
25
+ >
26
+ {#if error}
27
+ <CarbonError class="my-auto text-red-700 dark:text-red-500" />
28
+ {:else if loading}
29
+ <EosIconsLoading class="my-auto text-gray-500" />
30
+ {:else}
31
+ <CarbonCheckmark class="my-auto text-gray-500" />
32
+ {/if}
33
+ <span class="px-2 font-medium" class:text-red-700={error} class:dark:text-red-500={error}
34
+ >Web search
35
+ </span>
36
+ <div class="my-auto transition-all" class:rotate-90={detailsOpen}>
37
+ <CarbonCaretRight />
38
+ </div>
39
+ </summary>
40
+
41
+ <div class="content px-5 pb-5 pt-4">
42
+ {#if webSearchMessages.length === 0}
43
+ <div class="mx-auto w-fit">
44
+ <EosIconsLoading class="mb-3 h-4 w-4" />
45
+ </div>
46
+ {:else}
47
+ <ol>
48
+ {#each webSearchMessages as message}
49
+ {#if message.type === "update"}
50
+ <li class="group border-l pb-6 last:!border-transparent last:pb-0 dark:border-gray-800">
51
+ <div class="flex items-start">
52
+ <div
53
+ class="-ml-1.5 h-3 w-3 flex-none rounded-full bg-gray-200 dark:bg-gray-600 {loading
54
+ ? 'group-last:animate-pulse group-last:bg-gray-300 group-last:dark:bg-gray-500'
55
+ : ''}"
56
+ />
57
+ <h3 class="text-md -mt-1.5 pl-2.5 text-gray-800 dark:text-gray-100">
58
+ {message.message}
59
+ </h3>
60
+ </div>
61
+ {#if message.args}
62
+ <p class="mt-1.5 pl-4 text-gray-500 dark:text-gray-400">
63
+ {message.args}
64
+ </p>
65
+ {/if}
66
+ </li>
67
+ {:else if message.type === "error"}
68
+ <li class="group border-l pb-6 last:!border-transparent last:pb-0 dark:border-gray-800">
69
+ <div class="flex items-start">
70
+ <CarbonError
71
+ class="-ml-1.5 h-3 w-3 flex-none scale-110 text-red-700 dark:text-red-500"
72
+ />
73
+ <h3 class="text-md -mt-1.5 pl-2.5 text-red-700 dark:text-red-500">
74
+ {message.message}
75
+ </h3>
76
+ </div>
77
+ {#if message.args}
78
+ <p class="mt-1.5 pl-4 text-gray-500 dark:text-gray-400">
79
+ {message.args}
80
+ </p>
81
+ {/if}
82
+ </li>
83
+ {/if}
84
+ {/each}
85
+ </ol>
86
+ {/if}
87
+ </div>
88
+ </details>
89
+
90
+ <style>
91
+ @keyframes grow {
92
+ 0% {
93
+ font-size: 0;
94
+ opacity: 0;
95
+ }
96
+ 30% {
97
+ font-size: 1em;
98
+ opacity: 0;
99
+ }
100
+ 100% {
101
+ opacity: 1;
102
+ }
103
+ }
104
+
105
+ details[open] .content {
106
+ animation-name: grow;
107
+ animation-duration: 300ms;
108
+ animation-delay: 0ms;
109
+ }
110
+
111
+ details summary::-webkit-details-marker {
112
+ display: none;
113
+ }
114
+ </style>
src/lib/components/Portal.svelte ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { onMount, onDestroy } from "svelte";
3
+
4
+ let el: HTMLElement;
5
+
6
+ onMount(() => {
7
+ el.ownerDocument.body.appendChild(el);
8
+ });
9
+
10
+ onDestroy(() => {
11
+ if (el?.parentNode) {
12
+ el.parentNode.removeChild(el);
13
+ }
14
+ });
15
+ </script>
16
+
17
+ <div bind:this={el} class="contents" hidden>
18
+ <slot />
19
+ </div>
src/lib/components/ScrollToBottomBtn.svelte ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { fade } from "svelte/transition";
3
+ import { onDestroy } from "svelte";
4
+ import IconChevron from "./icons/IconChevron.svelte";
5
+
6
+ export let scrollNode: HTMLElement;
7
+ export { className as class };
8
+
9
+ let visible = false;
10
+ let className = "";
11
+ let observer: ResizeObserver | null = null;
12
+
13
+ $: if (scrollNode) {
14
+ destroy();
15
+
16
+ if (window.ResizeObserver) {
17
+ observer = new ResizeObserver(() => {
18
+ updateVisibility();
19
+ });
20
+ observer.observe(scrollNode);
21
+ }
22
+ scrollNode.addEventListener("scroll", updateVisibility);
23
+ }
24
+
25
+ function updateVisibility() {
26
+ if (!scrollNode) return;
27
+ visible =
28
+ Math.ceil(scrollNode.scrollTop) + 200 < scrollNode.scrollHeight - scrollNode.clientHeight;
29
+ }
30
+
31
+ function destroy() {
32
+ observer?.disconnect();
33
+ scrollNode?.removeEventListener("scroll", updateVisibility);
34
+ }
35
+
36
+ onDestroy(destroy);
37
+ </script>
38
+
39
+ {#if visible}
40
+ <button
41
+ transition:fade|local={{ duration: 150 }}
42
+ on:click={() => scrollNode.scrollTo({ top: scrollNode.scrollHeight, behavior: "smooth" })}
43
+ class="btn absolute flex h-[41px] w-[41px] rounded-full border bg-white shadow-md transition-all hover:bg-gray-100 dark:border-gray-600 dark:bg-gray-700 dark:shadow-gray-950 dark:hover:bg-gray-600 {className}"
44
+ ><IconChevron classNames="mt-[2px]" /></button
45
+ >
46
+ {/if}
src/lib/components/SettingsModal.svelte ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { createEventDispatcher } from "svelte";
3
+
4
+ import Modal from "$lib/components/Modal.svelte";
5
+ import CarbonClose from "~icons/carbon/close";
6
+ import Switch from "$lib/components/Switch.svelte";
7
+ import { enhance } from "$app/forms";
8
+ import { base } from "$app/paths";
9
+ import type { Model } from "$lib/types/Model";
10
+ import type { LayoutData } from "../../routes/$types";
11
+
12
+ export let settings: LayoutData["settings"];
13
+ export let models: Array<Model>;
14
+
15
+ let isConfirmingDeletion = false;
16
+
17
+ const dispatch = createEventDispatcher<{ close: void, deleteAllConversations: void }>();
18
+ </script>
19
+
20
+ <Modal on:close>
21
+ <div class="flex w-full flex-col gap-5 p-6">
22
+ <div class="flex items-start justify-between text-xl font-semibold text-gray-800">
23
+ <h2>Settings</h2>
24
+ <button type="button" class="group" on:click={() => dispatch("close")}>
25
+ <CarbonClose class="text-gray-900 group-hover:text-gray-500" />
26
+ </button>
27
+ </div>
28
+ <form
29
+ class="flex flex-col gap-5"
30
+ use:enhance={() => {
31
+ dispatch("close");
32
+ }}
33
+ method="post"
34
+ action="{base}/settings"
35
+ >
36
+ <form
37
+ on:submit|preventDefault={() => (isConfirmingDeletion = true)}
38
+ >
39
+ <button type="submit" class="underline decoration-gray-300 hover:decoration-gray-700">
40
+ Delete all conversations
41
+ </button>
42
+ </form>
43
+ <button
44
+ type="submit"
45
+ class="mt-2 rounded-full bg-black px-5 py-2 text-lg font-semibold text-gray-100 ring-gray-400 ring-offset-1 transition-all focus-visible:outline-none focus-visible:ring hover:ring"
46
+ >
47
+ Apply
48
+ </button>
49
+ </form>
50
+
51
+ {#if isConfirmingDeletion}
52
+ <Modal on:close={() => (isConfirmingDeletion = false)}>
53
+ <form
54
+ use:enhance={() => {
55
+ dispatch("close");
56
+ }}
57
+ method="post"
58
+ action="{base}/conversations?/delete"
59
+ class="flex w-full flex-col gap-5 p-6"
60
+ >
61
+ <div class="flex items-start justify-between text-xl font-semibold text-gray-800">
62
+ <h2>Are you sure?</h2>
63
+ <button type="button" class="group" on:click={() => (isConfirmingDeletion = false)}>
64
+ <CarbonClose class="text-gray-900 group-hover:text-gray-500" />
65
+ </button>
66
+ </div>
67
+ <p class="text-gray-800">
68
+ This action will delete all your conversations. This cannot be undone.
69
+ </p>
70
+ <button
71
+ type="submit"
72
+ class="mt-2 rounded-full bg-red-700 px-5 py-2 text-lg font-semibold text-gray-100 ring-gray-400 ring-offset-1 transition-all focus-visible:outline-none focus-visible:ring hover:ring"
73
+ >
74
+ Confirm deletion
75
+ </button>
76
+ </form>
77
+ </Modal>
78
+ {/if}
79
+ </div>
80
+ </Modal>
src/lib/components/StopGeneratingBtn.svelte ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import CarbonStopFilledAlt from "~icons/carbon/stop-filled-alt";
3
+
4
+ export let classNames = "";
5
+ </script>
6
+
7
+ <button
8
+ type="button"
9
+ on:click
10
+ class="btn flex h-9 rounded-lg border bg-white px-3 py-1 shadow-sm transition-all hover:bg-gray-100 dark:border-gray-600 dark:bg-gray-700 dark:hover:bg-gray-600 {classNames}"
11
+ >
12
+ <CarbonStopFilledAlt class="-ml-1 mr-1 h-[1.25rem] w-[1.1875rem] text-gray-400" /> Stop generating
13
+ </button>
src/lib/components/Switch.svelte ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ export let checked: boolean;
3
+ export let name: string;
4
+ </script>
5
+
6
+ <input bind:checked type="checkbox" {name} class="peer pointer-events-none absolute opacity-0" />
7
+ <div
8
+ on:click
9
+ on:keypress
10
+ class="relative inline-flex h-5 w-9 shrink-0 items-center rounded-full bg-gray-300 p-1 shadow-inner ring-gray-400 transition-all peer-checked:bg-blue-600 peer-focus-visible:ring peer-focus-visible:ring-offset-1 hover:bg-gray-400 dark:bg-gray-600 peer-checked:[&>div]:translate-x-3.5"
11
+ >
12
+ <div class="h-3.5 w-3.5 rounded-full bg-white shadow-sm transition-all" />
13
+ </div>
src/lib/components/Toast.svelte ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { fade } from "svelte/transition";
3
+
4
+ import IconDazzled from "$lib/components/icons/IconDazzled.svelte";
5
+
6
+ export let message = "";
7
+ </script>
8
+
9
+ <div
10
+ transition:fade={{ duration: 300 }}
11
+ class="pointer-events-none fixed right-0 top-12 z-20 bg-gradient-to-bl from-red-500/20 via-red-500/0 to-red-500/0 pb-36 pl-36 pr-2 pt-2 md:top-0 md:pr-8 md:pt-5"
12
+ >
13
+ <div
14
+ class="pointer-events-auto flex items-center rounded-full bg-white/90 px-3 py-1 shadow-sm dark:bg-gray-900/80"
15
+ >
16
+ <IconDazzled classNames="text-2xl mr-2" />
17
+ <h2 class="font-semibold">{message}</h2>
18
+ </div>
19
+ </div>
src/lib/components/Tooltip.svelte ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ export let classNames = "";
3
+ export let label = "Copied";
4
+ export let position = "left-1/2 top-full transform -translate-x-1/2 translate-y-2";
5
+ </script>
6
+
7
+ <div
8
+ class="
9
+ pointer-events-none absolute rounded bg-black px-2 py-1 font-normal leading-tight text-white shadow transition-opacity
10
+ {position}
11
+ {classNames}
12
+ "
13
+ >
14
+ <div
15
+ class="absolute bottom-full left-1/2 h-0 w-0 -translate-x-1/2 transform border-4 border-t-0 border-black"
16
+ style="
17
+ border-left-color: transparent;
18
+ border-right-color: transparent;
19
+ "
20
+ />
21
+ {label}
22
+ </div>
src/lib/components/WebSearchToggle.svelte ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { webSearchParameters } from "$lib/stores/webSearchParameters";
3
+ import CarbonInformation from "~icons/carbon/information";
4
+ import Switch from "./Switch.svelte";
5
+
6
+ const toggle = () => ($webSearchParameters.useSearch = !$webSearchParameters.useSearch);
7
+ </script>
8
+
9
+ <div
10
+ class="flex h-9 cursor-pointer select-none items-center gap-2 rounded-xl border bg-white p-1.5 shadow-sm hover:shadow-none dark:border-gray-800 dark:bg-gray-900"
11
+ on:click={toggle}
12
+ on:keypress={toggle}
13
+ >
14
+ <Switch name="useSearch" bind:checked={$webSearchParameters.useSearch} on:click on:keypress />
15
+ <div class="whitespace-nowrap text-sm text-gray-800 dark:text-gray-200">Search web</div>
16
+ <div class="group relative w-max">
17
+ <CarbonInformation class="text-xs text-gray-500" />
18
+ <div
19
+ class="pointer-events-none absolute -top-20 left-1/2 w-max -translate-x-1/2 rounded-md bg-gray-100 p-2 opacity-0 transition-opacity group-hover:opacity-100 dark:bg-gray-800"
20
+ >
21
+ <p class="max-w-sm text-sm text-gray-800 dark:text-gray-200">
22
+ When enabled, the model will try to complement its answer with information queried from the
23
+ web.
24
+ </p>
25
+ </div>
26
+ </div>
27
+ </div>
src/lib/components/chat/ChatInput.svelte ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { createEventDispatcher, onMount } from "svelte";
3
+
4
+ export let value = "";
5
+ export let minRows = 1;
6
+ export let maxRows: null | number = null;
7
+ export let placeholder = "";
8
+ export let disabled = false;
9
+
10
+ // Approximate width from which we disable autofocus
11
+ const TABLET_VIEWPORT_WIDTH = 768;
12
+
13
+ let innerWidth = 0;
14
+ let textareaElement: HTMLTextAreaElement;
15
+
16
+ const dispatch = createEventDispatcher<{ submit: void }>();
17
+
18
+ $: minHeight = `${1 + minRows * 1.5}em`;
19
+ $: maxHeight = maxRows ? `${1 + maxRows * 1.5}em` : `auto`;
20
+
21
+ function handleKeydown(event: KeyboardEvent) {
22
+ // submit on enter
23
+ if (event.key === "Enter" && !event.shiftKey) {
24
+ event.preventDefault();
25
+ dispatch("submit"); // use a custom event instead of `event.target.form.requestSubmit()` as it does not work on Safari 14
26
+ }
27
+ }
28
+
29
+ onMount(() => {
30
+ if (innerWidth > TABLET_VIEWPORT_WIDTH) {
31
+ textareaElement.focus();
32
+ }
33
+ });
34
+ </script>
35
+
36
+ <svelte:window bind:innerWidth />
37
+
38
+ <div class="relative min-w-0 flex-1">
39
+ <pre
40
+ class="scrollbar-custom invisible overflow-x-hidden overflow-y-scroll whitespace-pre-wrap break-words p-3"
41
+ aria-hidden="true"
42
+ style="min-height: {minHeight}; max-height: {maxHeight}">{(value || " ") + "\n"}</pre>
43
+
44
+ <textarea
45
+ enterkeyhint="send"
46
+ tabindex="0"
47
+ rows="1"
48
+ class="scrollbar-custom absolute top-0 m-0 h-full w-full resize-none scroll-p-3 overflow-x-hidden overflow-y-scroll border-0 bg-transparent p-3 outline-none focus:ring-0 focus-visible:ring-0"
49
+ bind:value
50
+ bind:this={textareaElement}
51
+ {disabled}
52
+ on:keydown={handleKeydown}
53
+ on:keypress
54
+ {placeholder}
55
+ />
56
+ </div>
57
+
58
+ <style>
59
+ pre,
60
+ textarea {
61
+ font-family: inherit;
62
+ box-sizing: border-box;
63
+ line-height: 1.5;
64
+ }
65
+ </style>
src/lib/components/chat/ChatIntroduction.svelte ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { PUBLIC_APP_NAME, PUBLIC_VERSION } from "$env/static/public";
3
+ import { PUBLIC_ANNOUNCEMENT_BANNERS } from "$env/static/public";
4
+ import Logo from "$lib/components/icons/Logo.svelte";
5
+ import { createEventDispatcher } from "svelte";
6
+ import IconChevron from "$lib/components/icons/IconChevron.svelte";
7
+ import CarbonArrowUpRight from "~icons/carbon/arrow-up-right";
8
+ import AnnouncementBanner from "../AnnouncementBanner.svelte";
9
+ import ModelsModal from "../ModelsModal.svelte";
10
+ import type { Model } from "$lib/types/Model";
11
+ import ModelCardMetadata from "../ModelCardMetadata.svelte";
12
+ import type { LayoutData } from "../../../routes/$types";
13
+ import { findCurrentModel } from "$lib/utils/models";
14
+ import { env } from "$env/dynamic/public";
15
+
16
+ export let currentModel: Model;
17
+ export let settings: LayoutData["settings"];
18
+ export let models: Model[];
19
+
20
+ let isModelsModalOpen = false;
21
+
22
+ $: currentModelMetadata = findCurrentModel(models, settings.activeModel);
23
+
24
+ const announcementBanners = PUBLIC_ANNOUNCEMENT_BANNERS
25
+ ? JSON.parse(PUBLIC_ANNOUNCEMENT_BANNERS)
26
+ : [];
27
+
28
+ const dispatch = createEventDispatcher<{ message: string }>();
29
+
30
+ $: title = env.PUBLIC_APP_NAME
31
+ </script>
32
+
33
+ <div class="my-auto grid gap-8 lg:grid-cols-3">
34
+ <div class="lg:col-span-1">
35
+ <div>
36
+ <div class="mb-3 flex items-center text-2xl font-semibold">
37
+ <Logo classNames="mr-1 flex-none" />
38
+ {PUBLIC_APP_NAME}
39
+ <div
40
+ class="ml-3 flex h-6 items-center rounded-lg border border-gray-100 bg-gray-50 px-2 text-base text-gray-400 dark:border-gray-700/60 dark:bg-gray-800"
41
+ >
42
+ v{PUBLIC_VERSION}
43
+ </div>
44
+ </div>
45
+ <p class="text-base text-gray-600 dark:text-gray-400">
46
+ Enjoying the best AI models, with privacy
47
+ </p>
48
+ </div>
49
+ </div>
50
+ <div class="lg:col-span-2 lg:pl-24">
51
+ {#if isModelsModalOpen}
52
+ <ModelsModal {settings} {models} on:close={() => (isModelsModalOpen = false)} />
53
+ {/if}
54
+ <div class="overflow-hidden rounded-xl border dark:border-gray-800">
55
+ <div class="flex p-3">
56
+ <div>
57
+ <div class="text-sm text-gray-600 dark:text-gray-400">Current Model</div>
58
+ <div class="font-semibold">{currentModel.displayName}</div>
59
+ </div>
60
+ {#if models.length > 1}
61
+ <button
62
+ type="button"
63
+ on:click={() => (isModelsModalOpen = true)}
64
+ class="btn ml-auto flex h-7 w-7 self-start rounded-full bg-gray-100 p-1 text-xs hover:bg-gray-100 dark:border-gray-600 dark:bg-gray-800 dark:hover:bg-gray-600"
65
+ ><IconChevron /></button
66
+ >
67
+ {/if}
68
+ </div>
69
+ <ModelCardMetadata variant="dark" model={currentModel} />
70
+ </div>
71
+ </div>
72
+ {#if currentModelMetadata.promptExamples}
73
+ <div class="lg:col-span-3 lg:mt-6">
74
+ <div class="grid gap-3 lg:grid-cols-3 lg:gap-5">
75
+ {#each currentModelMetadata.promptExamples as example}
76
+ <button
77
+ type="button"
78
+ class="rounded-xl border bg-gray-50 p-2.5 text-gray-600 hover:bg-gray-100 dark:border-gray-800 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700 sm:p-4"
79
+ on:click={() => dispatch("message", example.prompt)}
80
+ >
81
+ {example.title}
82
+ </button>
83
+ {/each}
84
+ </div>
85
+ </div>{/if}
86
+ </div>
src/lib/components/chat/ChatMessage.svelte ADDED
@@ -0,0 +1,212 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { marked } from "marked";
3
+ import type { Message } from "$lib/types/Message";
4
+ import { afterUpdate, createEventDispatcher } from "svelte";
5
+ import { deepestChild } from "$lib/utils/deepestChild";
6
+ import { page } from "$app/stores";
7
+
8
+ import CodeBlock from "../CodeBlock.svelte";
9
+ import IconLoading from "../icons/IconLoading.svelte";
10
+ import CarbonRotate360 from "~icons/carbon/rotate-360";
11
+ import CarbonDownload from "~icons/carbon/download";
12
+ import CarbonThumbsUp from "~icons/carbon/thumbs-up";
13
+ import CarbonThumbsDown from "~icons/carbon/thumbs-down";
14
+ import { PUBLIC_SEP_TOKEN } from "$lib/constants/publicSepToken";
15
+ import type { Model } from "$lib/types/Model";
16
+ import type { WebSearchMessage } from "$lib/types/WebSearch";
17
+
18
+ import OpenWebSearchResults from "../OpenWebSearchResults.svelte";
19
+
20
+ function sanitizeMd(md: string) {
21
+ let ret = md
22
+ .replace(/<\|[a-z]*$/, "")
23
+ .replace(/<\|[a-z]+\|$/, "")
24
+ .replace(/<$/, "")
25
+ .replaceAll(PUBLIC_SEP_TOKEN, " ")
26
+ .replaceAll(/<\|[a-z]+\|>/g, " ")
27
+ .replaceAll(/<br\s?\/?>/gi, "\n")
28
+ .replaceAll("<", "&lt;")
29
+ .trim();
30
+
31
+ for (const stop of [...(model.parameters?.stop ?? []), "<|endoftext|>"]) {
32
+ if (ret.endsWith(stop)) {
33
+ ret = ret.slice(0, -stop.length).trim();
34
+ }
35
+ }
36
+
37
+ return ret;
38
+ }
39
+ function unsanitizeMd(md: string) {
40
+ return md.replaceAll("&lt;", "<");
41
+ }
42
+
43
+ export let model: Model;
44
+ export let message: Message;
45
+ export let loading = false;
46
+ export let isAuthor = true;
47
+ export let readOnly = false;
48
+ export let isTapped = false;
49
+
50
+ export let webSearchMessages: WebSearchMessage[] = [];
51
+
52
+ const dispatch = createEventDispatcher<{
53
+ retry: { content: string; id: Message["id"] };
54
+ vote: { score: Message["score"]; id: Message["id"] };
55
+ }>();
56
+
57
+ let contentEl: HTMLElement;
58
+ let loadingEl: IconLoading;
59
+ let pendingTimeout: ReturnType<typeof setTimeout>;
60
+
61
+ const renderer = new marked.Renderer();
62
+
63
+ // For code blocks with simple backticks
64
+ renderer.codespan = (code) => {
65
+ // Unsanitize double-sanitized code
66
+ return `<code>${code.replaceAll("&amp;", "&")}</code>`;
67
+ };
68
+
69
+ const options: marked.MarkedOptions = {
70
+ ...marked.getDefaults(),
71
+ gfm: true,
72
+ breaks: true,
73
+ renderer,
74
+ };
75
+
76
+ $: tokens = marked.lexer(sanitizeMd(message.content));
77
+
78
+ afterUpdate(() => {
79
+ loadingEl?.$destroy();
80
+ clearTimeout(pendingTimeout);
81
+
82
+ // Add loading animation to the last message if update takes more than 600ms
83
+ if (loading) {
84
+ pendingTimeout = setTimeout(() => {
85
+ if (contentEl) {
86
+ loadingEl = new IconLoading({
87
+ target: deepestChild(contentEl),
88
+ props: { classNames: "loading inline ml-2" },
89
+ });
90
+ }
91
+ }, 600);
92
+ }
93
+ });
94
+
95
+ $: downloadLink =
96
+ message.from === "user" ? `${$page.url.pathname}/message/${message.id}/prompt` : undefined;
97
+
98
+ let webSearchIsDone = true;
99
+
100
+ $: webSearchIsDone =
101
+ webSearchMessages.length > 0 &&
102
+ webSearchMessages[webSearchMessages.length - 1].type === "result";
103
+ </script>
104
+
105
+ {#if message.from === "assistant"}
106
+ <div
107
+ class="group relative -mb-8 flex items-start justify-start gap-4 pb-8 leading-relaxed"
108
+ on:click={() => (isTapped = !isTapped)}
109
+ on:keypress={() => (isTapped = !isTapped)}
110
+ >
111
+ <img
112
+ alt=""
113
+ src="https://huggingface.co/avatars/2edb18bd0206c16b433841a47f53fa8e.svg"
114
+ class="mt-5 h-3 w-3 flex-none select-none rounded-full shadow-lg"
115
+ />
116
+ <div
117
+ class="relative min-h-[calc(2rem+theme(spacing[3.5])*2)] min-w-[60px] break-words rounded-2xl border border-gray-100 bg-gradient-to-br from-gray-50 px-5 py-3.5 text-gray-600 prose-pre:my-2 dark:border-gray-800 dark:from-gray-800/40 dark:text-gray-300"
118
+ >
119
+ {#if webSearchMessages && webSearchMessages.length > 0}
120
+ <OpenWebSearchResults
121
+ classNames={tokens.length ? "mb-3.5" : ""}
122
+ {webSearchMessages}
123
+ loading={!webSearchIsDone}
124
+ />
125
+ {/if}
126
+ {#if !message.content && (webSearchIsDone || (webSearchMessages && webSearchMessages.length === 0))}
127
+ <IconLoading />
128
+ {/if}
129
+
130
+ <div
131
+ class="prose max-w-none dark:prose-invert max-sm:prose-sm prose-headings:font-semibold prose-h1:text-lg prose-h2:text-base prose-h3:text-base prose-pre:bg-gray-800 dark:prose-pre:bg-gray-900"
132
+ bind:this={contentEl}
133
+ >
134
+ {#each tokens as token}
135
+ {#if token.type === "code"}
136
+ <CodeBlock lang={token.lang} code={unsanitizeMd(token.text)} />
137
+ {:else}
138
+ <!-- eslint-disable-next-line svelte/no-at-html-tags -->
139
+ {@html marked(token.raw, options)}
140
+ {/if}
141
+ {/each}
142
+ </div>
143
+ </div>
144
+ <!-- {#if isAuthor && !loading && message.content}
145
+ <div
146
+ class="absolute bottom-1 right-0 flex max-md:transition-all md:bottom-0 md:group-hover:visible md:group-hover:opacity-100
147
+ {message.score ? 'visible opacity-100' : 'invisible max-md:-translate-y-4 max-md:opacity-0'}
148
+ {isTapped ? 'max-md:visible max-md:translate-y-0 max-md:opacity-100' : ''}
149
+ "
150
+ >
151
+ <button
152
+ class="btn rounded-sm p-1 text-sm text-gray-400 focus:ring-0 hover:text-gray-500 dark:text-gray-400 dark:hover:text-gray-300
153
+ {message.score && message.score > 0
154
+ ? 'text-green-500 hover:text-green-500 dark:text-green-400 hover:dark:text-green-400'
155
+ : ''}"
156
+ title={message.score === 1 ? "Remove +1" : "+1"}
157
+ type="button"
158
+ on:click={() => dispatch("vote", { score: message.score === 1 ? 0 : 1, id: message.id })}
159
+ >
160
+ <CarbonThumbsUp class="h-[1.14em] w-[1.14em]" />
161
+ </button>
162
+ <button
163
+ class="btn rounded-sm p-1 text-sm text-gray-400 focus:ring-0 hover:text-gray-500 dark:text-gray-400 dark:hover:text-gray-300
164
+ {message.score && message.score < 0
165
+ ? 'text-red-500 hover:text-red-500 dark:text-red-400 hover:dark:text-red-400'
166
+ : ''}"
167
+ title={message.score === -1 ? "Remove -1" : "-1"}
168
+ type="button"
169
+ on:click={() =>
170
+ dispatch("vote", { score: message.score === -1 ? 0 : -1, id: message.id })}
171
+ >
172
+ <CarbonThumbsDown class="h-[1.14em] w-[1.14em]" />
173
+ </button>
174
+ </div>
175
+ {/if} -->
176
+ </div>
177
+ {/if}
178
+ {#if message.from === "user"}
179
+ <div class="group relative flex items-start justify-start gap-4 max-sm:text-sm">
180
+ <div class="mt-5 h-3 w-3 flex-none rounded-full" />
181
+ <div
182
+ class="max-w-full whitespace-break-spaces break-words rounded-2xl px-5 py-3.5 text-gray-500 dark:text-gray-400"
183
+ >
184
+ {message.content.trim()}
185
+ </div>
186
+ {#if !loading}
187
+ <div class="absolute right-0 top-3.5 flex gap-2 lg:-right-2">
188
+ <!-- {#if downloadLink}
189
+ <a
190
+ class="rounded-lg border border-gray-100 p-1 text-xs text-gray-400 group-hover:block hover:text-gray-500 dark:border-gray-800 dark:text-gray-400 dark:hover:text-gray-300 md:hidden"
191
+ title="Download prompt and parameters"
192
+ type="button"
193
+ target="_blank"
194
+ href={downloadLink}
195
+ >
196
+ <CarbonDownload />
197
+ </a>
198
+ {/if} -->
199
+ <!-- {#if !readOnly}
200
+ <button
201
+ class="cursor-pointer rounded-lg border border-gray-100 p-1 text-xs text-gray-400 group-hover:block hover:text-gray-500 dark:border-gray-800 dark:text-gray-400 dark:hover:text-gray-300 md:hidden lg:-right-2"
202
+ title="Retry"
203
+ type="button"
204
+ on:click={() => dispatch("retry", { content: message.content, id: message.id })}
205
+ >
206
+ <CarbonRotate360 />
207
+ </button>
208
+ {/if} -->
209
+ </div>
210
+ {/if}
211
+ </div>
212
+ {/if}
src/lib/components/chat/ChatMessages.svelte ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import type { Message } from "$lib/types/Message";
3
+ import { snapScrollToBottom } from "$lib/actions/snapScrollToBottom";
4
+ import ScrollToBottomBtn from "$lib/components/ScrollToBottomBtn.svelte";
5
+ import { tick } from "svelte";
6
+ import { randomUUID } from "$lib/utils/randomUuid";
7
+ import type { Model } from "$lib/types/Model";
8
+ import type { LayoutData } from "../../../routes/$types";
9
+ import ChatIntroduction from "./ChatIntroduction.svelte";
10
+ import ChatMessage from "./ChatMessage.svelte";
11
+ import type { WebSearchMessage } from "$lib/types/WebSearch";
12
+
13
+ export let messages: Message[];
14
+ export let loading: boolean;
15
+ export let pending: boolean;
16
+ export let isAuthor: boolean;
17
+ export let currentModel: Model;
18
+ export let settings: LayoutData["settings"];
19
+ export let models: Model[];
20
+ export let readOnly: boolean;
21
+ export let searches: Record<string, WebSearchMessage[]>;
22
+
23
+ let webSearchArray: Array<WebSearchMessage[] | undefined> = [];
24
+ let chatContainer: HTMLElement;
25
+
26
+ export let webSearchMessages: WebSearchMessage[] = [];
27
+
28
+ async function scrollToBottom() {
29
+ await tick();
30
+ chatContainer.scrollTop = chatContainer.scrollHeight;
31
+ }
32
+
33
+ // If last message is from user, scroll to bottom
34
+ $: if (messages[messages.length - 1]?.from === "user") {
35
+ scrollToBottom();
36
+ }
37
+
38
+ $: messages,
39
+ (webSearchArray = messages.map((message, idx) => {
40
+ if (message.webSearchId) {
41
+ return searches[message.webSearchId] ?? [];
42
+ } else if (idx === messages.length - 1) {
43
+ return webSearchMessages;
44
+ } else {
45
+ return [];
46
+ }
47
+ }));
48
+ </script>
49
+
50
+ <div
51
+ class="scrollbar-custom mr-1 h-full overflow-y-auto"
52
+ use:snapScrollToBottom={messages.length ? [...messages, ...webSearchMessages] : false}
53
+ bind:this={chatContainer}
54
+ >
55
+ <div class="mx-auto flex h-full max-w-3xl flex-col gap-6 px-5 pt-6 sm:gap-8 xl:max-w-4xl">
56
+ {#each messages as message, i}
57
+ <ChatMessage
58
+ loading={loading && i === messages.length - 1}
59
+ {message}
60
+ {isAuthor}
61
+ {readOnly}
62
+ model={currentModel}
63
+ webSearchMessages={webSearchArray[i]}
64
+ on:retry
65
+ on:vote
66
+ />
67
+ {:else}
68
+ <ChatIntroduction {settings} {models} {currentModel} on:message />
69
+ {/each}
70
+ {#if pending}
71
+ <ChatMessage
72
+ message={{ from: "assistant", content: "", id: randomUUID() }}
73
+ model={currentModel}
74
+ {webSearchMessages}
75
+ />
76
+ {/if}
77
+ <div class="h-44 flex-none" />
78
+ </div>
79
+ <ScrollToBottomBtn
80
+ class="bottom-36 right-4 max-md:hidden lg:right-10"
81
+ scrollNode={chatContainer}
82
+ />
83
+ </div>
src/lib/components/chat/ChatWindow.svelte ADDED
@@ -0,0 +1,147 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import type { Message } from "$lib/types/Message";
3
+ import { createEventDispatcher } from "svelte";
4
+
5
+ import CarbonSendAltFilled from "~icons/carbon/send-alt-filled";
6
+ import CarbonExport from "~icons/carbon/export";
7
+ import CarbonStopFilledAlt from "~icons/carbon/stop-filled-alt";
8
+ import EosIconsLoading from "~icons/eos-icons/loading";
9
+
10
+ import ChatMessages from "./ChatMessages.svelte";
11
+ import ChatInput from "./ChatInput.svelte";
12
+ import StopGeneratingBtn from "../StopGeneratingBtn.svelte";
13
+ import type { Model } from "$lib/types/Model";
14
+ import type { LayoutData } from "../../../routes/$types";
15
+ import WebSearchToggle from "../WebSearchToggle.svelte";
16
+ import type { WebSearchMessage } from "$lib/types/WebSearch";
17
+ import LoginModal from "../LoginModal.svelte";
18
+
19
+ export let messages: Message[] = [];
20
+ export let loading = false;
21
+ export let pending = false;
22
+ export let shared = false;
23
+ export let currentModel: Model;
24
+ export let models: Model[];
25
+ export let settings: LayoutData["settings"];
26
+ export let webSearchMessages: WebSearchMessage[] = [];
27
+ export let searches: Record<string, WebSearchMessage[]> = {};
28
+
29
+ export let loginRequired = false;
30
+ $: isReadOnly = !models.some((model) => model.id === currentModel.id);
31
+
32
+ let loginModalOpen = false;
33
+ let message: string;
34
+
35
+ const dispatch = createEventDispatcher<{
36
+ message: string;
37
+ share: void;
38
+ stop: void;
39
+ retry: { id: Message["id"]; content: string };
40
+ }>();
41
+
42
+ const handleSubmit = () => {
43
+ if (loading) return;
44
+ dispatch("message", message);
45
+ message = "";
46
+ };
47
+ </script>
48
+
49
+ <div class="relative min-h-0 min-w-0">
50
+ {#if loginModalOpen}
51
+ <LoginModal {settings} on:close={() => (loginModalOpen = false)} />
52
+ {/if}
53
+ <ChatMessages
54
+ {loading}
55
+ {pending}
56
+ {settings}
57
+ {currentModel}
58
+ {models}
59
+ {messages}
60
+ readOnly={isReadOnly}
61
+ isAuthor={!shared}
62
+ {webSearchMessages}
63
+ {searches}
64
+ on:message
65
+ on:vote
66
+ on:retry={(ev) => {
67
+ if (!loading) dispatch("retry", ev.detail);
68
+ }}
69
+ />
70
+ <div
71
+ class="dark:via-gray-80 pointer-events-none absolute inset-x-0 bottom-0 z-0 mx-auto flex w-full max-w-3xl flex-col items-center justify-center bg-gradient-to-t from-white via-white/80 to-white/0 px-3.5 py-4 dark:border-gray-800 dark:from-gray-900 dark:to-gray-900/0 max-md:border-t max-md:bg-white max-md:dark:bg-gray-900 sm:px-5 md:py-8 xl:max-w-4xl [&>*]:pointer-events-auto"
72
+ >
73
+ <div class="flex w-full pb-3 max-md:justify-between">
74
+ {#if settings?.searchEnabled}
75
+ <WebSearchToggle />
76
+ {/if}
77
+ {#if loading}
78
+ <StopGeneratingBtn
79
+ classNames={settings?.searchEnabled ? "md:-translate-x-1/2 md:mx-auto" : "mx-auto"}
80
+ on:click={() => dispatch("stop")}
81
+ />
82
+ {/if}
83
+ </div>
84
+ <form
85
+ on:submit|preventDefault={handleSubmit}
86
+ class="relative flex w-full max-w-4xl flex-1 items-center rounded-xl border bg-gray-100 focus-within:border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:focus-within:border-gray-500
87
+ {isReadOnly ? 'opacity-30' : ''}"
88
+ >
89
+ <div class="flex w-full flex-1 border-none bg-transparent">
90
+ <ChatInput
91
+ placeholder="Ask anything"
92
+ bind:value={message}
93
+ on:submit={handleSubmit}
94
+ on:keypress={() => {
95
+ if (loginRequired) loginModalOpen = true;
96
+ }}
97
+ maxRows={4}
98
+ disabled={isReadOnly}
99
+ />
100
+
101
+ {#if loading}
102
+ <button
103
+ class="btn mx-1 my-1 inline-block h-[2.4rem] self-end rounded-lg bg-transparent p-1 px-[0.7rem] text-gray-400 disabled:opacity-60 enabled:hover:text-gray-700 dark:disabled:opacity-40 enabled:dark:hover:text-gray-100 md:hidden"
104
+ on:click={() => dispatch("stop")}
105
+ >
106
+ <CarbonStopFilledAlt />
107
+ </button>
108
+ <div
109
+ class="mx-1 my-1 hidden h-[2.4rem] items-center p-1 px-[0.7rem] text-gray-400 disabled:opacity-60 enabled:hover:text-gray-700 dark:disabled:opacity-40 enabled:dark:hover:text-gray-100 md:flex"
110
+ >
111
+ <EosIconsLoading />
112
+ </div>
113
+ {:else}
114
+ <button
115
+ class="btn mx-1 my-1 h-[2.4rem] self-end rounded-lg bg-transparent p-1 px-[0.7rem] text-gray-400 disabled:opacity-60 enabled:hover:text-gray-700 dark:disabled:opacity-40 enabled:dark:hover:text-gray-100"
116
+ disabled={!message || isReadOnly}
117
+ type="submit"
118
+ >
119
+ <CarbonSendAltFilled />
120
+ </button>
121
+ {/if}
122
+ </div>
123
+ </form>
124
+ <div class="mt-2 flex justify-between self-stretch px-1 text-xs text-gray-400/90 max-sm:gap-2">
125
+ <p>
126
+ Model: <a
127
+ href={currentModel.modelUrl || "https://huggingface.co/" + currentModel.name}
128
+ target="_blank"
129
+ rel="noreferrer"
130
+ class="hover:underline">{currentModel.displayName}</a
131
+ > <span class="max-sm:hidden">·</span><br class="sm:hidden" /> Generated content may be inaccurate
132
+ or false.
133
+ <br><br> 🔒 All conversations are end-to-end protected
134
+ </p>
135
+ <!-- {#if messages.length}
136
+ <button
137
+ class="flex flex-none items-center hover:text-gray-400 hover:underline max-sm:rounded-lg max-sm:bg-gray-50 max-sm:px-2.5 dark:max-sm:bg-gray-800"
138
+ type="button"
139
+ on:click={() => dispatch("share")}
140
+ >
141
+ <CarbonExport class="text-[.6rem] sm:mr-1.5 sm:text-primary-500" />
142
+ <div class="max-sm:hidden">Share this conversation</div>
143
+ </button>
144
+ {/if} -->
145
+ </div>
146
+ </div>
147
+ </div>
src/lib/components/icons/IconChevron.svelte ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ export let classNames = "";
3
+ </script>
4
+
5
+ <svg
6
+ width="1em"
7
+ height="1em"
8
+ viewBox="0 0 15 6"
9
+ class={classNames}
10
+ fill="none"
11
+ xmlns="http://www.w3.org/2000/svg"
12
+ >
13
+ <path
14
+ d="M1.67236 1L7.67236 7L13.6724 1"
15
+ stroke="currentColor"
16
+ stroke-width="2"
17
+ stroke-linecap="round"
18
+ stroke-linejoin="round"
19
+ />
20
+ </svg>
src/lib/components/icons/IconCopy.svelte ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ export let classNames = "";
3
+ </script>
4
+
5
+ <svg
6
+ class={classNames}
7
+ xmlns="http://www.w3.org/2000/svg"
8
+ aria-hidden="true"
9
+ fill="currentColor"
10
+ focusable="false"
11
+ role="img"
12
+ width="1em"
13
+ height="1em"
14
+ preserveAspectRatio="xMidYMid meet"
15
+ viewBox="0 0 32 32"
16
+ >
17
+ <path
18
+ d="M28,10V28H10V10H28m0-2H10a2,2,0,0,0-2,2V28a2,2,0,0,0,2,2H28a2,2,0,0,0,2-2V10a2,2,0,0,0-2-2Z"
19
+ transform="translate(0)"
20
+ />
21
+ <path d="M4,18H2V4A2,2,0,0,1,4,2H18V4H4Z" transform="translate(0)" /><rect
22
+ fill="none"
23
+ width="32"
24
+ height="32"
25
+ />
26
+ </svg>
src/lib/components/icons/IconDazzled.svelte ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ export let classNames = "";
3
+ </script>
4
+
5
+ <svg
6
+ xmlns="http://www.w3.org/2000/svg"
7
+ width="1em"
8
+ height="1em"
9
+ class={classNames}
10
+ fill="none"
11
+ viewBox="0 0 26 23"
12
+ >
13
+ <path
14
+ fill="url(#a)"
15
+ d="M.93 10.65A10.17 10.17 0 0 1 11.11.48h4.67a9.45 9.45 0 0 1 0 18.89H4.53L1.62 22.2a.38.38 0 0 1-.69-.28V10.65Z"
16
+ />
17
+ <path
18
+ fill="#000"
19
+ fill-rule="evenodd"
20
+ d="M11.52 7.4a1.86 1.86 0 1 1-3.72 0 1.86 1.86 0 0 1 3.72 0Zm7.57 0a1.86 1.86 0 1 1-3.73 0 1.86 1.86 0 0 1 3.73 0ZM8.9 12.9a.55.55 0 0 0-.11.35.76.76 0 0 1-1.51 0c0-.95.67-1.94 1.76-1.94 1.09 0 1.76 1 1.76 1.94H9.3a.55.55 0 0 0-.12-.35c-.06-.07-.1-.08-.13-.08s-.08 0-.14.08Zm4.04 0a.55.55 0 0 0-.12.35h-1.51c0-.95.68-1.94 1.76-1.94 1.1 0 1.77 1 1.77 1.94h-1.51a.55.55 0 0 0-.12-.35c-.06-.07-.11-.08-.14-.08-.02 0-.07 0-.13.08Zm-1.89.79c-.02 0-.07-.01-.13-.08a.55.55 0 0 1-.12-.36h-1.5c0 .95.67 1.95 1.75 1.95 1.1 0 1.77-1 1.77-1.95h-1.51c0 .16-.06.28-.12.36-.06.07-.11.08-.14.08Zm4.04 0c-.03 0-.08-.01-.14-.08a.55.55 0 0 1-.12-.36h-1.5c0 .95.67 1.95 1.76 1.95 1.08 0 1.76-1 1.76-1.95h-1.51c0 .16-.06.28-.12.36-.06.07-.11.08-.13.08Zm1.76-.44c0-.16.05-.28.12-.35.06-.07.1-.08.13-.08s.08 0 .14.08c.06.07.11.2.11.35a.76.76 0 0 0 1.51 0c0-.95-.67-1.94-1.76-1.94-1.09 0-1.76 1-1.76 1.94h1.5Z"
21
+ clip-rule="evenodd"
22
+ />
23
+ <defs>
24
+ <radialGradient
25
+ id="a"
26
+ cx="0"
27
+ cy="0"
28
+ r="1"
29
+ gradientTransform="matrix(0 31.37 -34.85 0 13.08 -9.02)"
30
+ gradientUnits="userSpaceOnUse"
31
+ >
32
+ <stop stop-color="#FFD21E" />
33
+ <stop offset="1" stop-color="red" />
34
+ </radialGradient>
35
+ </defs>
36
+ </svg>
src/lib/components/icons/IconLoading.svelte ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ export let classNames = "";
3
+ </script>
4
+
5
+ <div class={"inline-flex h-8 flex-none items-center gap-1 " + classNames}>
6
+ <div
7
+ class="h-1 w-1 flex-none animate-bounce rounded-full bg-gray-500 dark:bg-gray-400"
8
+ style="animation-delay: 0.25s;"
9
+ />
10
+ <div
11
+ class="h-1 w-1 flex-none animate-bounce rounded-full bg-gray-500 dark:bg-gray-400"
12
+ style="animation-delay: 0.5s;"
13
+ />
14
+ <div
15
+ class="h-1 w-1 flex-none animate-bounce rounded-full bg-gray-500 dark:bg-gray-400"
16
+ style="animation-delay: 0.75s;"
17
+ />
18
+ </div>
src/lib/components/icons/Logo.svelte ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { page } from "$app/stores";
3
+ import { PUBLIC_APP_ASSETS, PUBLIC_APP_NAME, PUBLIC_ORIGIN } from "$env/static/public";
4
+ import { base } from "$app/paths";
5
+
6
+ export let classNames = "";
7
+ </script>
8
+
9
+ {#if PUBLIC_APP_ASSETS === "chatui"}
10
+ <svg
11
+ height="30"
12
+ width="30"
13
+ viewBox="0 0 30 30"
14
+ xmlns="http://www.w3.org/2000/svg"
15
+ class={classNames}
16
+ >
17
+ <path
18
+ d="M4.06151 14.1464C4.06151 11.8818 4.9611 9.71004 6.56237 8.10877C8.16364 6.5075 10.3354 5.60791 12.6 5.60791H16.5231C18.6254 5.60791 20.6416 6.44307 22.1282 7.92965C23.6148 9.41624 24.45 11.4325 24.45 13.5348C24.45 15.6372 23.6148 17.6534 22.1282 19.14C20.6416 20.6266 18.6254 21.4618 16.5231 21.4618H7.08459L4.63844 23.8387C4.59547 23.8942 4.53557 23.9343 4.4678 23.9527C4.40004 23.9712 4.32811 23.9671 4.2629 23.941C4.1977 23.9149 4.14276 23.8683 4.10643 23.8082C4.07009 23.7481 4.05432 23.6778 4.06151 23.6079V14.1464Z"
19
+ class="fill-primary-500"
20
+ />
21
+ </svg>
22
+ {:else}
23
+ <object
24
+ class={classNames}
25
+ data="{PUBLIC_ORIGIN || $page.url.origin}{base}/{PUBLIC_APP_ASSETS}/favicon.svg"
26
+ title="{PUBLIC_APP_NAME} logo"
27
+ />
28
+ {/if}