nsarrazin HF staff Mishig victor HF staff MichaelFried commited on
Commit
992a8de
1 Parent(s): 13489e8

Assistants feature (#639)

Browse files

* First push on assistants

* push fixes

* fix add assistant

* Sign up works

* lint

* mobile layout fixes

* design fixes

* Merge branch 'main' into feature/assistants

* fix copy button

* add error feedback

* hide duplicate feature

* remove wrong comments

* add autoredirect if assistant is missing

* latest changes:
- add edit feature
- hash assistant avatar
- get rid of ugly line
- check for non existent avatar
- make a better looking upload icon

* Update src/routes/conversation/+server.ts

Co-authored-by: Mishig <[email protected]>

* reused type more cleanly

* fix type in shared conversation

* fixed feature

* fix: share conv with an assistant

* delete assistant avatars in db when deleting avatar

* affordance on avatar upload

* improve assistant conv start on mobile

* settings modal fly in

* better mobile intro

* mobile padding

* link affordance

* Make assistants disabled by default, but enabled in huggingchat

* lint

* Fix bottom model name

* ui tweaks

* Initial work on chat thumbnails

* fix build

* Get rid of deps

* Update src/routes/settings/assistants/[assistantId]/avatar/+server.ts

Co-authored-by: Mishig <[email protected]>

* add comment to app_base

* Use event modifiers

* Use CSS uppercase instead everywhere

* Update src/lib/components/NavMenu.svelte

Co-authored-by: Mishig <[email protected]>

* Update src/routes/+layout.server.ts

Co-authored-by: Mishig <[email protected]>

* Clearer error message for avatar size check

* one less op on flag check

* revert back preventDefault change in LoginModal

* Update src/routes/settings/+layout.svelte

Co-authored-by: Mishig <[email protected]>

* Update src/routes/+layout.server.ts

Co-authored-by: Mishig <[email protected]>

* Update src/routes/+layout.server.ts

Co-authored-by: Mishig <[email protected]>

* Added app logo in corner of thumbnail and clamped description length

* improved thumbnails

* Remove warnings

* Reuse Assisntants settings component (#678)

* Update Assisntants settings

* format

* [Assistants] Use textToImage task for avatar generation (#662)

* Generate assistants avatar using stablediffusion

* wording

* Update +page.server.ts

Co-authored-by: Michael Fried <[email protected]>

* Add timeout & controls to avatar generation

* Add controls for avatar generation in .env

* Update src/routes/+layout.server.ts

Co-authored-by: Mishig <[email protected]>

* Update src/lib/components/AssistantSettings.svelte

Co-authored-by: Mishig <[email protected]>

* Fix avatar gen feature flag

* Can only upload avatar if generate is unchecked

---------

Co-authored-by: Michael Fried <[email protected]>
Co-authored-by: Mishig <[email protected]>

* layout

* small fixes

* hint

* Show feature if login is not required

* lint

* Only show creator name if it's defined

* tweaks

* thumbnail update

* thumbnail font-size

* Always display model at the bottom

* Bottom links now go to settings

* fix lint

* silent release

* fix bg on share link

* [Assistant] Delete avatar button instead of reset (#725)

* Add rate-limited image generating endpoint

* Add generate avatar button

* add little padding for firefox focus ring

* format

* fix upload image bug

* Fix uploads, replace reset by delete

* left-align buttons

* rm avatar generation feature

* final changes to delete feature

* sys prompt min height

* padding

* Add object-cover everywhere

---------

Co-authored-by: Victor Mustar <[email protected]>

---------

Co-authored-by: Mishig <[email protected]>
Co-authored-by: Victor Mustar <[email protected]>
Co-authored-by: Michael Fried <[email protected]>

This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .env +4 -1
  2. .env.template +2 -1
  3. .vscode/settings.json +1 -1
  4. package-lock.json +370 -0
  5. package.json +3 -0
  6. src/lib/components/AssistantSettings.svelte +277 -0
  7. src/lib/components/DisclaimerModal.svelte +1 -2
  8. src/lib/components/LoginModal.svelte +0 -1
  9. src/lib/components/NavConversationItem.svelte +22 -4
  10. src/lib/components/NavMenu.svelte +2 -7
  11. src/lib/components/chat/AssistantIntroduction.svelte +85 -0
  12. src/lib/components/chat/ChatMessages.svelte +33 -2
  13. src/lib/components/chat/ChatWindow.svelte +16 -15
  14. src/lib/server/database.ts +8 -0
  15. src/lib/stores/settings.ts +4 -0
  16. src/lib/types/Assistant.ts +15 -0
  17. src/lib/types/ConvSidebar.ts +8 -0
  18. src/lib/types/Conversation.ts +2 -0
  19. src/lib/types/Report.ts +10 -0
  20. src/lib/types/Settings.ts +5 -0
  21. src/lib/types/SharedConversation.ts +2 -0
  22. src/lib/utils/timeout.ts +6 -0
  23. src/routes/+layout.server.ts +67 -29
  24. src/routes/+layout.svelte +19 -8
  25. src/routes/+page.svelte +20 -1
  26. src/routes/assistant/[assistantId]/+page.server.ts +20 -0
  27. src/routes/assistant/[assistantId]/+page.svelte +112 -0
  28. src/routes/assistant/[assistantId]/thumbnail.png/+server.ts +64 -0
  29. src/routes/assistant/[assistantId]/thumbnail.png/ChatThumbnail.svelte +41 -0
  30. src/routes/conversation/+server.ts +14 -3
  31. src/routes/conversation/[id]/+page.server.ts +10 -0
  32. src/routes/conversation/[id]/share/+server.ts +1 -0
  33. src/routes/settings/+layout.server.ts +31 -0
  34. src/routes/settings/+layout.svelte +65 -18
  35. src/routes/settings/+server.ts +1 -2
  36. src/routes/settings/[...model]/+page.svelte +1 -1
  37. src/routes/settings/assistants/[assistantId]/+page.server.ts +115 -0
  38. src/routes/settings/assistants/[assistantId]/+page.svelte +156 -0
  39. src/routes/settings/assistants/[assistantId]/+page.ts +14 -0
  40. src/routes/settings/assistants/[assistantId]/avatar/+server.ts +46 -0
  41. src/routes/settings/assistants/[assistantId]/edit/+page.server.ts +136 -0
  42. src/routes/settings/assistants/[assistantId]/edit/+page.svelte +12 -0
  43. src/routes/settings/assistants/new/+page.server.ts +112 -0
  44. src/routes/settings/assistants/new/+page.svelte +9 -0
  45. static/fonts/Inter-Black.ttf +0 -0
  46. static/fonts/Inter-Bold.ttf +0 -0
  47. static/fonts/Inter-ExtraBold.ttf +0 -0
  48. static/fonts/Inter-ExtraLight.ttf +0 -0
  49. static/fonts/Inter-Light.ttf +0 -0
  50. static/fonts/Inter-Medium.ttf +0 -0
.env CHANGED
@@ -112,6 +112,7 @@ PARQUET_EXPORT_SECRET=
112
  RATE_LIMIT= # requests per minute
113
  MESSAGES_BEFORE_LOGIN=# how many messages a user can send in a conversation before having to login. set to 0 to force login right away
114
 
 
115
  PUBLIC_APP_NAME=ChatUI # name used as title throughout the app
116
  PUBLIC_APP_ASSETS=chatui # used to find logos & favicons in static/$PUBLIC_APP_ASSETS
117
  PUBLIC_APP_COLOR=blue # can be any of tailwind colors: https://tailwindcss.com/docs/customizing-colors#default-color-palette
@@ -126,4 +127,6 @@ EXPOSE_API=true
126
  # PUBLIC_APP_COLOR=yellow
127
  # PUBLIC_APP_DESCRIPTION="Making the community's best AI chat models available to everyone."
128
  # PUBLIC_APP_DATA_SHARING=1
129
- # PUBLIC_APP_DISCLAIMER=1
 
 
 
112
  RATE_LIMIT= # requests per minute
113
  MESSAGES_BEFORE_LOGIN=# how many messages a user can send in a conversation before having to login. set to 0 to force login right away
114
 
115
+ APP_BASE="" # base path of the app, e.g. /chat, left blank as default
116
  PUBLIC_APP_NAME=ChatUI # name used as title throughout the app
117
  PUBLIC_APP_ASSETS=chatui # used to find logos & favicons in static/$PUBLIC_APP_ASSETS
118
  PUBLIC_APP_COLOR=blue # can be any of tailwind colors: https://tailwindcss.com/docs/customizing-colors#default-color-palette
 
127
  # PUBLIC_APP_COLOR=yellow
128
  # PUBLIC_APP_DESCRIPTION="Making the community's best AI chat models available to everyone."
129
  # PUBLIC_APP_DATA_SHARING=1
130
+ # PUBLIC_APP_DISCLAIMER=1
131
+
132
+ ENABLE_ASSISTANTS=false #set to true to enable assistants feature
.env.template CHANGED
@@ -254,4 +254,5 @@ PUBLIC_GOOGLE_ANALYTICS_ID=G-8Q63TH4CSL
254
  # ADDRESS_HEADER=X-Forwarded-For
255
  # XFF_DEPTH=2
256
 
257
- EXPOSE_API=false
 
 
254
  # ADDRESS_HEADER=X-Forwarded-For
255
  # XFF_DEPTH=2
256
 
257
+ ENABLE_ASSISTANTS=true
258
+ EXPOSE_API=false
.vscode/settings.json CHANGED
@@ -2,7 +2,7 @@
2
  "editor.formatOnSave": true,
3
  "editor.defaultFormatter": "esbenp.prettier-vscode",
4
  "editor.codeActionsOnSave": {
5
- "source.fixAll": true
6
  },
7
  "eslint.validate": ["javascript", "svelte"]
8
  }
 
2
  "editor.formatOnSave": true,
3
  "editor.defaultFormatter": "esbenp.prettier-vscode",
4
  "editor.codeActionsOnSave": {
5
+ "source.fixAll": "explicit"
6
  },
7
  "eslint.validate": ["javascript", "svelte"]
8
  }
package-lock.json CHANGED
@@ -11,6 +11,7 @@
11
  "@huggingface/hub": "^0.5.1",
12
  "@huggingface/inference": "^2.6.3",
13
  "@iconify-json/bi": "^1.1.21",
 
14
  "@xenova/transformers": "^2.6.0",
15
  "autoprefixer": "^10.4.14",
16
  "browser-image-resizer": "^2.4.1",
@@ -28,6 +29,8 @@
28
  "parquetjs": "^0.11.2",
29
  "postcss": "^8.4.31",
30
  "saslprep": "^1.0.3",
 
 
31
  "serpapi": "^1.1.1",
32
  "tailwind-scrollbar": "^3.0.0",
33
  "tailwindcss": "^3.4.0",
@@ -790,6 +793,208 @@
790
  "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
791
  "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="
792
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
793
  "node_modules/@rollup/plugin-commonjs": {
794
  "version": "25.0.7",
795
  "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-25.0.7.tgz",
@@ -922,6 +1127,21 @@
922
  }
923
  }
924
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
925
  "node_modules/@sveltejs/adapter-node": {
926
  "version": "1.3.1",
927
  "resolved": "https://registry.npmjs.org/@sveltejs/adapter-node/-/adapter-node-1.3.1.tgz",
@@ -1931,6 +2151,14 @@
1931
  "node": ">= 6"
1932
  }
1933
  },
 
 
 
 
 
 
 
 
1934
  "node_modules/caniuse-lite": {
1935
  "version": "1.0.30001542",
1936
  "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001542.tgz",
@@ -2190,6 +2418,34 @@
2190
  "node": "*"
2191
  }
2192
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2193
  "node_modules/css-tree": {
2194
  "version": "2.3.1",
2195
  "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz",
@@ -2472,6 +2728,11 @@
2472
  "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.359.tgz",
2473
  "integrity": "sha512-OoVcngKCIuNXtZnsYoqlCvr0Cf3NIPzDIgwUfI9bdTFjXCrr79lI0kwQstLPZ7WhCezLlGksZk/BFAzoXC7GDw=="
2474
  },
 
 
 
 
 
2475
  "node_modules/end-of-stream": {
2476
  "version": "1.4.4",
2477
  "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
@@ -2542,6 +2803,11 @@
2542
  "node": ">=6"
2543
  }
2544
  },
 
 
 
 
 
2545
  "node_modules/escape-string-regexp": {
2546
  "version": "4.0.0",
2547
  "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
@@ -2874,6 +3140,11 @@
2874
  "reusify": "^1.0.4"
2875
  }
2876
  },
 
 
 
 
 
2877
  "node_modules/file-entry-cache": {
2878
  "version": "6.0.1",
2879
  "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
@@ -3184,6 +3455,17 @@
3184
  "node": ">= 0.4"
3185
  }
3186
  },
 
 
 
 
 
 
 
 
 
 
 
3187
  "node_modules/highlight.js": {
3188
  "version": "11.7.0",
3189
  "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.7.0.tgz",
@@ -3682,6 +3964,23 @@
3682
  "node": ">=10"
3683
  }
3684
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3685
  "node_modules/lines-and-columns": {
3686
  "version": "1.2.4",
3687
  "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
@@ -4417,6 +4716,11 @@
4417
  "url": "https://github.com/sponsors/sindresorhus"
4418
  }
4419
  },
 
 
 
 
 
4420
  "node_modules/parent-module": {
4421
  "version": "1.0.1",
4422
  "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@@ -4457,6 +4761,15 @@
4457
  "node": ">=0.6.19"
4458
  }
4459
  },
 
 
 
 
 
 
 
 
 
4460
  "node_modules/parse5": {
4461
  "version": "7.1.2",
4462
  "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz",
@@ -5290,6 +5603,34 @@
5290
  "node": ">=6"
5291
  }
5292
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5293
  "node_modules/saxes": {
5294
  "version": "6.0.0",
5295
  "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
@@ -5553,6 +5894,11 @@
5553
  "safe-buffer": "~5.2.0"
5554
  }
5555
  },
 
 
 
 
 
5556
  "node_modules/strip-ansi": {
5557
  "version": "6.0.1",
5558
  "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
@@ -6054,6 +6400,11 @@
6054
  "globrex": "^0.1.2"
6055
  }
6056
  },
 
 
 
 
 
6057
  "node_modules/tinybench": {
6058
  "version": "2.5.0",
6059
  "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.5.0.tgz",
@@ -6270,6 +6621,11 @@
6270
  "node": ">=0.8.0"
6271
  }
6272
  },
 
 
 
 
 
6273
  "node_modules/undici": {
6274
  "version": "5.26.4",
6275
  "resolved": "https://registry.npmjs.org/undici/-/undici-5.26.4.tgz",
@@ -6281,6 +6637,15 @@
6281
  "node": ">=14.0"
6282
  }
6283
  },
 
 
 
 
 
 
 
 
 
6284
  "node_modules/universalify": {
6285
  "version": "0.2.0",
6286
  "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz",
@@ -6769,6 +7134,11 @@
6769
  "url": "https://github.com/sponsors/sindresorhus"
6770
  }
6771
  },
 
 
 
 
 
6772
  "node_modules/zod": {
6773
  "version": "3.22.3",
6774
  "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.3.tgz",
 
11
  "@huggingface/hub": "^0.5.1",
12
  "@huggingface/inference": "^2.6.3",
13
  "@iconify-json/bi": "^1.1.21",
14
+ "@resvg/resvg-js": "^2.6.0",
15
  "@xenova/transformers": "^2.6.0",
16
  "autoprefixer": "^10.4.14",
17
  "browser-image-resizer": "^2.4.1",
 
29
  "parquetjs": "^0.11.2",
30
  "postcss": "^8.4.31",
31
  "saslprep": "^1.0.3",
32
+ "satori": "^0.10.11",
33
+ "satori-html": "^0.3.2",
34
  "serpapi": "^1.1.1",
35
  "tailwind-scrollbar": "^3.0.0",
36
  "tailwindcss": "^3.4.0",
 
793
  "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
794
  "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="
795
  },
796
+ "node_modules/@resvg/resvg-js": {
797
+ "version": "2.6.0",
798
+ "resolved": "https://registry.npmjs.org/@resvg/resvg-js/-/resvg-js-2.6.0.tgz",
799
+ "integrity": "sha512-Tf3YpbBKcQn991KKcw/vg7vZf98v01seSv6CVxZBbRkL/xyjnoYB6KgrFL6zskT1A4dWC/vg77KyNOW+ePaNlA==",
800
+ "engines": {
801
+ "node": ">= 10"
802
+ },
803
+ "optionalDependencies": {
804
+ "@resvg/resvg-js-android-arm-eabi": "2.6.0",
805
+ "@resvg/resvg-js-android-arm64": "2.6.0",
806
+ "@resvg/resvg-js-darwin-arm64": "2.6.0",
807
+ "@resvg/resvg-js-darwin-x64": "2.6.0",
808
+ "@resvg/resvg-js-linux-arm-gnueabihf": "2.6.0",
809
+ "@resvg/resvg-js-linux-arm64-gnu": "2.6.0",
810
+ "@resvg/resvg-js-linux-arm64-musl": "2.6.0",
811
+ "@resvg/resvg-js-linux-x64-gnu": "2.6.0",
812
+ "@resvg/resvg-js-linux-x64-musl": "2.6.0",
813
+ "@resvg/resvg-js-win32-arm64-msvc": "2.6.0",
814
+ "@resvg/resvg-js-win32-ia32-msvc": "2.6.0",
815
+ "@resvg/resvg-js-win32-x64-msvc": "2.6.0"
816
+ }
817
+ },
818
+ "node_modules/@resvg/resvg-js-android-arm-eabi": {
819
+ "version": "2.6.0",
820
+ "resolved": "https://registry.npmjs.org/@resvg/resvg-js-android-arm-eabi/-/resvg-js-android-arm-eabi-2.6.0.tgz",
821
+ "integrity": "sha512-lJnZ/2P5aMocrFMW7HWhVne5gH82I8xH6zsfH75MYr4+/JOaVcGCTEQ06XFohGMdYRP3v05SSPLPvTM/RHjxfA==",
822
+ "cpu": [
823
+ "arm"
824
+ ],
825
+ "optional": true,
826
+ "os": [
827
+ "android"
828
+ ],
829
+ "engines": {
830
+ "node": ">= 10"
831
+ }
832
+ },
833
+ "node_modules/@resvg/resvg-js-android-arm64": {
834
+ "version": "2.6.0",
835
+ "resolved": "https://registry.npmjs.org/@resvg/resvg-js-android-arm64/-/resvg-js-android-arm64-2.6.0.tgz",
836
+ "integrity": "sha512-N527f529bjMwYWShZYfBD60dXA4Fux+D695QsHQ93BDYZSHUoOh1CUGUyICevnTxs7VgEl98XpArmUWBZQVMfQ==",
837
+ "cpu": [
838
+ "arm64"
839
+ ],
840
+ "optional": true,
841
+ "os": [
842
+ "android"
843
+ ],
844
+ "engines": {
845
+ "node": ">= 10"
846
+ }
847
+ },
848
+ "node_modules/@resvg/resvg-js-darwin-arm64": {
849
+ "version": "2.6.0",
850
+ "resolved": "https://registry.npmjs.org/@resvg/resvg-js-darwin-arm64/-/resvg-js-darwin-arm64-2.6.0.tgz",
851
+ "integrity": "sha512-MabUKLVayEwlPo0mIqAmMt+qESN8LltCvv5+GLgVga1avpUrkxj/fkU1TKm8kQegutUjbP/B0QuMuUr0uhF8ew==",
852
+ "cpu": [
853
+ "arm64"
854
+ ],
855
+ "optional": true,
856
+ "os": [
857
+ "darwin"
858
+ ],
859
+ "engines": {
860
+ "node": ">= 10"
861
+ }
862
+ },
863
+ "node_modules/@resvg/resvg-js-darwin-x64": {
864
+ "version": "2.6.0",
865
+ "resolved": "https://registry.npmjs.org/@resvg/resvg-js-darwin-x64/-/resvg-js-darwin-x64-2.6.0.tgz",
866
+ "integrity": "sha512-zrFetdnSw/suXjmyxSjfDV7i61hahv6DDG6kM7BYN2yJ3Es5+BZtqYZTcIWogPJedYKmzN1YTMWGd/3f0ubFiA==",
867
+ "cpu": [
868
+ "x64"
869
+ ],
870
+ "optional": true,
871
+ "os": [
872
+ "darwin"
873
+ ],
874
+ "engines": {
875
+ "node": ">= 10"
876
+ }
877
+ },
878
+ "node_modules/@resvg/resvg-js-linux-arm-gnueabihf": {
879
+ "version": "2.6.0",
880
+ "resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-arm-gnueabihf/-/resvg-js-linux-arm-gnueabihf-2.6.0.tgz",
881
+ "integrity": "sha512-sH4gxXt7v7dGwjGyzLwn7SFGvwZG6DQqLaZ11MmzbCwd9Zosy1TnmrMJfn6TJ7RHezmQMgBPi18bl55FZ1AT4A==",
882
+ "cpu": [
883
+ "arm"
884
+ ],
885
+ "optional": true,
886
+ "os": [
887
+ "linux"
888
+ ],
889
+ "engines": {
890
+ "node": ">= 10"
891
+ }
892
+ },
893
+ "node_modules/@resvg/resvg-js-linux-arm64-gnu": {
894
+ "version": "2.6.0",
895
+ "resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-arm64-gnu/-/resvg-js-linux-arm64-gnu-2.6.0.tgz",
896
+ "integrity": "sha512-fCyMncqCJtrlANADIduYF4IfnWQ295UKib7DAxFXQhBsM9PLDTpizr0qemZcCNadcwSVHnAIzL4tliZhCM8P6A==",
897
+ "cpu": [
898
+ "arm64"
899
+ ],
900
+ "optional": true,
901
+ "os": [
902
+ "linux"
903
+ ],
904
+ "engines": {
905
+ "node": ">= 10"
906
+ }
907
+ },
908
+ "node_modules/@resvg/resvg-js-linux-arm64-musl": {
909
+ "version": "2.6.0",
910
+ "resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-arm64-musl/-/resvg-js-linux-arm64-musl-2.6.0.tgz",
911
+ "integrity": "sha512-ouLjTgBQHQyxLht4FdMPTvuY8xzJigM9EM2Tlu0llWkN1mKyTQrvYWi6TA6XnKdzDJHy7ZLpWpjZi7F5+Pg+Vg==",
912
+ "cpu": [
913
+ "arm64"
914
+ ],
915
+ "optional": true,
916
+ "os": [
917
+ "linux"
918
+ ],
919
+ "engines": {
920
+ "node": ">= 10"
921
+ }
922
+ },
923
+ "node_modules/@resvg/resvg-js-linux-x64-gnu": {
924
+ "version": "2.6.0",
925
+ "resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-x64-gnu/-/resvg-js-linux-x64-gnu-2.6.0.tgz",
926
+ "integrity": "sha512-n3zC8DWsvxC1AwxpKFclIPapDFibs5XdIRoV/mcIlxlh0vseW1F49b97F33BtJQRmlntsqqN6GMMqx8byB7B+Q==",
927
+ "cpu": [
928
+ "x64"
929
+ ],
930
+ "optional": true,
931
+ "os": [
932
+ "linux"
933
+ ],
934
+ "engines": {
935
+ "node": ">= 10"
936
+ }
937
+ },
938
+ "node_modules/@resvg/resvg-js-linux-x64-musl": {
939
+ "version": "2.6.0",
940
+ "resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-x64-musl/-/resvg-js-linux-x64-musl-2.6.0.tgz",
941
+ "integrity": "sha512-n4tasK1HOlAxdTEROgYA1aCfsEKk0UOFDNd/AQTTZlTmCbHKXPq+O8npaaKlwXquxlVK8vrkcWbksbiGqbCAcw==",
942
+ "cpu": [
943
+ "x64"
944
+ ],
945
+ "optional": true,
946
+ "os": [
947
+ "linux"
948
+ ],
949
+ "engines": {
950
+ "node": ">= 10"
951
+ }
952
+ },
953
+ "node_modules/@resvg/resvg-js-win32-arm64-msvc": {
954
+ "version": "2.6.0",
955
+ "resolved": "https://registry.npmjs.org/@resvg/resvg-js-win32-arm64-msvc/-/resvg-js-win32-arm64-msvc-2.6.0.tgz",
956
+ "integrity": "sha512-X2+EoBJFwDI5LDVb51Sk7ldnVLitMGr9WwU/i21i3fAeAXZb3hM16k67DeTy16OYkT2dk/RfU1tP1wG+rWbz2Q==",
957
+ "cpu": [
958
+ "arm64"
959
+ ],
960
+ "optional": true,
961
+ "os": [
962
+ "win32"
963
+ ],
964
+ "engines": {
965
+ "node": ">= 10"
966
+ }
967
+ },
968
+ "node_modules/@resvg/resvg-js-win32-ia32-msvc": {
969
+ "version": "2.6.0",
970
+ "resolved": "https://registry.npmjs.org/@resvg/resvg-js-win32-ia32-msvc/-/resvg-js-win32-ia32-msvc-2.6.0.tgz",
971
+ "integrity": "sha512-L7oevWjQoUgK5W1fCKn0euSVemhDXVhrjtwqpc7MwBKKimYeiOshO1Li1pa8bBt5PESahenhWgdB6lav9O0fEg==",
972
+ "cpu": [
973
+ "ia32"
974
+ ],
975
+ "optional": true,
976
+ "os": [
977
+ "win32"
978
+ ],
979
+ "engines": {
980
+ "node": ">= 10"
981
+ }
982
+ },
983
+ "node_modules/@resvg/resvg-js-win32-x64-msvc": {
984
+ "version": "2.6.0",
985
+ "resolved": "https://registry.npmjs.org/@resvg/resvg-js-win32-x64-msvc/-/resvg-js-win32-x64-msvc-2.6.0.tgz",
986
+ "integrity": "sha512-8lJlghb+Unki5AyKgsnFbRJwkEj9r1NpwyuBG8yEJiG1W9eEGl03R3I7bsVa3haof/3J1NlWf0rzSa1G++A2iw==",
987
+ "cpu": [
988
+ "x64"
989
+ ],
990
+ "optional": true,
991
+ "os": [
992
+ "win32"
993
+ ],
994
+ "engines": {
995
+ "node": ">= 10"
996
+ }
997
+ },
998
  "node_modules/@rollup/plugin-commonjs": {
999
  "version": "25.0.7",
1000
  "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-25.0.7.tgz",
 
1127
  }
1128
  }
1129
  },
1130
+ "node_modules/@shuding/opentype.js": {
1131
+ "version": "1.4.0-beta.0",
1132
+ "resolved": "https://registry.npmjs.org/@shuding/opentype.js/-/opentype.js-1.4.0-beta.0.tgz",
1133
+ "integrity": "sha512-3NgmNyH3l/Hv6EvsWJbsvpcpUba6R8IREQ83nH83cyakCw7uM1arZKNfHwv1Wz6jgqrF/j4x5ELvR6PnK9nTcA==",
1134
+ "dependencies": {
1135
+ "fflate": "^0.7.3",
1136
+ "string.prototype.codepointat": "^0.2.1"
1137
+ },
1138
+ "bin": {
1139
+ "ot": "bin/ot"
1140
+ },
1141
+ "engines": {
1142
+ "node": ">= 8.0.0"
1143
+ }
1144
+ },
1145
  "node_modules/@sveltejs/adapter-node": {
1146
  "version": "1.3.1",
1147
  "resolved": "https://registry.npmjs.org/@sveltejs/adapter-node/-/adapter-node-1.3.1.tgz",
 
2151
  "node": ">= 6"
2152
  }
2153
  },
2154
+ "node_modules/camelize": {
2155
+ "version": "1.0.1",
2156
+ "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz",
2157
+ "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==",
2158
+ "funding": {
2159
+ "url": "https://github.com/sponsors/ljharb"
2160
+ }
2161
+ },
2162
  "node_modules/caniuse-lite": {
2163
  "version": "1.0.30001542",
2164
  "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001542.tgz",
 
2418
  "node": "*"
2419
  }
2420
  },
2421
+ "node_modules/css-background-parser": {
2422
+ "version": "0.1.0",
2423
+ "resolved": "https://registry.npmjs.org/css-background-parser/-/css-background-parser-0.1.0.tgz",
2424
+ "integrity": "sha512-2EZLisiZQ+7m4wwur/qiYJRniHX4K5Tc9w93MT3AS0WS1u5kaZ4FKXlOTBhOjc+CgEgPiGY+fX1yWD8UwpEqUA=="
2425
+ },
2426
+ "node_modules/css-box-shadow": {
2427
+ "version": "1.0.0-3",
2428
+ "resolved": "https://registry.npmjs.org/css-box-shadow/-/css-box-shadow-1.0.0-3.tgz",
2429
+ "integrity": "sha512-9jaqR6e7Ohds+aWwmhe6wILJ99xYQbfmK9QQB9CcMjDbTxPZjwEmUQpU91OG05Xgm8BahT5fW+svbsQGjS/zPg=="
2430
+ },
2431
+ "node_modules/css-color-keywords": {
2432
+ "version": "1.0.0",
2433
+ "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz",
2434
+ "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==",
2435
+ "engines": {
2436
+ "node": ">=4"
2437
+ }
2438
+ },
2439
+ "node_modules/css-to-react-native": {
2440
+ "version": "3.2.0",
2441
+ "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz",
2442
+ "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==",
2443
+ "dependencies": {
2444
+ "camelize": "^1.0.0",
2445
+ "css-color-keywords": "^1.0.0",
2446
+ "postcss-value-parser": "^4.0.2"
2447
+ }
2448
+ },
2449
  "node_modules/css-tree": {
2450
  "version": "2.3.1",
2451
  "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz",
 
2728
  "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.359.tgz",
2729
  "integrity": "sha512-OoVcngKCIuNXtZnsYoqlCvr0Cf3NIPzDIgwUfI9bdTFjXCrr79lI0kwQstLPZ7WhCezLlGksZk/BFAzoXC7GDw=="
2730
  },
2731
+ "node_modules/emoji-regex": {
2732
+ "version": "10.3.0",
2733
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz",
2734
+ "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw=="
2735
+ },
2736
  "node_modules/end-of-stream": {
2737
  "version": "1.4.4",
2738
  "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
 
2803
  "node": ">=6"
2804
  }
2805
  },
2806
+ "node_modules/escape-html": {
2807
+ "version": "1.0.3",
2808
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
2809
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
2810
+ },
2811
  "node_modules/escape-string-regexp": {
2812
  "version": "4.0.0",
2813
  "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
 
3140
  "reusify": "^1.0.4"
3141
  }
3142
  },
3143
+ "node_modules/fflate": {
3144
+ "version": "0.7.4",
3145
+ "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.7.4.tgz",
3146
+ "integrity": "sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw=="
3147
+ },
3148
  "node_modules/file-entry-cache": {
3149
  "version": "6.0.1",
3150
  "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
 
3455
  "node": ">= 0.4"
3456
  }
3457
  },
3458
+ "node_modules/hex-rgb": {
3459
+ "version": "4.3.0",
3460
+ "resolved": "https://registry.npmjs.org/hex-rgb/-/hex-rgb-4.3.0.tgz",
3461
+ "integrity": "sha512-Ox1pJVrDCyGHMG9CFg1tmrRUMRPRsAWYc/PinY0XzJU4K7y7vjNoLKIQ7BR5UJMCxNN8EM1MNDmHWA/B3aZUuw==",
3462
+ "engines": {
3463
+ "node": ">=6"
3464
+ },
3465
+ "funding": {
3466
+ "url": "https://github.com/sponsors/sindresorhus"
3467
+ }
3468
+ },
3469
  "node_modules/highlight.js": {
3470
  "version": "11.7.0",
3471
  "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.7.0.tgz",
 
3964
  "node": ">=10"
3965
  }
3966
  },
3967
+ "node_modules/linebreak": {
3968
+ "version": "1.1.0",
3969
+ "resolved": "https://registry.npmjs.org/linebreak/-/linebreak-1.1.0.tgz",
3970
+ "integrity": "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==",
3971
+ "dependencies": {
3972
+ "base64-js": "0.0.8",
3973
+ "unicode-trie": "^2.0.0"
3974
+ }
3975
+ },
3976
+ "node_modules/linebreak/node_modules/base64-js": {
3977
+ "version": "0.0.8",
3978
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz",
3979
+ "integrity": "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==",
3980
+ "engines": {
3981
+ "node": ">= 0.4"
3982
+ }
3983
+ },
3984
  "node_modules/lines-and-columns": {
3985
  "version": "1.2.4",
3986
  "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
 
4716
  "url": "https://github.com/sponsors/sindresorhus"
4717
  }
4718
  },
4719
+ "node_modules/pako": {
4720
+ "version": "0.2.9",
4721
+ "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz",
4722
+ "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA=="
4723
+ },
4724
  "node_modules/parent-module": {
4725
  "version": "1.0.1",
4726
  "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
 
4761
  "node": ">=0.6.19"
4762
  }
4763
  },
4764
+ "node_modules/parse-css-color": {
4765
+ "version": "0.2.1",
4766
+ "resolved": "https://registry.npmjs.org/parse-css-color/-/parse-css-color-0.2.1.tgz",
4767
+ "integrity": "sha512-bwS/GGIFV3b6KS4uwpzCFj4w297Yl3uqnSgIPsoQkx7GMLROXfMnWvxfNkL0oh8HVhZA4hvJoEoEIqonfJ3BWg==",
4768
+ "dependencies": {
4769
+ "color-name": "^1.1.4",
4770
+ "hex-rgb": "^4.1.0"
4771
+ }
4772
+ },
4773
  "node_modules/parse5": {
4774
  "version": "7.1.2",
4775
  "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz",
 
5603
  "node": ">=6"
5604
  }
5605
  },
5606
+ "node_modules/satori": {
5607
+ "version": "0.10.11",
5608
+ "resolved": "https://registry.npmjs.org/satori/-/satori-0.10.11.tgz",
5609
+ "integrity": "sha512-yLm1xPRPZUaKcBZJ6nmezoJjHB4MqV8x7Mu0PyZUJodRWRDD27UbeMwzuY9LEGG57WYLO4CQsGPlbHWV1Ex9TQ==",
5610
+ "dependencies": {
5611
+ "@shuding/opentype.js": "1.4.0-beta.0",
5612
+ "css-background-parser": "^0.1.0",
5613
+ "css-box-shadow": "1.0.0-3",
5614
+ "css-to-react-native": "^3.0.0",
5615
+ "emoji-regex": "^10.2.1",
5616
+ "escape-html": "^1.0.3",
5617
+ "linebreak": "^1.1.0",
5618
+ "parse-css-color": "^0.2.1",
5619
+ "postcss-value-parser": "^4.2.0",
5620
+ "yoga-wasm-web": "^0.3.3"
5621
+ },
5622
+ "engines": {
5623
+ "node": ">=16"
5624
+ }
5625
+ },
5626
+ "node_modules/satori-html": {
5627
+ "version": "0.3.2",
5628
+ "resolved": "https://registry.npmjs.org/satori-html/-/satori-html-0.3.2.tgz",
5629
+ "integrity": "sha512-wjTh14iqADFKDK80e51/98MplTGfxz2RmIzh0GqShlf4a67+BooLywF17TvJPD6phO0Hxm7Mf1N5LtRYvdkYRA==",
5630
+ "dependencies": {
5631
+ "ultrahtml": "^1.2.0"
5632
+ }
5633
+ },
5634
  "node_modules/saxes": {
5635
  "version": "6.0.0",
5636
  "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
 
5894
  "safe-buffer": "~5.2.0"
5895
  }
5896
  },
5897
+ "node_modules/string.prototype.codepointat": {
5898
+ "version": "0.2.1",
5899
+ "resolved": "https://registry.npmjs.org/string.prototype.codepointat/-/string.prototype.codepointat-0.2.1.tgz",
5900
+ "integrity": "sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg=="
5901
+ },
5902
  "node_modules/strip-ansi": {
5903
  "version": "6.0.1",
5904
  "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
 
6400
  "globrex": "^0.1.2"
6401
  }
6402
  },
6403
+ "node_modules/tiny-inflate": {
6404
+ "version": "1.0.3",
6405
+ "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz",
6406
+ "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="
6407
+ },
6408
  "node_modules/tinybench": {
6409
  "version": "2.5.0",
6410
  "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.5.0.tgz",
 
6621
  "node": ">=0.8.0"
6622
  }
6623
  },
6624
+ "node_modules/ultrahtml": {
6625
+ "version": "1.5.2",
6626
+ "resolved": "https://registry.npmjs.org/ultrahtml/-/ultrahtml-1.5.2.tgz",
6627
+ "integrity": "sha512-qh4mBffhlkiXwDAOxvSGxhL0QEQsTbnP9BozOK3OYPEGvPvdWzvAUaXNtUSMdNsKDtuyjEbyVUPFZ52SSLhLqw=="
6628
+ },
6629
  "node_modules/undici": {
6630
  "version": "5.26.4",
6631
  "resolved": "https://registry.npmjs.org/undici/-/undici-5.26.4.tgz",
 
6637
  "node": ">=14.0"
6638
  }
6639
  },
6640
+ "node_modules/unicode-trie": {
6641
+ "version": "2.0.0",
6642
+ "resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz",
6643
+ "integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==",
6644
+ "dependencies": {
6645
+ "pako": "^0.2.5",
6646
+ "tiny-inflate": "^1.0.0"
6647
+ }
6648
+ },
6649
  "node_modules/universalify": {
6650
  "version": "0.2.0",
6651
  "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz",
 
7134
  "url": "https://github.com/sponsors/sindresorhus"
7135
  }
7136
  },
7137
+ "node_modules/yoga-wasm-web": {
7138
+ "version": "0.3.3",
7139
+ "resolved": "https://registry.npmjs.org/yoga-wasm-web/-/yoga-wasm-web-0.3.3.tgz",
7140
+ "integrity": "sha512-N+d4UJSJbt/R3wqY7Coqs5pcV0aUj2j9IaQ3rNj9bVCLld8tTGKRa2USARjnvZJWVx1NDmQev8EknoczaOQDOA=="
7141
+ },
7142
  "node_modules/zod": {
7143
  "version": "3.22.3",
7144
  "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.3.tgz",
package.json CHANGED
@@ -47,6 +47,7 @@
47
  "@huggingface/hub": "^0.5.1",
48
  "@huggingface/inference": "^2.6.3",
49
  "@iconify-json/bi": "^1.1.21",
 
50
  "@xenova/transformers": "^2.6.0",
51
  "autoprefixer": "^10.4.14",
52
  "browser-image-resizer": "^2.4.1",
@@ -64,6 +65,8 @@
64
  "parquetjs": "^0.11.2",
65
  "postcss": "^8.4.31",
66
  "saslprep": "^1.0.3",
 
 
67
  "serpapi": "^1.1.1",
68
  "tailwind-scrollbar": "^3.0.0",
69
  "tailwindcss": "^3.4.0",
 
47
  "@huggingface/hub": "^0.5.1",
48
  "@huggingface/inference": "^2.6.3",
49
  "@iconify-json/bi": "^1.1.21",
50
+ "@resvg/resvg-js": "^2.6.0",
51
  "@xenova/transformers": "^2.6.0",
52
  "autoprefixer": "^10.4.14",
53
  "browser-image-resizer": "^2.4.1",
 
65
  "parquetjs": "^0.11.2",
66
  "postcss": "^8.4.31",
67
  "saslprep": "^1.0.3",
68
+ "satori": "^0.10.11",
69
+ "satori-html": "^0.3.2",
70
  "serpapi": "^1.1.1",
71
  "tailwind-scrollbar": "^3.0.0",
72
  "tailwindcss": "^3.4.0",
src/lib/components/AssistantSettings.svelte ADDED
@@ -0,0 +1,277 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import type { readAndCompressImage } from "browser-image-resizer";
3
+ import type { Model } from "$lib/types/Model";
4
+ import type { Assistant } from "$lib/types/Assistant";
5
+
6
+ import { onMount } from "svelte";
7
+ import { applyAction, enhance } from "$app/forms";
8
+ import { base } from "$app/paths";
9
+ import CarbonPen from "~icons/carbon/pen";
10
+ import CarbonUpload from "~icons/carbon/upload";
11
+ import { useSettingsStore } from "$lib/stores/settings";
12
+ import IconLoading from "./icons/IconLoading.svelte";
13
+
14
+ type ActionData = {
15
+ error: boolean;
16
+ errors: {
17
+ field: string | number;
18
+ message: string;
19
+ }[];
20
+ } | null;
21
+
22
+ type AssistantFront = Omit<Assistant, "_id" | "createdById"> & { _id: string };
23
+
24
+ export let form: ActionData;
25
+ export let assistant: AssistantFront | undefined = undefined;
26
+ export let models: Model[] = [];
27
+
28
+ let files: FileList | null = null;
29
+
30
+ const settings = useSettingsStore();
31
+
32
+ let compress: typeof readAndCompressImage | null = null;
33
+
34
+ onMount(async () => {
35
+ const module = await import("browser-image-resizer");
36
+ compress = module.readAndCompressImage;
37
+ });
38
+
39
+ let inputMessage1 = assistant?.exampleInputs[0] ?? "";
40
+ let inputMessage2 = assistant?.exampleInputs[1] ?? "";
41
+ let inputMessage3 = assistant?.exampleInputs[2] ?? "";
42
+ let inputMessage4 = assistant?.exampleInputs[3] ?? "";
43
+
44
+ function resetErrors() {
45
+ if (form) {
46
+ form.errors = [];
47
+ form.error = false;
48
+ }
49
+ }
50
+
51
+ function onFilesChange(e: Event) {
52
+ const inputEl = e.target as HTMLInputElement;
53
+ if (inputEl.files?.length) {
54
+ files = inputEl.files;
55
+ resetErrors();
56
+ deleteExistingAvatar = false;
57
+ }
58
+ }
59
+
60
+ function getError(field: string, returnForm: ActionData) {
61
+ return returnForm?.errors.find((error) => error.field === field)?.message ?? "";
62
+ }
63
+
64
+ let deleteExistingAvatar = false;
65
+
66
+ let loading = false;
67
+ </script>
68
+
69
+ <form
70
+ method="POST"
71
+ class="flex h-full flex-col"
72
+ enctype="multipart/form-data"
73
+ use:enhance={async ({ formData }) => {
74
+ loading = true;
75
+ if (files?.[0] && files[0].size > 0 && compress) {
76
+ await compress(files[0], {
77
+ maxWidth: 500,
78
+ maxHeight: 500,
79
+ quality: 1,
80
+ }).then((resizedImage) => {
81
+ formData.set("avatar", resizedImage);
82
+ });
83
+ }
84
+
85
+ if (deleteExistingAvatar === true) {
86
+ if (assistant?.avatar) {
87
+ // if there is an avatar we explicitly removei t
88
+ formData.set("avatar", "null");
89
+ } else {
90
+ // else we just remove it from the input
91
+ formData.delete("avatar");
92
+ }
93
+ }
94
+
95
+ return async ({ result }) => {
96
+ loading = false;
97
+ await applyAction(result);
98
+ };
99
+ }}
100
+ >
101
+ {#if assistant}
102
+ <h2 class="text-xl font-semibold">Edit assistant ({assistant?.name ?? ""})</h2>
103
+ <p class="mb-6 text-sm text-gray-500">
104
+ Modifying an existing assistant will propagate those changes to all users.
105
+ </p>
106
+ {:else}
107
+ <h2 class="text-xl font-semibold">Create new assistant</h2>
108
+ <p class="mb-6 text-sm text-gray-500">
109
+ Assistants are public, and can be accessed by anyone with the link.
110
+ </p>
111
+ {/if}
112
+
113
+ <div class="mx-1 grid flex-1 grid-cols-2 gap-4 max-sm:grid-cols-1">
114
+ <div class="flex flex-col gap-4">
115
+ <div>
116
+ <span class="mb-1 block pb-2 text-sm font-semibold">Avatar</span>
117
+ <input
118
+ type="file"
119
+ accept="image/*"
120
+ name="avatar"
121
+ id="avatar"
122
+ class="hidden"
123
+ on:change={onFilesChange}
124
+ />
125
+
126
+ {#if (files && files[0]) || (assistant?.avatar && !deleteExistingAvatar)}
127
+ <div class="group relative mx-auto h-12 w-12">
128
+ {#if files && files[0]}
129
+ <img
130
+ src={URL.createObjectURL(files[0])}
131
+ alt="avatar"
132
+ class="crop mx-auto h-12 w-12 cursor-pointer rounded-full object-cover"
133
+ />
134
+ {:else if assistant?.avatar}
135
+ <img
136
+ src="{base}/settings/assistants/{assistant._id}/avatar?hash={assistant.avatar}"
137
+ alt="avatar"
138
+ class="crop mx-auto h-12 w-12 cursor-pointer rounded-full object-cover"
139
+ />
140
+ {/if}
141
+
142
+ <label
143
+ for="avatar"
144
+ class="invisible absolute bottom-0 h-12 w-12 rounded-full bg-black bg-opacity-50 p-1 group-hover:visible hover:visible"
145
+ >
146
+ <CarbonPen class="mx-auto my-auto h-full cursor-pointer text-center text-white" />
147
+ </label>
148
+ </div>
149
+ <div class="mx-auto w-max pt-1">
150
+ <button
151
+ type="button"
152
+ on:click|stopPropagation|preventDefault={() => {
153
+ files = null;
154
+ deleteExistingAvatar = true;
155
+ }}
156
+ class="mx-auto w-max text-center text-xs text-gray-600 hover:underline"
157
+ >
158
+ Delete
159
+ </button>
160
+ </div>
161
+ {:else}
162
+ <div class="mb-1 flex w-max flex-row gap-4">
163
+ <label
164
+ for="avatar"
165
+ class="btn flex h-8 rounded-lg border bg-white px-3 py-1 text-gray-500 shadow-sm transition-all hover:bg-gray-100"
166
+ >
167
+ <CarbonUpload class="mr-2 text-xs " /> Upload
168
+ </label>
169
+ </div>
170
+ <p class="text-xs text-red-500">{getError("avatar", form)}</p>
171
+ {/if}
172
+ </div>
173
+
174
+ <label>
175
+ <span class="mb-1 text-sm font-semibold">Name</span>
176
+ <input
177
+ name="name"
178
+ class="w-full rounded-lg border-2 border-gray-200 bg-gray-100 p-2"
179
+ placeholder="My awesome model"
180
+ value={assistant?.name ?? ""}
181
+ />
182
+ <p class="text-xs text-red-500">{getError("name", form)}</p>
183
+ </label>
184
+
185
+ <label>
186
+ <span class="mb-1 text-sm font-semibold">Description</span>
187
+ <textarea
188
+ name="description"
189
+ class="w-full rounded-lg border-2 border-gray-200 bg-gray-100 p-2"
190
+ placeholder="He knows everything about python"
191
+ value={assistant?.description ?? ""}
192
+ />
193
+ <p class="text-xs text-red-500">{getError("description", form)}</p>
194
+ </label>
195
+
196
+ <label>
197
+ <span class="mb-1 text-sm font-semibold">Model</span>
198
+ <select name="modelId" class="w-full rounded-lg border-2 border-gray-200 bg-gray-100 p-2">
199
+ {#each models as model}
200
+ <option
201
+ value={model.id}
202
+ selected={assistant
203
+ ? assistant?.modelId === model.id
204
+ : $settings.activeModel === model.id}>{model.displayName}</option
205
+ >
206
+ {/each}
207
+ <p class="text-xs text-red-500">{getError("modelId", form)}</p>
208
+ </select>
209
+ </label>
210
+
211
+ <label>
212
+ <span class="mb-1 text-sm font-semibold">Start messages</span>
213
+ <div class="flex flex-col gap-2 md:max-h-32">
214
+ <input
215
+ name="exampleInput1"
216
+ bind:value={inputMessage1}
217
+ class="w-full rounded-lg border-2 border-gray-200 bg-gray-100 p-2"
218
+ />
219
+ {#if !!inputMessage1 || !!inputMessage2}
220
+ <input
221
+ name="exampleInput2"
222
+ bind:value={inputMessage2}
223
+ class="w-full rounded-lg border-2 border-gray-200 bg-gray-100 p-2"
224
+ />
225
+ {/if}
226
+ {#if !!inputMessage2 || !!inputMessage3}
227
+ <input
228
+ name="exampleInput3"
229
+ bind:value={inputMessage3}
230
+ class="w-full rounded-lg border-2 border-gray-200 bg-gray-100 p-2"
231
+ />
232
+ {/if}
233
+ {#if !!inputMessage3 || !!inputMessage4}
234
+ <input
235
+ name="exampleInput4"
236
+ bind:value={inputMessage4}
237
+ class="w-full rounded-lg border-2 border-gray-200 bg-gray-100 p-2"
238
+ />
239
+ {/if}
240
+ </div>
241
+ <p class="text-xs text-red-500">{getError("inputMessage1", form)}</p>
242
+ </label>
243
+ </div>
244
+
245
+ <label class="flex flex-col">
246
+ <span class="mb-1 text-sm font-semibold"> Instructions (system prompt) </span>
247
+ <textarea
248
+ name="preprompt"
249
+ class="min-h-[8lh] flex-1 rounded-lg border-2 border-gray-200 bg-gray-100 p-2 text-sm"
250
+ placeholder="You'll act as..."
251
+ value={assistant?.preprompt ?? ""}
252
+ />
253
+ <p class="text-xs text-red-500">{getError("preprompt", form)}</p>
254
+ </label>
255
+ </div>
256
+
257
+ <div class="mt-5 flex justify-end gap-2">
258
+ <a
259
+ href={assistant ? `${base}/settings/assistants/${assistant?._id}` : `${base}/settings`}
260
+ class="rounded-full bg-gray-200 px-8 py-2 font-semibold text-gray-600">Cancel</a
261
+ >
262
+ <button
263
+ type="submit"
264
+ disabled={loading}
265
+ aria-disabled={loading}
266
+ class="rounded-full bg-black px-8 py-2 font-semibold md:px-20"
267
+ class:bg-gray-200={loading}
268
+ class:text-gray-600={loading}
269
+ class:text-white={!loading}
270
+ >
271
+ {assistant ? "Save" : "Create"}
272
+ {#if loading}
273
+ <IconLoading classNames="ml-2 h-min" />
274
+ {/if}
275
+ </button>
276
+ </div>
277
+ </form>
src/lib/components/DisclaimerModal.svelte CHANGED
@@ -36,9 +36,8 @@
36
  class:bg-white={$page.data.loginEnabled}
37
  class:text-gray-800={$page.data.loginEnabled}
38
  class:hover:bg-slate-100={$page.data.loginEnabled}
39
- on:click={(e) => {
40
  if (!cookiesAreEnabled()) {
41
- e.preventDefault();
42
  window.open(window.location.href, "_blank");
43
  }
44
 
 
36
  class:bg-white={$page.data.loginEnabled}
37
  class:text-gray-800={$page.data.loginEnabled}
38
  class:hover:bg-slate-100={$page.data.loginEnabled}
39
+ on:click|preventDefault|stopPropagation={() => {
40
  if (!cookiesAreEnabled()) {
 
41
  window.open(window.location.href, "_blank");
42
  }
43
 
src/lib/components/LoginModal.svelte CHANGED
@@ -51,7 +51,6 @@
51
  e.preventDefault();
52
  window.open(window.location.href, "_blank");
53
  }
54
-
55
  $settings.ethicsModalAccepted = true;
56
  }}
57
  >
 
51
  e.preventDefault();
52
  window.open(window.location.href, "_blank");
53
  }
 
54
  $settings.ethicsModalAccepted = true;
55
  }}
56
  >
src/lib/components/NavConversationItem.svelte CHANGED
@@ -7,8 +7,10 @@
7
  import CarbonTrashCan from "~icons/carbon/trash-can";
8
  import CarbonClose from "~icons/carbon/close";
9
  import CarbonEdit from "~icons/carbon/edit";
 
 
10
 
11
- export let conv: { id: string; title: string };
12
 
13
  let confirmDelete = false;
14
 
@@ -16,6 +18,8 @@
16
  deleteConversation: string;
17
  editConversationTitle: { id: string; title: string };
18
  }>();
 
 
19
  </script>
20
 
21
  <a
@@ -29,11 +33,25 @@
29
  ? 'bg-gray-100 dark:bg-gray-700'
30
  : ''}"
31
  >
32
- <div class="flex-1 truncate">
33
  {#if confirmDelete}
34
- <span class="font-semibold"> Delete </span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  {/if}
36
- {conv.title}
37
  </div>
38
 
39
  {#if confirmDelete}
 
7
  import CarbonTrashCan from "~icons/carbon/trash-can";
8
  import CarbonClose from "~icons/carbon/close";
9
  import CarbonEdit from "~icons/carbon/edit";
10
+ import { useSettingsStore } from "$lib/stores/settings";
11
+ import type { ConvSidebar } from "$lib/types/ConvSidebar";
12
 
13
+ export let conv: ConvSidebar;
14
 
15
  let confirmDelete = false;
16
 
 
18
  deleteConversation: string;
19
  editConversationTitle: { id: string; title: string };
20
  }>();
21
+
22
+ const settings = useSettingsStore();
23
  </script>
24
 
25
  <a
 
33
  ? 'bg-gray-100 dark:bg-gray-700'
34
  : ''}"
35
  >
36
+ <div class="flex flex-1 items-center truncate">
37
  {#if confirmDelete}
38
+ <span class="mr-1 font-semibold"> Delete </span>
39
+ {/if}
40
+ {#if conv.avatarHash && !$settings.hideEmojiOnSidebar}
41
+ <img
42
+ src="{base}/settings/assistants/{conv.assistantId}/avatar?hash={conv.avatarHash}"
43
+ alt="Assistant avatar"
44
+ class="mr-1.5 inline size-4 rounded-full object-cover"
45
+ />
46
+ {conv.title.replace(/\p{Emoji}/gu, "")}
47
+ {:else if conv.assistantId}
48
+ <div
49
+ class="mr-1.5 flex size-4 items-center justify-center rounded-full bg-gray-300 text-xs font-bold uppercase text-gray-500"
50
+ />
51
+ {conv.title.replace(/\p{Emoji}/gu, "")}
52
+ {:else}
53
+ {conv.title}
54
  {/if}
 
55
  </div>
56
 
57
  {#if confirmDelete}
src/lib/components/NavMenu.svelte CHANGED
@@ -7,14 +7,9 @@
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
- interface Conv {
12
- id: string;
13
- title: string;
14
- updatedAt: Date;
15
- }
16
-
17
- export let conversations: Array<Conv> = [];
18
  export let canLogin: boolean;
19
  export let user: LayoutData["user"];
20
 
 
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
+ import type { ConvSidebar } from "$lib/types/ConvSidebar";
11
 
12
+ export let conversations: ConvSidebar[] = [];
 
 
 
 
 
 
13
  export let canLogin: boolean;
14
  export let user: LayoutData["user"];
15
 
src/lib/components/chat/AssistantIntroduction.svelte ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { createEventDispatcher } from "svelte";
3
+ import IconGear from "~icons/bi/gear-fill";
4
+ import { base } from "$app/paths";
5
+ import type { Assistant } from "$lib/types/Assistant";
6
+
7
+ export let assistant: Pick<
8
+ Assistant,
9
+ "avatar" | "name" | "modelId" | "createdByName" | "exampleInputs" | "_id" | "description"
10
+ >;
11
+
12
+ const dispatch = createEventDispatcher<{ message: string }>();
13
+ </script>
14
+
15
+ <div class="flex h-full w-full flex-col content-center items-center justify-center">
16
+ <div
17
+ class="relative mt-auto rounded-2xl bg-gray-100 text-gray-600 dark:border-gray-800 dark:bg-gray-800/60 dark:text-gray-300"
18
+ >
19
+ <div class="flex items-center gap-4 p-4 pr-10 md:p-8 md:pt-10">
20
+ {#if assistant.avatar}
21
+ <img
22
+ src={`${base}/settings/assistants/${assistant._id.toString()}/avatar?hash=${
23
+ assistant.avatar
24
+ }`}
25
+ alt="avatar"
26
+ class="size-16 rounded-full object-cover md:size-32"
27
+ />
28
+ {:else}
29
+ <div
30
+ class="flex size-12 flex-none items-center justify-center rounded-full bg-gray-300 object-cover text-xl font-bold uppercase text-gray-500 sm:text-4xl md:h-32 md:w-32 dark:bg-gray-600"
31
+ >
32
+ {assistant?.name[0]}
33
+ </div>
34
+ {/if}
35
+
36
+ <div class="flex h-full flex-col">
37
+ <p
38
+ class="mb-2 w-fit truncate text-ellipsis rounded-full bg-gray-200 px-3 py-1 text-xs text-gray-600 dark:bg-gray-700 dark:text-gray-400"
39
+ >
40
+ Assistant
41
+ </p>
42
+ <p class="text-xl font-bold sm:text-2xl">{assistant.name}</p>
43
+ <p class="text-sm text-gray-500 dark:text-gray-400">
44
+ {assistant.description}
45
+ </p>
46
+
47
+ {#if assistant.createdByName}
48
+ <p class="pt-2 text-sm text-gray-400 dark:text-gray-500">
49
+ Created by <a
50
+ class="hover:underline"
51
+ href="https://hf.co/{assistant.createdByName}"
52
+ target="_blank"
53
+ >
54
+ {assistant.createdByName}
55
+ </a>
56
+ </p>
57
+ {/if}
58
+ </div>
59
+ </div>
60
+ <div class="absolute right-2 top-3 sm:top-2">
61
+ <a
62
+ href="{base}/settings/assistants/{assistant._id.toString()}"
63
+ class="flex size-7 items-center justify-center rounded-full border bg-gray-200 p-1 text-xs hover:bg-gray-100 dark:border-gray-700 dark:bg-gray-700 dark:hover:bg-gray-600"
64
+ ><IconGear /></a
65
+ >
66
+ </div>
67
+ </div>
68
+ {#if assistant.exampleInputs}
69
+ <div class="mx-auto mt-auto w-full gap-8 sm:-mb-8">
70
+ <div class="md:col-span-2 md:mt-6">
71
+ <div class="grid grid-cols-1 gap-3 md:grid-cols-2">
72
+ {#each assistant.exampleInputs as example}
73
+ <button
74
+ type="button"
75
+ class="truncate whitespace-nowrap rounded-xl border bg-gray-50 px-3 py-2 text-left text-smd text-gray-600 hover:bg-gray-100 dark:border-gray-800 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700"
76
+ on:click={() => dispatch("message", example)}
77
+ >
78
+ {example}
79
+ </button>
80
+ {/each}
81
+ </div>
82
+ </div>
83
+ </div>
84
+ {/if}
85
+ </div>
src/lib/components/chat/ChatMessages.svelte CHANGED
@@ -10,12 +10,17 @@
10
  import type { WebSearchUpdate } from "$lib/types/MessageUpdate";
11
  import { browser } from "$app/environment";
12
  import SystemPromptModal from "../SystemPromptModal.svelte";
 
 
 
 
13
 
14
  export let messages: Message[];
15
  export let loading: boolean;
16
  export let pending: boolean;
17
  export let isAuthor: boolean;
18
  export let currentModel: Model;
 
19
  export let models: Model[];
20
  export let preprompt: string | undefined;
21
  export let readOnly: boolean;
@@ -42,7 +47,29 @@
42
  >
43
  <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">
44
  {#each messages as message, i}
45
- {#if i === 0 && preprompt && preprompt != currentModel.preprompt}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  <SystemPromptModal preprompt={preprompt ?? ""} />
47
  {/if}
48
  <ChatMessage
@@ -57,7 +84,11 @@
57
  on:continue
58
  />
59
  {:else}
60
- <ChatIntroduction {models} {currentModel} on:message />
 
 
 
 
61
  {/each}
62
  {#if pending && messages[messages.length - 1]?.from === "user"}
63
  <ChatMessage
 
10
  import type { WebSearchUpdate } from "$lib/types/MessageUpdate";
11
  import { browser } from "$app/environment";
12
  import SystemPromptModal from "../SystemPromptModal.svelte";
13
+ import type { Assistant } from "$lib/types/Assistant";
14
+ import AssistantIntroduction from "./AssistantIntroduction.svelte";
15
+ import { page } from "$app/stores";
16
+ import { base } from "$app/paths";
17
 
18
  export let messages: Message[];
19
  export let loading: boolean;
20
  export let pending: boolean;
21
  export let isAuthor: boolean;
22
  export let currentModel: Model;
23
+ export let assistant: Assistant | undefined;
24
  export let models: Model[];
25
  export let preprompt: string | undefined;
26
  export let readOnly: boolean;
 
47
  >
48
  <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">
49
  {#each messages as message, i}
50
+ {#if i === 0 && $page.data?.assistant}
51
+ <a
52
+ class="mx-auto flex items-center gap-1.5 rounded-full border border-gray-100 bg-gray-50 py-1 pl-1 pr-3 text-sm text-gray-800 hover:bg-gray-100 dark:border-gray-800 dark:bg-gray-800 dark:text-gray-200 dark:hover:bg-gray-700"
53
+ href="{base}/settings/assistants/{$page.data.assistant._id}"
54
+ >
55
+ {#if $page.data?.assistant.avatar}
56
+ <img
57
+ src="{base}/settings/assistants/{$page.data?.assistant._id.toString()}/avatar?hash=${$page
58
+ .data?.assistant.avatar}"
59
+ alt="Avatar"
60
+ class="size-5 rounded-full object-cover"
61
+ />
62
+ {:else}
63
+ <div
64
+ class="flex size-6 items-center justify-center rounded-full bg-gray-300 font-bold uppercase text-gray-500"
65
+ >
66
+ {$page.data?.assistant.name[0]}
67
+ </div>
68
+ {/if}
69
+
70
+ {$page.data.assistant.name}
71
+ </a>
72
+ {:else if i === 0 && preprompt && preprompt != currentModel.preprompt}
73
  <SystemPromptModal preprompt={preprompt ?? ""} />
74
  {/if}
75
  <ChatMessage
 
84
  on:continue
85
  />
86
  {:else}
87
+ {#if !assistant}
88
+ <ChatIntroduction {models} {currentModel} on:message />
89
+ {:else}
90
+ <AssistantIntroduction {assistant} on:message />
91
+ {/if}
92
  {/each}
93
  {#if pending && messages[messages.length - 1]?.from === "user"}
94
  <ChatMessage
src/lib/components/chat/ChatWindow.svelte CHANGED
@@ -18,12 +18,12 @@
18
  import LoginModal from "../LoginModal.svelte";
19
  import type { WebSearchUpdate } from "$lib/types/MessageUpdate";
20
  import { page } from "$app/stores";
21
- import DisclaimerModal from "../DisclaimerModal.svelte";
22
  import FileDropzone from "./FileDropzone.svelte";
23
  import RetryBtn from "../RetryBtn.svelte";
24
  import UploadBtn from "../UploadBtn.svelte";
25
  import file2base64 from "$lib/utils/file2base64";
26
- import { useSettingsStore } from "$lib/stores/settings";
 
27
  import ContinueBtn from "../ContinueBtn.svelte";
28
 
29
  export let messages: Message[] = [];
@@ -32,6 +32,7 @@
32
  export let shared = false;
33
  export let currentModel: Model;
34
  export let models: Model[];
 
35
  export let webSearchMessages: WebSearchUpdate[] = [];
36
  export let preprompt: string | undefined = undefined;
37
  export let files: File[] = [];
@@ -78,8 +79,6 @@
78
 
79
  $: sources = files.map((file) => file2base64(file));
80
 
81
- const settings = useSettingsStore();
82
-
83
  function onShare() {
84
  dispatch("share");
85
  isSharedRecently = true;
@@ -99,9 +98,7 @@
99
  </script>
100
 
101
  <div class="relative min-h-0 min-w-0">
102
- {#if !$settings.ethicsModalAccepted}
103
- <DisclaimerModal />
104
- {:else if loginModalOpen}
105
  <LoginModal
106
  on:close={() => {
107
  loginModalOpen = false;
@@ -113,6 +110,7 @@
113
  {pending}
114
  {currentModel}
115
  {models}
 
116
  {messages}
117
  readOnly={isReadOnly}
118
  isAuthor={!shared}
@@ -162,7 +160,7 @@
162
 
163
  <div class="w-full">
164
  <div class="flex w-full pb-3">
165
- {#if $page.data.settings?.searchEnabled}
166
  <WebSearchToggle />
167
  {/if}
168
  {#if loading}
@@ -252,13 +250,16 @@
252
  class="mt-2 flex justify-between self-stretch px-1 text-xs text-gray-400/90 max-md:mb-2 max-sm:gap-2"
253
  >
254
  <p>
255
- Model: <a
256
- href={currentModel.modelUrl || "https://huggingface.co/" + currentModel.name}
257
- target="_blank"
258
- rel="noreferrer"
259
- class="hover:underline">{currentModel.displayName}</a
260
- > <span class="max-sm:hidden">·</span><br class="sm:hidden" /> Generated content may be inaccurate
261
- or false.
 
 
 
262
  </p>
263
  {#if messages.length}
264
  <button
 
18
  import LoginModal from "../LoginModal.svelte";
19
  import type { WebSearchUpdate } from "$lib/types/MessageUpdate";
20
  import { page } from "$app/stores";
 
21
  import FileDropzone from "./FileDropzone.svelte";
22
  import RetryBtn from "../RetryBtn.svelte";
23
  import UploadBtn from "../UploadBtn.svelte";
24
  import file2base64 from "$lib/utils/file2base64";
25
+ import type { Assistant } from "$lib/types/Assistant";
26
+ import { base } from "$app/paths";
27
  import ContinueBtn from "../ContinueBtn.svelte";
28
 
29
  export let messages: Message[] = [];
 
32
  export let shared = false;
33
  export let currentModel: Model;
34
  export let models: Model[];
35
+ export let assistant: Assistant | undefined = undefined;
36
  export let webSearchMessages: WebSearchUpdate[] = [];
37
  export let preprompt: string | undefined = undefined;
38
  export let files: File[] = [];
 
79
 
80
  $: sources = files.map((file) => file2base64(file));
81
 
 
 
82
  function onShare() {
83
  dispatch("share");
84
  isSharedRecently = true;
 
98
  </script>
99
 
100
  <div class="relative min-h-0 min-w-0">
101
+ {#if loginModalOpen}
 
 
102
  <LoginModal
103
  on:close={() => {
104
  loginModalOpen = false;
 
110
  {pending}
111
  {currentModel}
112
  {models}
113
+ {assistant}
114
  {messages}
115
  readOnly={isReadOnly}
116
  isAuthor={!shared}
 
160
 
161
  <div class="w-full">
162
  <div class="flex w-full pb-3">
163
+ {#if $page.data.settings?.searchEnabled && !assistant}
164
  <WebSearchToggle />
165
  {/if}
166
  {#if loading}
 
250
  class="mt-2 flex justify-between self-stretch px-1 text-xs text-gray-400/90 max-md:mb-2 max-sm:gap-2"
251
  >
252
  <p>
253
+ Model:
254
+ {#if !assistant}
255
+ <a href="{base}/settings/{currentModel.id}" class="hover:underline"
256
+ >{currentModel.displayName}</a
257
+ >{:else}
258
+ {@const model = models.find((m) => m.id === assistant?.modelId)}
259
+ <a href="{base}/settings/assistants/{assistant._id}" class="hover:underline"
260
+ >{model?.displayName}</a
261
+ >{/if} <span class="max-sm:hidden">·</span><br class="sm:hidden" /> Generated content may
262
+ be inaccurate or false.
263
  </p>
264
  {#if messages.length}
265
  <button
src/lib/server/database.ts CHANGED
@@ -7,6 +7,8 @@ import type { Settings } from "$lib/types/Settings";
7
  import type { User } from "$lib/types/User";
8
  import type { MessageEvent } from "$lib/types/MessageEvent";
9
  import type { Session } from "$lib/types/Session";
 
 
10
 
11
  if (!MONGODB_URL) {
12
  throw new Error(
@@ -23,6 +25,8 @@ export const connectPromise = client.connect().catch(console.error);
23
  const db = client.db(MONGODB_DB_NAME + (import.meta.env.MODE === "test" ? "-test" : ""));
24
 
25
  const conversations = db.collection<Conversation>("conversations");
 
 
26
  const sharedConversations = db.collection<SharedConversation>("sharedConversations");
27
  const abortedGenerations = db.collection<AbortedGeneration>("abortedGenerations");
28
  const settings = db.collection<Settings>("settings");
@@ -34,6 +38,8 @@ const bucket = new GridFSBucket(db, { bucketName: "files" });
34
  export { client, db };
35
  export const collections = {
36
  conversations,
 
 
37
  sharedConversations,
38
  abortedGenerations,
39
  settings,
@@ -66,4 +72,6 @@ client.on("open", () => {
66
  messageEvents.createIndex({ createdAt: 1 }, { expireAfterSeconds: 60 }).catch(console.error);
67
  sessions.createIndex({ expiresAt: 1 }, { expireAfterSeconds: 0 }).catch(console.error);
68
  sessions.createIndex({ sessionId: 1 }, { unique: true }).catch(console.error);
 
 
69
  });
 
7
  import type { User } from "$lib/types/User";
8
  import type { MessageEvent } from "$lib/types/MessageEvent";
9
  import type { Session } from "$lib/types/Session";
10
+ import type { Assistant } from "$lib/types/Assistant";
11
+ import type { Report } from "$lib/types/Report";
12
 
13
  if (!MONGODB_URL) {
14
  throw new Error(
 
25
  const db = client.db(MONGODB_DB_NAME + (import.meta.env.MODE === "test" ? "-test" : ""));
26
 
27
  const conversations = db.collection<Conversation>("conversations");
28
+ const assistants = db.collection<Assistant>("assistants");
29
+ const reports = db.collection<Report>("reports");
30
  const sharedConversations = db.collection<SharedConversation>("sharedConversations");
31
  const abortedGenerations = db.collection<AbortedGeneration>("abortedGenerations");
32
  const settings = db.collection<Settings>("settings");
 
38
  export { client, db };
39
  export const collections = {
40
  conversations,
41
+ assistants,
42
+ reports,
43
  sharedConversations,
44
  abortedGenerations,
45
  settings,
 
72
  messageEvents.createIndex({ createdAt: 1 }, { expireAfterSeconds: 60 }).catch(console.error);
73
  sessions.createIndex({ expiresAt: 1 }, { expireAfterSeconds: 0 }).catch(console.error);
74
  sessions.createIndex({ sessionId: 1 }, { unique: true }).catch(console.error);
75
+ assistants.createIndex({ createdBy: 1 }).catch(console.error);
76
+ reports.createIndex({ assistantId: 1 }).catch(console.error);
77
  });
src/lib/stores/settings.ts CHANGED
@@ -2,6 +2,7 @@ import { browser } from "$app/environment";
2
  import { invalidate } from "$app/navigation";
3
  import { base } from "$app/paths";
4
  import { UrlDependency } from "$lib/types/UrlDependency";
 
5
  import { getContext, setContext } from "svelte";
6
  import { type Writable, writable, get } from "svelte/store";
7
 
@@ -13,7 +14,9 @@ type SettingsStore = {
13
  activeModel: string;
14
  customPrompts: Record<string, string>;
15
  recentlySaved: boolean;
 
16
  };
 
17
  export function useSettingsStore() {
18
  return getContext<Writable<SettingsStore>>("settings");
19
  }
@@ -44,6 +47,7 @@ export function createSettingsStore(initialValue: Omit<SettingsStore, "recentlyS
44
  }),
45
  });
46
 
 
47
  // set savedRecently to true for 3s
48
  baseStore.update((s) => ({
49
  ...s,
 
2
  import { invalidate } from "$app/navigation";
3
  import { base } from "$app/paths";
4
  import { UrlDependency } from "$lib/types/UrlDependency";
5
+ import type { ObjectId } from "mongodb";
6
  import { getContext, setContext } from "svelte";
7
  import { type Writable, writable, get } from "svelte/store";
8
 
 
14
  activeModel: string;
15
  customPrompts: Record<string, string>;
16
  recentlySaved: boolean;
17
+ assistants: Array<ObjectId | string>;
18
  };
19
+
20
  export function useSettingsStore() {
21
  return getContext<Writable<SettingsStore>>("settings");
22
  }
 
47
  }),
48
  });
49
 
50
+ invalidate(UrlDependency.ConversationList);
51
  // set savedRecently to true for 3s
52
  baseStore.update((s) => ({
53
  ...s,
src/lib/types/Assistant.ts ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { ObjectId } from "mongodb";
2
+ import type { User } from "./User";
3
+ import type { Timestamps } from "./Timestamps";
4
+
5
+ export interface Assistant extends Timestamps {
6
+ _id: ObjectId;
7
+ createdById: User["_id"] | string; // user id or session
8
+ createdByName?: User["username"];
9
+ avatar?: string;
10
+ name: string;
11
+ description?: string;
12
+ modelId: string;
13
+ exampleInputs: string[];
14
+ preprompt: string;
15
+ }
src/lib/types/ConvSidebar.ts ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ export interface ConvSidebar {
2
+ id: string;
3
+ title: string;
4
+ updatedAt: Date;
5
+ model?: string;
6
+ assistantId?: string;
7
+ avatarHash?: string;
8
+ }
src/lib/types/Conversation.ts CHANGED
@@ -2,6 +2,7 @@ import type { ObjectId } from "mongodb";
2
  import type { Message } from "./Message";
3
  import type { Timestamps } from "./Timestamps";
4
  import type { User } from "./User";
 
5
 
6
  export interface Conversation extends Timestamps {
7
  _id: ObjectId;
@@ -20,4 +21,5 @@ export interface Conversation extends Timestamps {
20
  };
21
 
22
  preprompt?: string;
 
23
  }
 
2
  import type { Message } from "./Message";
3
  import type { Timestamps } from "./Timestamps";
4
  import type { User } from "./User";
5
+ import type { Assistant } from "./Assistant";
6
 
7
  export interface Conversation extends Timestamps {
8
  _id: ObjectId;
 
21
  };
22
 
23
  preprompt?: string;
24
+ assistantId?: Assistant["_id"];
25
  }
src/lib/types/Report.ts ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { ObjectId } from "mongodb";
2
+ import type { User } from "./User";
3
+ import type { Assistant } from "./Assistant";
4
+ import type { Timestamps } from "./Timestamps";
5
+
6
+ export interface Report extends Timestamps {
7
+ _id: ObjectId;
8
+ createdBy: User["_id"] | string;
9
+ assistantId: Assistant["_id"];
10
+ }
src/lib/types/Settings.ts CHANGED
@@ -1,4 +1,5 @@
1
  import { defaultModel } from "$lib/server/models";
 
2
  import type { Timestamps } from "./Timestamps";
3
  import type { User } from "./User";
4
 
@@ -18,6 +19,8 @@ export interface Settings extends Timestamps {
18
 
19
  // model name and system prompts
20
  customPrompts?: Record<string, string>;
 
 
21
  }
22
 
23
  // TODO: move this to a constant file along with other constants
@@ -25,4 +28,6 @@ export const DEFAULT_SETTINGS = {
25
  shareConversationsWithModelAuthors: true,
26
  activeModel: defaultModel.id,
27
  hideEmojiOnSidebar: false,
 
 
28
  };
 
1
  import { defaultModel } from "$lib/server/models";
2
+ import type { Assistant } from "./Assistant";
3
  import type { Timestamps } from "./Timestamps";
4
  import type { User } from "./User";
5
 
 
19
 
20
  // model name and system prompts
21
  customPrompts?: Record<string, string>;
22
+
23
+ assistants?: Assistant["_id"][];
24
  }
25
 
26
  // TODO: move this to a constant file along with other constants
 
28
  shareConversationsWithModelAuthors: true,
29
  activeModel: defaultModel.id,
30
  hideEmojiOnSidebar: false,
31
+ customPrompts: {},
32
+ assistants: [],
33
  };
src/lib/types/SharedConversation.ts CHANGED
@@ -1,3 +1,4 @@
 
1
  import type { Message } from "./Message";
2
  import type { Timestamps } from "./Timestamps";
3
 
@@ -12,4 +13,5 @@ export interface SharedConversation extends Timestamps {
12
  title: string;
13
  messages: Message[];
14
  preprompt?: string;
 
15
  }
 
1
+ import type { Assistant } from "./Assistant";
2
  import type { Message } from "./Message";
3
  import type { Timestamps } from "./Timestamps";
4
 
 
13
  title: string;
14
  messages: Message[];
15
  preprompt?: string;
16
+ assistantId?: Assistant["_id"];
17
  }
src/lib/utils/timeout.ts ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ export const timeout = <T>(prom: Promise<T>, time: number): Promise<T> => {
2
+ let timer: NodeJS.Timeout;
3
+ return Promise.race([prom, new Promise<T>((_r, rej) => (timer = setTimeout(rej, time)))]).finally(
4
+ () => clearTimeout(timer)
5
+ );
6
+ };
src/routes/+layout.server.ts CHANGED
@@ -12,16 +12,22 @@ import {
12
  MESSAGES_BEFORE_LOGIN,
13
  YDC_API_KEY,
14
  USE_LOCAL_WEBSEARCH,
 
15
  } from "$env/static/private";
 
 
16
 
17
  export const load: LayoutServerLoad = async ({ locals, depends }) => {
18
- const { conversations } = collections;
19
  depends(UrlDependency.ConversationList);
20
 
21
  const settings = await collections.settings.findOne(authCondition(locals));
22
 
23
  // If the active model in settings is not valid, set it to the default model. This can happen if model was disabled.
24
- if (settings && !validateModel(models).safeParse(settings?.activeModel).success) {
 
 
 
 
25
  settings.activeModel = defaultModel.id;
26
  await collections.settings.updateOne(authCondition(locals), {
27
  $set: { activeModel: defaultModel.id },
@@ -42,7 +48,7 @@ export const load: LayoutServerLoad = async ({ locals, depends }) => {
42
  // get the number of messages where `from === "assistant"` across all conversations.
43
  const totalMessages =
44
  (
45
- await conversations
46
  .aggregate([
47
  { $match: authCondition(locals) },
48
  { $project: { messages: 1 } },
@@ -59,33 +65,61 @@ export const load: LayoutServerLoad = async ({ locals, depends }) => {
59
 
60
  const loginRequired = requiresUser && !locals.user && userHasExceededMessages;
61
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
  return {
63
- conversations: await conversations
64
- .find(authCondition(locals))
65
- .sort({ updatedAt: -1 })
66
- .project<Pick<Conversation, "title" | "model" | "_id" | "updatedAt" | "createdAt">>({
67
- title: 1,
68
- model: 1,
69
- _id: 1,
70
- updatedAt: 1,
71
- createdAt: 1,
72
- })
73
- .map((conv) => {
74
- // remove emojis if settings say so
75
- if (settings?.hideEmojiOnSidebar) {
76
- conv.title = conv.title.replace(/\p{Emoji}/gu, "");
77
- }
78
-
79
- // remove invalid unicode and trim whitespaces
80
- conv.title = conv.title.replace(/\uFFFD/gu, "").trimStart();
81
- return {
82
- id: conv._id.toString(),
83
- title: settings?.hideEmojiOnSidebar ? conv.title.replace(/\p{Emoji}/gu, "") : conv.title,
84
- model: conv.model ?? defaultModel,
85
- updatedAt: conv.updatedAt,
86
- };
87
- })
88
- .toArray(),
89
  settings: {
90
  searchEnabled: !!(
91
  SERPAPI_KEY ||
@@ -102,6 +136,7 @@ export const load: LayoutServerLoad = async ({ locals, depends }) => {
102
  settings?.shareConversationsWithModelAuthors ??
103
  DEFAULT_SETTINGS.shareConversationsWithModelAuthors,
104
  customPrompts: settings?.customPrompts ?? {},
 
105
  },
106
  models: models.map((model) => ({
107
  id: model.id,
@@ -120,10 +155,13 @@ export const load: LayoutServerLoad = async ({ locals, depends }) => {
120
  })),
121
  oldModels,
122
  user: locals.user && {
 
123
  username: locals.user.username,
124
  avatarUrl: locals.user.avatarUrl,
125
  email: locals.user.email,
126
  },
 
 
127
  loginRequired,
128
  loginEnabled: requiresUser,
129
  guestMode: requiresUser && messagesBeforeLogin > 0,
 
12
  MESSAGES_BEFORE_LOGIN,
13
  YDC_API_KEY,
14
  USE_LOCAL_WEBSEARCH,
15
+ ENABLE_ASSISTANTS,
16
  } from "$env/static/private";
17
+ import { ObjectId } from "mongodb";
18
+ import type { ConvSidebar } from "$lib/types/ConvSidebar";
19
 
20
  export const load: LayoutServerLoad = async ({ locals, depends }) => {
 
21
  depends(UrlDependency.ConversationList);
22
 
23
  const settings = await collections.settings.findOne(authCondition(locals));
24
 
25
  // If the active model in settings is not valid, set it to the default model. This can happen if model was disabled.
26
+ if (
27
+ settings &&
28
+ !validateModel(models).safeParse(settings?.activeModel).success &&
29
+ !settings.assistants?.map((el) => el.toString())?.includes(settings?.activeModel)
30
+ ) {
31
  settings.activeModel = defaultModel.id;
32
  await collections.settings.updateOne(authCondition(locals), {
33
  $set: { activeModel: defaultModel.id },
 
48
  // get the number of messages where `from === "assistant"` across all conversations.
49
  const totalMessages =
50
  (
51
+ await collections.conversations
52
  .aggregate([
53
  { $match: authCondition(locals) },
54
  { $project: { messages: 1 } },
 
65
 
66
  const loginRequired = requiresUser && !locals.user && userHasExceededMessages;
67
 
68
+ const enableAssistants = ENABLE_ASSISTANTS === "true";
69
+
70
+ const assistantActive = !models.map(({ id }) => id).includes(settings?.activeModel ?? "");
71
+
72
+ const assistant = assistantActive
73
+ ? JSON.parse(
74
+ JSON.stringify(
75
+ await collections.assistants.findOne({
76
+ _id: new ObjectId(settings?.activeModel),
77
+ })
78
+ )
79
+ )
80
+ : null;
81
+
82
+ const conversations = await collections.conversations
83
+ .find(authCondition(locals))
84
+ .sort({ updatedAt: -1 })
85
+ .project<
86
+ Pick<Conversation, "title" | "model" | "_id" | "updatedAt" | "createdAt" | "assistantId">
87
+ >({
88
+ title: 1,
89
+ model: 1,
90
+ _id: 1,
91
+ updatedAt: 1,
92
+ createdAt: 1,
93
+ assistantId: 1,
94
+ })
95
+ .toArray();
96
+
97
+ const assistantIds = conversations
98
+ .map((conv) => conv.assistantId)
99
+ .filter((el) => !!el) as ObjectId[];
100
+
101
+ const assistants = await collections.assistants.find({ _id: { $in: assistantIds } }).toArray();
102
+
103
  return {
104
+ conversations: conversations.map((conv) => {
105
+ if (settings?.hideEmojiOnSidebar) {
106
+ conv.title = conv.title.replace(/\p{Emoji}/gu, "");
107
+ }
108
+
109
+ // remove invalid unicode and trim whitespaces
110
+ conv.title = conv.title.replace(/\uFFFD/gu, "").trimStart();
111
+
112
+ return {
113
+ id: conv._id.toString(),
114
+ title: conv.title,
115
+ model: conv.model ?? defaultModel,
116
+ updatedAt: conv.updatedAt,
117
+ assistantId: conv.assistantId?.toString(),
118
+ avatarHash:
119
+ conv.assistantId &&
120
+ assistants.find((a) => a._id.toString() === conv.assistantId?.toString())?.avatar,
121
+ };
122
+ }) satisfies ConvSidebar[],
 
 
 
 
 
 
 
123
  settings: {
124
  searchEnabled: !!(
125
  SERPAPI_KEY ||
 
136
  settings?.shareConversationsWithModelAuthors ??
137
  DEFAULT_SETTINGS.shareConversationsWithModelAuthors,
138
  customPrompts: settings?.customPrompts ?? {},
139
+ assistants: settings?.assistants?.map((el) => el.toString()) ?? [],
140
  },
141
  models: models.map((model) => ({
142
  id: model.id,
 
155
  })),
156
  oldModels,
157
  user: locals.user && {
158
+ id: locals.user._id.toString(),
159
  username: locals.user.username,
160
  avatarUrl: locals.user.avatarUrl,
161
  email: locals.user.email,
162
  },
163
+ assistant,
164
+ enableAssistants,
165
  loginRequired,
166
  loginEnabled: requiresUser,
167
  guestMode: requiresUser && messagesBeforeLogin > 0,
src/routes/+layout.svelte CHANGED
@@ -4,7 +4,7 @@
4
  import { page } from "$app/stores";
5
  import "../styles/main.css";
6
  import { base } from "$app/paths";
7
- import { PUBLIC_ORIGIN } from "$env/static/public";
8
 
9
  import { shareConversation } from "$lib/shareConversation";
10
  import { UrlDependency } from "$lib/types/UrlDependency";
@@ -17,6 +17,7 @@
17
  import titleUpdate from "$lib/stores/titleUpdate";
18
  import { createSettingsStore } from "$lib/stores/settings";
19
  import { browser } from "$app/environment";
 
20
 
21
  export let data;
22
 
@@ -120,13 +121,19 @@
120
  <meta name="description" content="The first open source alternative to ChatGPT. 💪" />
121
  <meta name="twitter:card" content="summary_large_image" />
122
  <meta name="twitter:site" content="@huggingface" />
123
- <meta property="og:title" content={PUBLIC_APP_NAME} />
124
- <meta property="og:type" content="website" />
125
- <meta property="og:url" content="{PUBLIC_ORIGIN || $page.url.origin}{base}" />
126
- <meta
127
- property="og:image"
128
- content="{PUBLIC_ORIGIN || $page.url.origin}{base}/{PUBLIC_APP_ASSETS}/thumbnail.png"
129
- />
 
 
 
 
 
 
130
  <link
131
  rel="icon"
132
  href="{PUBLIC_ORIGIN || $page.url.origin}{base}/{PUBLIC_APP_ASSETS}/favicon.ico"
@@ -147,6 +154,10 @@
147
  />
148
  </svelte:head>
149
 
 
 
 
 
150
  <div
151
  class="grid h-full w-screen grid-cols-1 grid-rows-[auto,1fr] overflow-hidden text-smd md:grid-cols-[280px,1fr] md:grid-rows-[1fr] dark:text-gray-300"
152
  >
 
4
  import { page } from "$app/stores";
5
  import "../styles/main.css";
6
  import { base } from "$app/paths";
7
+ import { PUBLIC_APP_DESCRIPTION, PUBLIC_ORIGIN } from "$env/static/public";
8
 
9
  import { shareConversation } from "$lib/shareConversation";
10
  import { UrlDependency } from "$lib/types/UrlDependency";
 
17
  import titleUpdate from "$lib/stores/titleUpdate";
18
  import { createSettingsStore } from "$lib/stores/settings";
19
  import { browser } from "$app/environment";
20
+ import DisclaimerModal from "$lib/components/DisclaimerModal.svelte";
21
 
22
  export let data;
23
 
 
121
  <meta name="description" content="The first open source alternative to ChatGPT. 💪" />
122
  <meta name="twitter:card" content="summary_large_image" />
123
  <meta name="twitter:site" content="@huggingface" />
124
+
125
+ <!-- use those meta tags everywhere except on the share assistant page -->
126
+ <!-- feel free to refacto if there's a better way -->
127
+ {#if !$page.url.pathname.includes("/assistant/")}
128
+ <meta property="og:title" content={PUBLIC_APP_NAME} />
129
+ <meta property="og:type" content="website" />
130
+ <meta property="og:url" content="{PUBLIC_ORIGIN || $page.url.origin}{base}" />
131
+ <meta
132
+ property="og:image"
133
+ content="{PUBLIC_ORIGIN || $page.url.origin}{base}/{PUBLIC_APP_ASSETS}/thumbnail.png"
134
+ />
135
+ <meta property="og:description" content={PUBLIC_APP_DESCRIPTION} />
136
+ {/if}
137
  <link
138
  rel="icon"
139
  href="{PUBLIC_ORIGIN || $page.url.origin}{base}/{PUBLIC_APP_ASSETS}/favicon.ico"
 
154
  />
155
  </svelte:head>
156
 
157
+ {#if !$settings.ethicsModalAccepted}
158
+ <DisclaimerModal />
159
+ {/if}
160
+
161
  <div
162
  class="grid h-full w-screen grid-cols-1 grid-rows-[auto,1fr] overflow-hidden text-smd md:grid-cols-[280px,1fr] md:grid-rows-[1fr] dark:text-gray-300"
163
  >
src/routes/+page.svelte CHANGED
@@ -17,14 +17,32 @@
17
  async function createConversation(message: string) {
18
  try {
19
  loading = true;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  const res = await fetch(`${base}/conversation`, {
21
  method: "POST",
22
  headers: {
23
  "Content-Type": "application/json",
24
  },
25
  body: JSON.stringify({
26
- model: $settings.activeModel,
27
  preprompt: $settings.customPrompts[$settings.activeModel],
 
28
  }),
29
  });
30
 
@@ -60,6 +78,7 @@
60
  <ChatWindow
61
  on:message={(ev) => createConversation(ev.detail)}
62
  {loading}
 
63
  currentModel={findCurrentModel([...data.models, ...data.oldModels], $settings.activeModel)}
64
  models={data.models}
65
  bind:files
 
17
  async function createConversation(message: string) {
18
  try {
19
  loading = true;
20
+
21
+ // check if $settings.activeModel is a valid model
22
+ // else check if it's an assistant, and use that model
23
+ // else use the first model
24
+
25
+ const validModels = data.models.map((model) => model.id);
26
+
27
+ let model;
28
+ if (validModels.includes($settings.activeModel)) {
29
+ model = $settings.activeModel;
30
+ } else {
31
+ if (validModels.includes(data.assistant?.modelId)) {
32
+ model = data.assistant?.modelId;
33
+ } else {
34
+ model = data.models[0].id;
35
+ }
36
+ }
37
  const res = await fetch(`${base}/conversation`, {
38
  method: "POST",
39
  headers: {
40
  "Content-Type": "application/json",
41
  },
42
  body: JSON.stringify({
43
+ model,
44
  preprompt: $settings.customPrompts[$settings.activeModel],
45
+ assistantId: data.assistant?._id,
46
  }),
47
  });
48
 
 
78
  <ChatWindow
79
  on:message={(ev) => createConversation(ev.detail)}
80
  {loading}
81
+ assistant={data.assistant}
82
  currentModel={findCurrentModel([...data.models, ...data.oldModels], $settings.activeModel)}
83
  models={data.models}
84
  bind:files
src/routes/assistant/[assistantId]/+page.server.ts ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { base } from "$app/paths";
2
+ import { collections } from "$lib/server/database.js";
3
+ import { redirect } from "@sveltejs/kit";
4
+ import { ObjectId } from "mongodb";
5
+
6
+ export const load = async ({ params }) => {
7
+ try {
8
+ const assistant = await collections.assistants.findOne({
9
+ _id: new ObjectId(params.assistantId),
10
+ });
11
+
12
+ if (!assistant) {
13
+ throw redirect(302, `${base}`);
14
+ }
15
+
16
+ return { assistant: JSON.parse(JSON.stringify(assistant)) };
17
+ } catch {
18
+ throw redirect(302, `${base}`);
19
+ }
20
+ };
src/routes/assistant/[assistantId]/+page.svelte ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { base } from "$app/paths";
3
+ import { clickOutside } from "$lib/actions/clickOutside";
4
+ import { afterNavigate, goto } from "$app/navigation";
5
+
6
+ import { useSettingsStore } from "$lib/stores/settings";
7
+ import type { PageData } from "./$types";
8
+ import { applyAction, enhance } from "$app/forms";
9
+ import { PUBLIC_APP_NAME, PUBLIC_ORIGIN } from "$env/static/public";
10
+ import { page } from "$app/stores";
11
+
12
+ export let data: PageData;
13
+
14
+ let previousPage: string = base;
15
+
16
+ afterNavigate(({ from }) => {
17
+ if (!from?.url.pathname.includes("settings")) {
18
+ previousPage = from?.url.pathname || previousPage;
19
+ }
20
+ });
21
+
22
+ const settings = useSettingsStore();
23
+ </script>
24
+
25
+ <svelte:head>
26
+ <meta property="og:title" content={data.assistant.name + " - " + PUBLIC_APP_NAME} />
27
+ <meta property="og:type" content="link" />
28
+ <meta
29
+ property="og:description"
30
+ content={`Use the ${data.assistant.name} assistant inside of ${PUBLIC_APP_NAME}`}
31
+ />
32
+ <meta
33
+ property="og:image"
34
+ content="{PUBLIC_ORIGIN || $page.url.origin}{base}/assistant/{data.assistant._id}/thumbnail.png"
35
+ />
36
+ <meta property="og:url" content={$page.url.href} />
37
+ <meta name="twitter:card" content="summary_large_image" />
38
+ </svelte:head>
39
+
40
+ <div
41
+ class="fixed inset-0 flex items-center justify-center bg-black/80 backdrop-blur-sm dark:bg-black/50"
42
+ >
43
+ <dialog
44
+ open
45
+ use:clickOutside={() => {
46
+ goto(previousPage);
47
+ }}
48
+ class="z-10 flex flex-col content-center items-center gap-x-10 gap-y-2 overflow-hidden rounded-2xl bg-white p-4 text-center shadow-2xl outline-none max-sm:px-6 md:w-96 md:grid-cols-3 md:grid-rows-[auto,1fr] md:p-8"
49
+ >
50
+ {#if data.assistant.avatar}
51
+ <img
52
+ class="h-24 w-24 rounded-full object-cover"
53
+ src="{base}/settings/assistants/{data.assistant._id}/avatar?hash={data.assistant.avatar}"
54
+ alt="avatar"
55
+ />
56
+ {:else}
57
+ <div
58
+ class="flex h-24 w-24 items-center justify-center rounded-full bg-gray-300 font-bold uppercase text-gray-500"
59
+ >
60
+ {data.assistant.name[0]}
61
+ </div>
62
+ {/if}
63
+ <h1 class="text-2xl font-bold">
64
+ {data.assistant.name}
65
+ </h1>
66
+ <h3 class="text-sm text-gray-700">
67
+ {data.assistant.description}
68
+ </h3>
69
+ {#if data.assistant.createdByName}
70
+ <p class="text-sm text-gray-500">
71
+ Created by <a
72
+ class="hover:underline"
73
+ href="https://hf.co/{data.assistant.createdByName}"
74
+ target="_blank"
75
+ >
76
+ {data.assistant.createdByName}
77
+ </a>
78
+ </p>
79
+ {/if}
80
+ <button
81
+ class="mt-4 w-full rounded-full bg-gray-200 px-4 py-2 font-semibold text-gray-700"
82
+ on:click={() => {
83
+ goto(previousPage);
84
+ }}
85
+ >
86
+ Cancel
87
+ </button>
88
+ <form
89
+ method="POST"
90
+ action="{base}/settings/assistants/{data.assistant._id}?/subscribe"
91
+ class="w-full"
92
+ use:enhance={() => {
93
+ return async ({ result }) => {
94
+ // `result` is an `ActionResult` object
95
+ if (result.type === "success") {
96
+ $settings.activeModel = data.assistant._id;
97
+ goto(`${base}`);
98
+ } else {
99
+ await applyAction(result);
100
+ }
101
+ };
102
+ }}
103
+ >
104
+ <button
105
+ type="submit"
106
+ class=" w-full rounded-full bg-black px-4 py-3 font-semibold text-white"
107
+ >
108
+ Start chatting
109
+ </button>
110
+ </form>
111
+ </dialog>
112
+ </div>
src/routes/assistant/[assistantId]/thumbnail.png/+server.ts ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { APP_BASE } from "$env/static/private";
2
+ import ChatThumbnail from "./ChatThumbnail.svelte";
3
+ import { collections } from "$lib/server/database";
4
+ import { error, type RequestHandler } from "@sveltejs/kit";
5
+ import { ObjectId } from "mongodb";
6
+ import type { SvelteComponent } from "svelte";
7
+
8
+ import { Resvg } from "@resvg/resvg-js";
9
+ import satori from "satori";
10
+ import { html } from "satori-html";
11
+ import { base } from "$app/paths";
12
+
13
+ export const GET: RequestHandler = (async ({ url, params, fetch }) => {
14
+ const assistant = await collections.assistants.findOne({
15
+ _id: new ObjectId(params.assistantId),
16
+ });
17
+
18
+ if (!assistant) {
19
+ throw error(404, "Assistant not found.");
20
+ }
21
+
22
+ const renderedComponent = (ChatThumbnail as unknown as SvelteComponent).render({
23
+ href: url.origin,
24
+ name: assistant.name,
25
+ description: assistant.description,
26
+ createdByName: assistant.createdByName,
27
+ avatarUrl: assistant.avatar
28
+ ? url.origin + APP_BASE + "/settings/assistants/" + assistant._id + "/avatar"
29
+ : undefined,
30
+ });
31
+
32
+ const reactLike = html(
33
+ "<style>" + renderedComponent.css.code + "</style>" + renderedComponent.html
34
+ );
35
+
36
+ const svg = await satori(reactLike, {
37
+ width: 1200,
38
+ height: 648,
39
+ fonts: [
40
+ {
41
+ name: "Inter",
42
+ data: await fetch(base + "/fonts/Inter-Regular.ttf").then((r) => r.arrayBuffer()),
43
+ weight: 500,
44
+ },
45
+ {
46
+ name: "Inter",
47
+ data: await fetch(base + "/fonts/Inter-Bold.ttf").then((r) => r.arrayBuffer()),
48
+ weight: 700,
49
+ },
50
+ ],
51
+ });
52
+
53
+ const png = new Resvg(svg, {
54
+ fitTo: { mode: "original" },
55
+ })
56
+ .render()
57
+ .asPng();
58
+
59
+ return new Response(png, {
60
+ headers: {
61
+ "Content-Type": "image/png",
62
+ },
63
+ });
64
+ }) satisfies RequestHandler;
src/routes/assistant/[assistantId]/thumbnail.png/ChatThumbnail.svelte ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { base } from "$app/paths";
3
+ import { PUBLIC_APP_ASSETS } from "$env/static/public";
4
+
5
+ export let href: string = "";
6
+ export let name: string;
7
+ export let description: string = "";
8
+ export let createdByName: string | undefined;
9
+ export let avatarUrl: string | undefined;
10
+
11
+ const imgUrl = `${href}${base}/${PUBLIC_APP_ASSETS}/logo.svg`;
12
+ </script>
13
+
14
+ <div class="flex h-full w-full flex-col items-center justify-center bg-black p-2">
15
+ <div class="flex w-full max-w-[540px] items-start justify-center text-white">
16
+ {#if avatarUrl}
17
+ <img class="h-64 w-64 rounded-full" src={avatarUrl} alt="avatar" />
18
+ {/if}
19
+ <div class="ml-10 flex flex-col items-start">
20
+ <p class="mb-2 mt-0 text-3xl font-normal text-gray-400">
21
+ <img class="mr-1.5 h-8 w-8" src={imgUrl} alt="app logo" />
22
+ AI assistant
23
+ </p>
24
+ <h1 class="m-0 {name.length < 38 ? 'text-5xl' : 'text-4xl'} text-balance font-black">
25
+ {name}
26
+ </h1>
27
+ <p class="mb-8 text-pretty text-2xl">
28
+ {description.slice(0, 160)}
29
+ {#if description.length > 160}...{/if}
30
+ </p>
31
+ <div class="rounded-full bg-[#FFA800] px-8 py-3 text-3xl font-semibold text-black">
32
+ Start chatting
33
+ </div>
34
+ </div>
35
+ </div>
36
+ {#if createdByName}
37
+ <p class="absolute bottom-4 right-8 text-2xl text-gray-400">
38
+ An AI assistant created by {createdByName}
39
+ </p>
40
+ {/if}
41
+ </div>
src/routes/conversation/+server.ts CHANGED
@@ -18,11 +18,11 @@ export const POST: RequestHandler = async ({ locals, request }) => {
18
  .object({
19
  fromShare: z.string().optional(),
20
  model: validateModel(models),
 
21
  preprompt: z.string().optional(),
22
  })
23
  .parse(JSON.parse(body));
24
 
25
- let preprompt = values.preprompt;
26
  let embeddingModel: string;
27
 
28
  if (values.fromShare) {
@@ -37,8 +37,9 @@ export const POST: RequestHandler = async ({ locals, request }) => {
37
  title = conversation.title;
38
  messages = conversation.messages;
39
  values.model = conversation.model;
 
 
40
  embeddingModel = conversation.embeddingModel;
41
- preprompt = conversation.preprompt;
42
  }
43
 
44
  const model = models.find((m) => m.name === values.model);
@@ -54,7 +55,16 @@ export const POST: RequestHandler = async ({ locals, request }) => {
54
  }
55
 
56
  // Use the model preprompt if there is no conversation/preprompt in the request body
57
- preprompt = preprompt === undefined ? model?.preprompt : preprompt;
 
 
 
 
 
 
 
 
 
58
 
59
  const res = await collections.conversations.insertOne({
60
  _id: new ObjectId(),
@@ -62,6 +72,7 @@ export const POST: RequestHandler = async ({ locals, request }) => {
62
  messages,
63
  model: values.model,
64
  preprompt: preprompt === model?.preprompt ? model?.preprompt : preprompt,
 
65
  createdAt: new Date(),
66
  updatedAt: new Date(),
67
  embeddingModel,
 
18
  .object({
19
  fromShare: z.string().optional(),
20
  model: validateModel(models),
21
+ assistantId: z.string().optional(),
22
  preprompt: z.string().optional(),
23
  })
24
  .parse(JSON.parse(body));
25
 
 
26
  let embeddingModel: string;
27
 
28
  if (values.fromShare) {
 
37
  title = conversation.title;
38
  messages = conversation.messages;
39
  values.model = conversation.model;
40
+ values.preprompt = conversation.preprompt;
41
+ values.assistantId = conversation.assistantId?.toString();
42
  embeddingModel = conversation.embeddingModel;
 
43
  }
44
 
45
  const model = models.find((m) => m.name === values.model);
 
55
  }
56
 
57
  // Use the model preprompt if there is no conversation/preprompt in the request body
58
+ const preprompt = await (async () => {
59
+ if (values.assistantId) {
60
+ const assistant = await collections.assistants.findOne({
61
+ _id: new ObjectId(values.assistantId),
62
+ });
63
+ return assistant?.preprompt;
64
+ } else {
65
+ return values?.preprompt ?? model?.preprompt;
66
+ }
67
+ })();
68
 
69
  const res = await collections.conversations.insertOne({
70
  _id: new ObjectId(),
 
72
  messages,
73
  model: values.model,
74
  preprompt: preprompt === model?.preprompt ? model?.preprompt : preprompt,
75
+ assistantId: values.assistantId ? new ObjectId(values.assistantId) : undefined,
76
  createdAt: new Date(),
77
  updatedAt: new Date(),
78
  embeddingModel,
src/routes/conversation/[id]/+page.server.ts CHANGED
@@ -44,11 +44,21 @@ export const load = async ({ params, depends, locals }) => {
44
  throw error(404, "Conversation not found.");
45
  }
46
  }
 
47
  return {
48
  messages: conversation.messages,
49
  title: conversation.title,
50
  model: conversation.model,
51
  preprompt: conversation.preprompt,
 
 
 
 
 
 
 
 
 
52
  shared,
53
  };
54
  };
 
44
  throw error(404, "Conversation not found.");
45
  }
46
  }
47
+
48
  return {
49
  messages: conversation.messages,
50
  title: conversation.title,
51
  model: conversation.model,
52
  preprompt: conversation.preprompt,
53
+ assistant: conversation.assistantId
54
+ ? JSON.parse(
55
+ JSON.stringify(
56
+ await collections.assistants.findOne({
57
+ _id: new ObjectId(conversation.assistantId),
58
+ })
59
+ )
60
+ )
61
+ : null,
62
  shared,
63
  };
64
  };
src/routes/conversation/[id]/share/+server.ts CHANGED
@@ -40,6 +40,7 @@ export async function POST({ params, url, locals }) {
40
  model: conversation.model,
41
  embeddingModel: conversation.embeddingModel,
42
  preprompt: conversation.preprompt,
 
43
  };
44
 
45
  await collections.sharedConversations.insertOne(shared);
 
40
  model: conversation.model,
41
  embeddingModel: conversation.embeddingModel,
42
  preprompt: conversation.preprompt,
43
+ assistantId: conversation.assistantId,
44
  };
45
 
46
  await collections.sharedConversations.insertOne(shared);
src/routes/settings/+layout.server.ts ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { collections } from "$lib/server/database";
2
+ import { ObjectId } from "mongodb";
3
+ import type { LayoutServerLoad } from "./$types";
4
+
5
+ export const load = (async ({ locals, parent }) => {
6
+ const { settings } = await parent();
7
+
8
+ // find assistants matching the settings assistants
9
+ const assistants = await collections.assistants
10
+ .find({
11
+ _id: { $in: settings.assistants.map((el) => new ObjectId(el)) },
12
+ })
13
+ .toArray();
14
+
15
+ return {
16
+ assistants: await Promise.all(
17
+ assistants.map(async (el) => ({
18
+ ...el,
19
+ _id: el._id.toString(),
20
+ createdById: undefined,
21
+ createdByMe:
22
+ el.createdById.toString() === (locals.user?._id ?? locals.sessionId).toString(),
23
+ reported:
24
+ (await collections.reports.countDocuments({
25
+ assistantId: el._id,
26
+ createdBy: locals.user?._id ?? locals.sessionId,
27
+ })) > 0,
28
+ }))
29
+ ),
30
+ };
31
+ }) satisfies LayoutServerLoad;
src/routes/settings/+layout.svelte CHANGED
@@ -1,14 +1,16 @@
1
  <script lang="ts">
2
  import { base } from "$app/paths";
3
  import { clickOutside } from "$lib/actions/clickOutside";
4
- import { browser } from "$app/environment";
5
  import { afterNavigate, goto } from "$app/navigation";
6
  import { page } from "$app/stores";
7
  import { useSettingsStore } from "$lib/stores/settings";
8
  import CarbonClose from "~icons/carbon/close";
9
  import CarbonCheckmark from "~icons/carbon/checkmark";
 
10
 
11
  import UserIcon from "~icons/carbon/user";
 
 
12
  export let data;
13
 
14
  let previousPage: string = base;
@@ -20,25 +22,27 @@
20
  });
21
 
22
  const settings = useSettingsStore();
 
 
23
  </script>
24
 
25
  <div
26
  class="fixed inset-0 flex items-center justify-center bg-black/80 backdrop-blur-sm dark:bg-black/50"
 
27
  >
28
  <dialog
 
29
  open
30
  use:clickOutside={() => {
31
- if (browser) window;
32
  goto(previousPage);
33
  }}
34
- class="xl: z-10 grid h-[95dvh] w-[90dvw] grid-cols-1 content-start gap-x-10 gap-y-6 overflow-hidden rounded-2xl bg-white p-4 shadow-2xl outline-none sm:h-[80dvh] md:grid-cols-3 md:grid-rows-[auto,1fr] md:p-8 xl:w-[1200px] 2xl:h-[70dvh]"
35
  >
36
- <div class="col-span-1 flex items-center justify-between md:col-span-3">
37
  <h2 class="text-xl font-bold">Settings</h2>
38
  <button
39
  class="btn rounded-lg"
40
  on:click={() => {
41
- if (browser) window;
42
  goto(previousPage);
43
  }}
44
  >
@@ -46,38 +50,81 @@
46
  </button>
47
  </div>
48
  <div
49
- class="col-span-1 flex flex-col overflow-y-auto whitespace-nowrap max-md:-mx-4 max-md:h-[245px] max-md:border md:pr-6"
50
  >
 
 
51
  {#each data.models.filter((el) => !el.unlisted) as model}
52
  <a
53
  href="{base}/settings/{model.id}"
54
- class="group flex h-11 flex-none items-center gap-3 pl-3 pr-2 text-gray-500 hover:bg-gray-100 md:rounded-xl {model.id ===
55
- $page.params.model
56
- ? '!bg-gray-100 !text-gray-800'
57
- : ''}"
58
  >
59
  <div class="truncate">{model.displayName}</div>
60
  {#if model.id === $settings.activeModel}
61
  <div
62
- class="rounded-lg bg-black px-2 py-1.5 text-xs font-semibold leading-none text-white"
63
  >
64
  Active
65
  </div>
66
  {/if}
67
  </a>
68
  {/each}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69
  <a
70
  href="{base}/settings"
71
- class="group mt-auto flex h-11 flex-none items-center gap-3 pl-3 pr-2 text-gray-500 hover:bg-gray-100 max-md:order-first md:rounded-xl {$page
72
- .params.model === undefined
73
- ? '!bg-gray-100 !text-gray-800'
74
- : ''}"
75
  >
76
- <UserIcon class="pr-1 text-lg" />
77
  Application Settings
78
  </a>
79
  </div>
80
- <div class="col-span-1 overflow-y-auto md:col-span-2">
81
  <slot />
82
  </div>
83
 
@@ -85,7 +132,7 @@
85
  <div
86
  class="absolute bottom-4 right-4 m-2 flex items-center gap-1.5 rounded-full border border-gray-300 bg-gray-200 px-3 py-1 text-black"
87
  >
88
- <CarbonCheckmark />
89
  Saved
90
  </div>
91
  {/if}
 
1
  <script lang="ts">
2
  import { base } from "$app/paths";
3
  import { clickOutside } from "$lib/actions/clickOutside";
 
4
  import { afterNavigate, goto } from "$app/navigation";
5
  import { page } from "$app/stores";
6
  import { useSettingsStore } from "$lib/stores/settings";
7
  import CarbonClose from "~icons/carbon/close";
8
  import CarbonCheckmark from "~icons/carbon/checkmark";
9
+ import CarbonAdd from "~icons/carbon/add";
10
 
11
  import UserIcon from "~icons/carbon/user";
12
+ import { fade, fly } from "svelte/transition";
13
+ import { PUBLIC_APP_ASSETS } from "$env/static/public";
14
  export let data;
15
 
16
  let previousPage: string = base;
 
22
  });
23
 
24
  const settings = useSettingsStore();
25
+
26
+ const isHuggingChat = PUBLIC_APP_ASSETS === "huggingchat";
27
  </script>
28
 
29
  <div
30
  class="fixed inset-0 flex items-center justify-center bg-black/80 backdrop-blur-sm dark:bg-black/50"
31
+ in:fade
32
  >
33
  <dialog
34
+ in:fly={{ y: 100 }}
35
  open
36
  use:clickOutside={() => {
 
37
  goto(previousPage);
38
  }}
39
+ class="xl: z-10 grid h-[95dvh] w-[90dvw] grid-cols-1 content-start gap-x-8 overflow-hidden rounded-2xl bg-white p-4 shadow-2xl outline-none sm:h-[80dvh] md:grid-cols-3 md:grid-rows-[auto,1fr] md:p-8 xl:w-[1200px] 2xl:h-[70dvh]"
40
  >
41
+ <div class="col-span-1 mb-4 flex items-center justify-between md:col-span-3">
42
  <h2 class="text-xl font-bold">Settings</h2>
43
  <button
44
  class="btn rounded-lg"
45
  on:click={() => {
 
46
  goto(previousPage);
47
  }}
48
  >
 
50
  </button>
51
  </div>
52
  <div
53
+ class="col-span-1 flex flex-col overflow-y-auto whitespace-nowrap max-md:-mx-4 max-md:h-[245px] max-md:border max-md:border-b-2 md:pr-6"
54
  >
55
+ <h3 class="pb-3 pl-3 pt-2 text-[.8rem] text-gray-800 sm:pl-1">Models</h3>
56
+
57
  {#each data.models.filter((el) => !el.unlisted) as model}
58
  <a
59
  href="{base}/settings/{model.id}"
60
+ class="group flex h-10 flex-none items-center gap-2 pl-3 pr-2 text-sm text-gray-500 hover:bg-gray-100 md:rounded-xl
61
+ {model.id === $page.params.model ? '!bg-gray-100 !text-gray-800' : ''}"
 
 
62
  >
63
  <div class="truncate">{model.displayName}</div>
64
  {#if model.id === $settings.activeModel}
65
  <div
66
+ class="ml-auto rounded-lg bg-black px-2 py-1.5 text-xs font-semibold leading-none text-white"
67
  >
68
  Active
69
  </div>
70
  {/if}
71
  </a>
72
  {/each}
73
+ <!-- if its huggingchat, the number of assistants owned by the user must be non-zero to show the UI -->
74
+ {#if data.enableAssistants && (!isHuggingChat || data.assistants.length >= 1)}
75
+ <h3 class="pb-3 pl-3 pt-5 text-[.8rem] text-gray-800 sm:pl-1">Assistants</h3>
76
+ {#each data.assistants as assistant}
77
+ <a
78
+ href="{base}/settings/assistants/{assistant._id.toString()}"
79
+ class="group flex h-10 flex-none items-center gap-2 pl-2 pr-2 text-sm text-gray-500 hover:bg-gray-100 md:rounded-xl
80
+ {assistant._id.toString() === $page.params.assistantId ? '!bg-gray-100 !text-gray-800' : ''}"
81
+ >
82
+ {#if assistant.avatar}
83
+ <img
84
+ src="{base}/settings/assistants/{assistant._id.toString()}/avatar?hash={assistant.avatar}"
85
+ alt="Avatar"
86
+ class="h-6 w-6 rounded-full object-cover"
87
+ />
88
+ {:else}
89
+ <div
90
+ class="flex size-6 items-center justify-center rounded-full bg-gray-300 font-bold uppercase text-gray-500"
91
+ >
92
+ {assistant.name[0]}
93
+ </div>
94
+ {/if}
95
+ <div class="truncate">{assistant.name}</div>
96
+ {#if assistant._id.toString() === $settings.activeModel}
97
+ <div
98
+ class="ml-auto rounded-lg bg-black px-2 py-1.5 text-xs font-semibold leading-none text-white"
99
+ >
100
+ Active
101
+ </div>
102
+ {/if}
103
+ </a>
104
+ {/each}
105
+
106
+ {#if !data.loginEnabled || (data.loginEnabled && !!data.user)}
107
+ <a
108
+ href="{base}/settings/assistants/new"
109
+ class="group flex h-10 flex-none items-center gap-2 pl-3 pr-2 text-sm text-gray-500 hover:bg-gray-100 md:rounded-xl
110
+ {$page.url.pathname === `${base}/settings/assistants/new` ? '!bg-gray-100 !text-gray-800' : ''}"
111
+ >
112
+ <CarbonAdd />
113
+ <div class="truncate">Create new assistant</div>
114
+ </a>
115
+ {/if}
116
+ {/if}
117
+
118
  <a
119
  href="{base}/settings"
120
+ class="group mt-auto flex h-10 flex-none items-center gap-2 pl-3 pr-2 text-sm text-gray-500 hover:bg-gray-100 max-md:order-first md:rounded-xl
121
+ {$page.url.pathname === `${base}/settings` ? '!bg-gray-100 !text-gray-800' : ''}"
 
 
122
  >
123
+ <UserIcon class="text-lg" />
124
  Application Settings
125
  </a>
126
  </div>
127
+ <div class="col-span-1 overflow-y-auto px-4 max-md:-mx-4 max-md:pt-6 md:col-span-2">
128
  <slot />
129
  </div>
130
 
 
132
  <div
133
  class="absolute bottom-4 right-4 m-2 flex items-center gap-1.5 rounded-full border border-gray-300 bg-gray-200 px-3 py-1 text-black"
134
  >
135
+ <CarbonCheckmark class="text-green-500" />
136
  Saved
137
  </div>
138
  {/if}
src/routes/settings/+server.ts CHANGED
@@ -1,6 +1,5 @@
1
  import { collections } from "$lib/server/database";
2
  import { z } from "zod";
3
- import { models, validateModel } from "$lib/server/models";
4
  import { authCondition } from "$lib/server/auth";
5
  import { DEFAULT_SETTINGS } from "$lib/types/Settings";
6
 
@@ -14,7 +13,7 @@ export async function POST({ request, locals }) {
14
  .default(DEFAULT_SETTINGS.shareConversationsWithModelAuthors),
15
  hideEmojiOnSidebar: z.boolean().default(DEFAULT_SETTINGS.hideEmojiOnSidebar),
16
  ethicsModalAccepted: z.boolean().optional(),
17
- activeModel: validateModel(models).default(DEFAULT_SETTINGS.activeModel),
18
  customPrompts: z.record(z.string()).default({}),
19
  })
20
  .parse(body);
 
1
  import { collections } from "$lib/server/database";
2
  import { z } from "zod";
 
3
  import { authCondition } from "$lib/server/auth";
4
  import { DEFAULT_SETTINGS } from "$lib/types/Settings";
5
 
 
13
  .default(DEFAULT_SETTINGS.shareConversationsWithModelAuthors),
14
  hideEmojiOnSidebar: z.boolean().default(DEFAULT_SETTINGS.hideEmojiOnSidebar),
15
  ethicsModalAccepted: z.boolean().optional(),
16
+ activeModel: z.string().default(DEFAULT_SETTINGS.activeModel),
17
  customPrompts: z.record(z.string()).default({}),
18
  })
19
  .parse(body);
src/routes/settings/[...model]/+page.svelte CHANGED
@@ -78,7 +78,7 @@
78
  value="{PUBLIC_ORIGIN || $page.url.origin}{base}?model={model.id}"
79
  classNames="!border-none !shadow-none !py-0 !px-1 !rounded-md"
80
  >
81
- <div class="flex items-center gap-1.5">
82
  <CarbonLink />Copy direct link to model
83
  </div>
84
  </CopyToClipBoardBtn>
 
78
  value="{PUBLIC_ORIGIN || $page.url.origin}{base}?model={model.id}"
79
  classNames="!border-none !shadow-none !py-0 !px-1 !rounded-md"
80
  >
81
+ <div class="flex items-center gap-1.5 hover:underline">
82
  <CarbonLink />Copy direct link to model
83
  </div>
84
  </CopyToClipBoardBtn>
src/routes/settings/assistants/[assistantId]/+page.server.ts ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { collections } from "$lib/server/database";
2
+ import { type Actions, fail, redirect } from "@sveltejs/kit";
3
+ import { ObjectId } from "mongodb";
4
+ import { authCondition } from "$lib/server/auth";
5
+ import { base } from "$app/paths";
6
+
7
+ async function assistantOnlyIfAuthor(locals: App.Locals, assistantId?: string) {
8
+ const assistant = await collections.assistants.findOne({ _id: new ObjectId(assistantId) });
9
+
10
+ if (!assistant) {
11
+ throw Error("Assistant not found");
12
+ }
13
+
14
+ if (assistant.createdById.toString() !== (locals.user?._id ?? locals.sessionId).toString()) {
15
+ throw Error("You are not the author of this assistant");
16
+ }
17
+
18
+ return assistant;
19
+ }
20
+
21
+ export const actions: Actions = {
22
+ delete: async ({ params, locals }) => {
23
+ let assistant;
24
+ try {
25
+ assistant = await assistantOnlyIfAuthor(locals, params.assistantId);
26
+ } catch (e) {
27
+ return fail(400, { error: true, message: (e as Error).message });
28
+ }
29
+
30
+ await collections.assistants.deleteOne({ _id: assistant._id });
31
+
32
+ // and remove it from all users settings
33
+ await collections.settings.updateMany(
34
+ {},
35
+ {
36
+ $pull: { assistants: assistant._id },
37
+ }
38
+ );
39
+
40
+ // and delete all avatars
41
+ const fileCursor = collections.bucket.find({ filename: assistant._id.toString() });
42
+
43
+ // Step 2: Delete the existing file if it exists
44
+ let fileId = await fileCursor.next();
45
+ while (fileId) {
46
+ await collections.bucket.delete(fileId._id);
47
+ fileId = await fileCursor.next();
48
+ }
49
+
50
+ throw redirect(302, `${base}/settings`);
51
+ },
52
+ report: async ({ params, locals }) => {
53
+ // is there already a report from this user for this model ?
54
+ const report = await collections.reports.findOne({
55
+ assistantId: new ObjectId(params.assistantId),
56
+ createdBy: locals.user?._id ?? locals.sessionId,
57
+ });
58
+
59
+ if (report) {
60
+ return fail(400, { error: true, message: "Already reported" });
61
+ }
62
+
63
+ const { acknowledged } = await collections.reports.insertOne({
64
+ _id: new ObjectId(),
65
+ assistantId: new ObjectId(params.assistantId),
66
+ createdBy: locals.user?._id ?? locals.sessionId,
67
+ createdAt: new Date(),
68
+ updatedAt: new Date(),
69
+ });
70
+
71
+ if (!acknowledged) {
72
+ return fail(500, { error: true, message: "Failed to report assistant" });
73
+ }
74
+ return { from: "report", ok: true, message: "Assistant reported" };
75
+ },
76
+
77
+ subscribe: async ({ params, locals }) => {
78
+ const assistant = await collections.assistants.findOne({
79
+ _id: new ObjectId(params.assistantId),
80
+ });
81
+
82
+ if (!assistant) {
83
+ return fail(404, { error: true, message: "Assistant not found" });
84
+ }
85
+
86
+ // don't push if it's already there
87
+ const settings = await collections.settings.findOne(authCondition(locals));
88
+
89
+ if (settings?.assistants?.includes(assistant._id)) {
90
+ return fail(400, { error: true, message: "Already subscribed" });
91
+ }
92
+
93
+ await collections.settings.updateOne(authCondition(locals), {
94
+ $push: { assistants: assistant._id },
95
+ });
96
+
97
+ return { from: "subscribe", ok: true, message: "Assistant added" };
98
+ },
99
+
100
+ unsubscribe: async ({ params, locals }) => {
101
+ const assistant = await collections.assistants.findOne({
102
+ _id: new ObjectId(params.assistantId),
103
+ });
104
+
105
+ if (!assistant) {
106
+ return fail(404, { error: true, message: "Assistant not found" });
107
+ }
108
+
109
+ await collections.settings.updateOne(authCondition(locals), {
110
+ $pull: { assistants: assistant._id },
111
+ });
112
+
113
+ throw redirect(302, `${base}/settings`);
114
+ },
115
+ };
src/routes/settings/assistants/[assistantId]/+page.svelte ADDED
@@ -0,0 +1,156 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { enhance } from "$app/forms";
3
+ import { base } from "$app/paths";
4
+ import { page } from "$app/stores";
5
+ import { PUBLIC_ORIGIN, PUBLIC_SHARE_PREFIX } from "$env/static/public";
6
+ import { useSettingsStore } from "$lib/stores/settings";
7
+ import type { PageData } from "./$types";
8
+
9
+ import CarbonPen from "~icons/carbon/pen";
10
+ import CarbonTrash from "~icons/carbon/trash-can";
11
+ import CarbonCopy from "~icons/carbon/copy-file";
12
+ import CarbonFlag from "~icons/carbon/flag";
13
+ import CarbonLink from "~icons/carbon/link";
14
+ import CopyToClipBoardBtn from "$lib/components/CopyToClipBoardBtn.svelte";
15
+
16
+ export let data: PageData;
17
+
18
+ $: assistant = data.assistants.find((el) => el._id.toString() === $page.params.assistantId);
19
+
20
+ const settings = useSettingsStore();
21
+
22
+ $: isActive = $settings.activeModel === $page.params.assistantId;
23
+
24
+ const prefix = PUBLIC_SHARE_PREFIX || `${PUBLIC_ORIGIN || $page.url.origin}${base}`;
25
+
26
+ $: shareUrl = `${prefix}/assistant/${assistant?._id}`;
27
+ </script>
28
+
29
+ <div class="flex h-full flex-col gap-2">
30
+ <div class="flex gap-6">
31
+ {#if assistant?.avatar}
32
+ <!-- crop image if not square -->
33
+ <img
34
+ src={`${base}/settings/assistants/${assistant?._id}/avatar?hash=${assistant?.avatar}`}
35
+ alt="Avatar"
36
+ class="h-24 w-24 rounded-full object-cover"
37
+ />
38
+ {:else}
39
+ <div
40
+ class="flex size-16 flex-none items-center justify-center rounded-full bg-gray-300 text-4xl font-semibold uppercase text-gray-500 sm:size-24"
41
+ >
42
+ {assistant?.name[0]}
43
+ </div>
44
+ {/if}
45
+
46
+ <div>
47
+ <h1 class="text-xl font-semibold">
48
+ {assistant?.name}
49
+ </h1>
50
+
51
+ {#if assistant?.description}
52
+ <p class="pb-2 text-sm text-gray-500">
53
+ {assistant.description}
54
+ </p>
55
+ {/if}
56
+
57
+ <p class="text-sm text-gray-500">
58
+ Model: <span class="font-semibold"> {assistant?.modelId} </span>
59
+ </p>
60
+ <button
61
+ class="{isActive
62
+ ? 'bg-gray-100'
63
+ : 'bg-black text-white'} my-2 flex w-fit items-center rounded-full px-3 py-1"
64
+ disabled={isActive}
65
+ name="Activate model"
66
+ on:click|stopPropagation={() => {
67
+ $settings.activeModel = $page.params.assistantId;
68
+ }}
69
+ >
70
+ {isActive ? "Active" : "Activate"}
71
+ </button>
72
+ </div>
73
+ </div>
74
+
75
+ <div>
76
+ <h2 class="text-lg font-semibold">Direct URL</h2>
77
+
78
+ <p class="pb-2 text-sm text-gray-500">
79
+ People with this link will be able to use your assistant.
80
+ {#if !assistant?.createdByMe && assistant?.createdByName}
81
+ Created by <a
82
+ class="underline"
83
+ target="_blank"
84
+ href={"https://hf.co/" + assistant?.createdByName}
85
+ >
86
+ {assistant?.createdByName}
87
+ </a>
88
+ {/if}
89
+ </p>
90
+
91
+ <div
92
+ class="flex flex-row gap-2 rounded-lg border-2 border-gray-200 bg-gray-100 py-2 pl-3 pr-1.5"
93
+ >
94
+ <input disabled class="flex-1 truncate bg-inherit" value={shareUrl} />
95
+ <CopyToClipBoardBtn
96
+ value={shareUrl}
97
+ classNames="!border-none !shadow-none !py-0 !px-1 !rounded-md"
98
+ >
99
+ <div class="flex items-center gap-1.5 text-gray-500 hover:underline">
100
+ <CarbonLink />Copy
101
+ </div>
102
+ </CopyToClipBoardBtn>
103
+ </div>
104
+ </div>
105
+
106
+ <!-- <div>
107
+ <h2 class="mb-2 text-lg font-semibold">Model used</h2>
108
+
109
+ <div
110
+ class="flex flex-row gap-2 rounded-lg border-2 border-gray-200 bg-gray-100 py-2 pl-3 pr-1.5"
111
+ >
112
+ <input disabled class="flex-1" value="Model" />
113
+ </div>
114
+ </div> -->
115
+
116
+ <h2 class="mt-4 text-lg font-semibold">System Instructions</h2>
117
+
118
+ <textarea disabled class="h-[8lh] w-full rounded-lg border-2 border-gray-200 bg-gray-100 p-2"
119
+ >{assistant?.preprompt}</textarea
120
+ >
121
+
122
+ <div class="mt-5 flex gap-4">
123
+ {#if assistant?.createdByMe}
124
+ <a href="{base}/settings/assistants/{assistant?._id}/edit" class="underline"
125
+ ><CarbonPen class="mr-1.5 inline" />Edit assistant</a
126
+ >
127
+ <form method="POST" action="?/delete" use:enhance>
128
+ <button type="submit" class="flex items-center underline">
129
+ <CarbonTrash class="mr-1.5 inline" />Delete assistant</button
130
+ >
131
+ </form>
132
+ {:else}
133
+ <form method="POST" action="?/unsubscribe" use:enhance>
134
+ <button type="submit" class="underline">
135
+ <CarbonTrash class="mr-1.5 inline" />Remove assistant</button
136
+ >
137
+ </form>
138
+ <form method="POST" action="?/edit" use:enhance class="hidden">
139
+ <button type="submit" class="underline">
140
+ <CarbonCopy class="mr-1.5 inline" />Duplicate assistant</button
141
+ >
142
+ </form>
143
+ {#if !assistant?.reported}
144
+ <form method="POST" action="?/report" use:enhance>
145
+ <button type="submit" class="underline">
146
+ <CarbonFlag class="mr-1.5 inline" />Report assistant</button
147
+ >
148
+ </form>
149
+ {:else}
150
+ <button type="button" disabled class="text-gray-700">
151
+ <CarbonFlag class="mr-1.5 inline" />Reported</button
152
+ >
153
+ {/if}
154
+ {/if}
155
+ </div>
156
+ </div>
src/routes/settings/assistants/[assistantId]/+page.ts ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { base } from "$app/paths";
2
+ import { redirect } from "@sveltejs/kit";
3
+
4
+ export async function load({ parent, params }) {
5
+ const data = await parent();
6
+
7
+ const assistant = data.settings.assistants.find((id) => id === params.assistantId);
8
+
9
+ if (!assistant) {
10
+ throw redirect(302, `${base}/assistant/${params.assistantId}`);
11
+ }
12
+
13
+ return data;
14
+ }
src/routes/settings/assistants/[assistantId]/avatar/+server.ts ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { collections } from "$lib/server/database";
2
+ import { error, type RequestHandler } from "@sveltejs/kit";
3
+ import { ObjectId } from "mongodb";
4
+
5
+ export const GET: RequestHandler = async ({ params }) => {
6
+ const assistant = await collections.assistants.findOne({
7
+ _id: new ObjectId(params.assistantId),
8
+ });
9
+
10
+ if (!assistant) {
11
+ throw error(404, "No assistant found");
12
+ }
13
+
14
+ if (!assistant.avatar) {
15
+ throw error(404, "No avatar found");
16
+ }
17
+
18
+ const fileId = collections.bucket.find({ filename: assistant._id.toString() });
19
+
20
+ let mime = "";
21
+
22
+ const content = await fileId.next().then(async (file) => {
23
+ mime = file?.metadata?.mime;
24
+
25
+ if (!file?._id) {
26
+ throw error(404, "Avatar not found");
27
+ }
28
+
29
+ const fileStream = collections.bucket.openDownloadStream(file?._id);
30
+
31
+ const fileBuffer = await new Promise<Buffer>((resolve, reject) => {
32
+ const chunks: Uint8Array[] = [];
33
+ fileStream.on("data", (chunk) => chunks.push(chunk));
34
+ fileStream.on("error", reject);
35
+ fileStream.on("end", () => resolve(Buffer.concat(chunks)));
36
+ });
37
+
38
+ return fileBuffer;
39
+ });
40
+
41
+ return new Response(content, {
42
+ headers: {
43
+ "Content-Type": mime ?? "application/octet-stream",
44
+ },
45
+ });
46
+ };
src/routes/settings/assistants/[assistantId]/edit/+page.server.ts ADDED
@@ -0,0 +1,136 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { base } from "$app/paths";
2
+ import { requiresUser } from "$lib/server/auth";
3
+ import { collections } from "$lib/server/database";
4
+ import { fail, type Actions, redirect } from "@sveltejs/kit";
5
+ import { ObjectId } from "mongodb";
6
+
7
+ import { z } from "zod";
8
+ import sizeof from "image-size";
9
+ import { sha256 } from "$lib/utils/sha256";
10
+
11
+ const newAsssistantSchema = z.object({
12
+ name: z.string().min(1),
13
+ modelId: z.string().min(1),
14
+ preprompt: z.string().min(1),
15
+ description: z.string().optional(),
16
+ exampleInput1: z.string().optional(),
17
+ exampleInput2: z.string().optional(),
18
+ exampleInput3: z.string().optional(),
19
+ exampleInput4: z.string().optional(),
20
+ avatar: z.union([z.instanceof(File), z.literal("null")]).optional(),
21
+ });
22
+
23
+ const uploadAvatar = async (avatar: File, assistantId: ObjectId): Promise<string> => {
24
+ const hash = await sha256(await avatar.text());
25
+ const upload = collections.bucket.openUploadStream(`${assistantId.toString()}`, {
26
+ metadata: { type: avatar.type, hash },
27
+ });
28
+
29
+ upload.write((await avatar.arrayBuffer()) as unknown as Buffer);
30
+ upload.end();
31
+
32
+ // only return the filename when upload throws a finish event or a 10s time out occurs
33
+ return new Promise((resolve, reject) => {
34
+ upload.once("finish", () => resolve(hash));
35
+ upload.once("error", reject);
36
+ setTimeout(() => reject(new Error("Upload timed out")), 10000);
37
+ });
38
+ };
39
+
40
+ export const actions: Actions = {
41
+ default: async ({ request, locals, params }) => {
42
+ const assistant = await collections.assistants.findOne({
43
+ _id: new ObjectId(params.assistantId),
44
+ });
45
+
46
+ if (!assistant) {
47
+ throw Error("Assistant not found");
48
+ }
49
+
50
+ if (assistant.createdById.toString() !== (locals.user?._id ?? locals.sessionId).toString()) {
51
+ throw Error("You are not the author of this assistant");
52
+ }
53
+
54
+ const formData = Object.fromEntries(await request.formData());
55
+
56
+ const parse = newAsssistantSchema.safeParse(formData);
57
+
58
+ if (!parse.success) {
59
+ // Loop through the errors array and create a custom errors array
60
+ const errors = parse.error.errors.map((error) => {
61
+ return {
62
+ field: error.path[0],
63
+ message: error.message,
64
+ };
65
+ });
66
+
67
+ return fail(400, { error: true, errors });
68
+ }
69
+
70
+ // can only create assistants when logged in, IF login is setup
71
+ if (!locals.user && requiresUser) {
72
+ const errors = [{ field: "preprompt", message: "Must be logged in. Unauthorized" }];
73
+ return fail(400, { error: true, errors });
74
+ }
75
+
76
+ const exampleInputs: string[] = [
77
+ parse?.data?.exampleInput1 ?? "",
78
+ parse?.data?.exampleInput2 ?? "",
79
+ parse?.data?.exampleInput3 ?? "",
80
+ parse?.data?.exampleInput4 ?? "",
81
+ ].filter((input) => !!input);
82
+
83
+ const deleteAvatar = parse.data.avatar === "null";
84
+
85
+ let hash;
86
+ if (parse.data.avatar && parse.data.avatar !== "null" && parse.data.avatar.size > 0) {
87
+ const dims = sizeof(Buffer.from(await parse.data.avatar.arrayBuffer()));
88
+
89
+ if ((dims.height ?? 1000) > 512 || (dims.width ?? 1000) > 512) {
90
+ const errors = [{ field: "avatar", message: "Avatar too big" }];
91
+ return fail(400, { error: true, errors });
92
+ }
93
+
94
+ const fileCursor = collections.bucket.find({ filename: assistant._id.toString() });
95
+
96
+ // Step 2: Delete the existing file if it exists
97
+ let fileId = await fileCursor.next();
98
+ while (fileId) {
99
+ await collections.bucket.delete(fileId._id);
100
+ fileId = await fileCursor.next();
101
+ }
102
+
103
+ hash = await uploadAvatar(parse.data.avatar, assistant._id);
104
+ } else if (deleteAvatar) {
105
+ // delete the avatar
106
+ const fileCursor = collections.bucket.find({ filename: assistant._id.toString() });
107
+
108
+ let fileId = await fileCursor.next();
109
+ while (fileId) {
110
+ await collections.bucket.delete(fileId._id);
111
+ fileId = await fileCursor.next();
112
+ }
113
+ }
114
+
115
+ const { acknowledged } = await collections.assistants.replaceOne(
116
+ {
117
+ _id: assistant._id,
118
+ },
119
+ {
120
+ createdById: assistant?.createdById,
121
+ createdByName: locals.user?.username ?? locals.user?.name,
122
+ ...parse.data,
123
+ exampleInputs,
124
+ avatar: deleteAvatar ? undefined : hash ?? assistant.avatar,
125
+ createdAt: new Date(),
126
+ updatedAt: new Date(),
127
+ }
128
+ );
129
+
130
+ if (acknowledged) {
131
+ throw redirect(302, `${base}/settings/assistants/${assistant._id}`);
132
+ } else {
133
+ throw Error("Update failed");
134
+ }
135
+ },
136
+ };
src/routes/settings/assistants/[assistantId]/edit/+page.svelte ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import type { PageData, ActionData } from "./$types";
3
+ import { page } from "$app/stores";
4
+ import AssistantSettings from "$lib/components/AssistantSettings.svelte";
5
+
6
+ export let data: PageData;
7
+ export let form: ActionData;
8
+
9
+ $: assistant = data.assistants.find((el) => el._id.toString() === $page.params.assistantId);
10
+ </script>
11
+
12
+ <AssistantSettings bind:form {assistant} models={data.models} />
src/routes/settings/assistants/new/+page.server.ts ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { base } from "$app/paths";
2
+ import { authCondition, requiresUser } from "$lib/server/auth";
3
+ import { collections } from "$lib/server/database";
4
+ import { fail, type Actions, redirect } from "@sveltejs/kit";
5
+ import { ObjectId } from "mongodb";
6
+
7
+ import { z } from "zod";
8
+ import sizeof from "image-size";
9
+ import { sha256 } from "$lib/utils/sha256";
10
+
11
+ const newAsssistantSchema = z.object({
12
+ name: z.string().min(1),
13
+ modelId: z.string().min(1),
14
+ preprompt: z.string().min(1),
15
+ description: z.string().optional(),
16
+ exampleInput1: z.string().optional(),
17
+ exampleInput2: z.string().optional(),
18
+ exampleInput3: z.string().optional(),
19
+ exampleInput4: z.string().optional(),
20
+ avatar: z.instanceof(File).optional(),
21
+ });
22
+
23
+ const uploadAvatar = async (avatar: File, assistantId: ObjectId): Promise<string> => {
24
+ const hash = await sha256(await avatar.text());
25
+ const upload = collections.bucket.openUploadStream(`${assistantId.toString()}`, {
26
+ metadata: { type: avatar.type, hash },
27
+ });
28
+
29
+ upload.write((await avatar.arrayBuffer()) as unknown as Buffer);
30
+ upload.end();
31
+
32
+ // only return the filename when upload throws a finish event or a 10s time out occurs
33
+ return new Promise((resolve, reject) => {
34
+ upload.once("finish", () => resolve(hash));
35
+ upload.once("error", reject);
36
+ setTimeout(() => reject(new Error("Upload timed out")), 10000);
37
+ });
38
+ };
39
+
40
+ export const actions: Actions = {
41
+ default: async ({ request, locals }) => {
42
+ const formData = Object.fromEntries(await request.formData());
43
+
44
+ const parse = newAsssistantSchema.safeParse(formData);
45
+
46
+ if (!parse.success) {
47
+ // Loop through the errors array and create a custom errors array
48
+ const errors = parse.error.errors.map((error) => {
49
+ return {
50
+ field: error.path[0],
51
+ message: error.message,
52
+ };
53
+ });
54
+
55
+ return fail(400, { error: true, errors });
56
+ }
57
+
58
+ // can only create assistants when logged in, IF login is setup
59
+ if (!locals.user && requiresUser) {
60
+ const errors = [{ field: "preprompt", message: "Must be logged in. Unauthorized" }];
61
+ return fail(400, { error: true, errors });
62
+ }
63
+
64
+ const createdById = locals.user?._id ?? locals.sessionId;
65
+
66
+ const newAssistantId = new ObjectId();
67
+
68
+ const exampleInputs: string[] = [
69
+ parse?.data?.exampleInput1 ?? "",
70
+ parse?.data?.exampleInput2 ?? "",
71
+ parse?.data?.exampleInput3 ?? "",
72
+ parse?.data?.exampleInput4 ?? "",
73
+ ].filter((input) => !!input);
74
+
75
+ let hash;
76
+ if (parse.data.avatar && parse.data.avatar.size > 0) {
77
+ const dims = sizeof(Buffer.from(await parse.data.avatar.arrayBuffer()));
78
+
79
+ if ((dims.height ?? 1000) > 512 || (dims.width ?? 1000) > 512) {
80
+ const errors = [
81
+ {
82
+ field: "avatar",
83
+ message:
84
+ "Avatar is too big. Please make sure the size of your avatar is no bigger than 512px by 512px.",
85
+ },
86
+ ];
87
+ return fail(400, { error: true, errors });
88
+ }
89
+
90
+ hash = await uploadAvatar(parse.data.avatar, newAssistantId);
91
+ }
92
+
93
+ const { insertedId } = await collections.assistants.insertOne({
94
+ _id: newAssistantId,
95
+ createdById,
96
+ createdByName: locals.user?.username ?? locals.user?.name,
97
+ ...parse.data,
98
+ exampleInputs,
99
+ avatar: hash,
100
+ createdAt: new Date(),
101
+ updatedAt: new Date(),
102
+ });
103
+
104
+ // add insertedId to user settings
105
+
106
+ await collections.settings.updateOne(authCondition(locals), {
107
+ $push: { assistants: insertedId },
108
+ });
109
+
110
+ throw redirect(302, `${base}/settings/assistants/${insertedId}`);
111
+ },
112
+ };
src/routes/settings/assistants/new/+page.svelte ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import type { ActionData, PageData } from "./$types";
3
+ import AssistantSettings from "$lib/components/AssistantSettings.svelte";
4
+
5
+ export let data: PageData;
6
+ export let form: ActionData;
7
+ </script>
8
+
9
+ <AssistantSettings bind:form models={data.models} />
static/fonts/Inter-Black.ttf ADDED
Binary file (317 kB). View file
 
static/fonts/Inter-Bold.ttf ADDED
Binary file (317 kB). View file
 
static/fonts/Inter-ExtraBold.ttf ADDED
Binary file (317 kB). View file
 
static/fonts/Inter-ExtraLight.ttf ADDED
Binary file (311 kB). View file
 
static/fonts/Inter-Light.ttf ADDED
Binary file (311 kB). View file
 
static/fonts/Inter-Medium.ttf ADDED
Binary file (315 kB). View file