diff --git a/.env b/.env
index 1265eede2f1349f95f3db3ada7bdae68ea0aa133..c14a1ae41b952cbcb807957619ca51a2c54e52c4 100644
--- a/.env
+++ b/.env
@@ -112,6 +112,7 @@ PARQUET_EXPORT_SECRET=
 RATE_LIMIT= # requests per minute
 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
 
+APP_BASE="" # base path of the app, e.g. /chat, left blank as default
 PUBLIC_APP_NAME=ChatUI # name used as title throughout the app
 PUBLIC_APP_ASSETS=chatui # used to find logos & favicons in static/$PUBLIC_APP_ASSETS
 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
 # PUBLIC_APP_COLOR=yellow
 # PUBLIC_APP_DESCRIPTION="Making the community's best AI chat models available to everyone."
 # PUBLIC_APP_DATA_SHARING=1
-# PUBLIC_APP_DISCLAIMER=1
\ No newline at end of file
+# PUBLIC_APP_DISCLAIMER=1
+
+ENABLE_ASSISTANTS=false #set to true to enable assistants feature
\ No newline at end of file
diff --git a/.env.template b/.env.template
index d8573a499c7af9e782dc1e23d43536d3a08dfcd6..8fe36ccac7129d69d95742a4f3567b83d6beb7e6 100644
--- a/.env.template
+++ b/.env.template
@@ -254,4 +254,5 @@ PUBLIC_GOOGLE_ANALYTICS_ID=G-8Q63TH4CSL
 # ADDRESS_HEADER=X-Forwarded-For
 # XFF_DEPTH=2
 
-EXPOSE_API=false
\ No newline at end of file
+ENABLE_ASSISTANTS=true
+EXPOSE_API=false
diff --git a/.vscode/settings.json b/.vscode/settings.json
index c32c1bbc3ef092e6702bb6ef83bf3fd87308d20b..0d24922796c50aa3b9c2007e1f04d02ad42174bf 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -2,7 +2,7 @@
 	"editor.formatOnSave": true,
 	"editor.defaultFormatter": "esbenp.prettier-vscode",
 	"editor.codeActionsOnSave": {
-		"source.fixAll": true
+		"source.fixAll": "explicit"
 	},
 	"eslint.validate": ["javascript", "svelte"]
 }
diff --git a/package-lock.json b/package-lock.json
index 23ca6bbbe3432f630b111e3242a365633b634ced..91688155b655aa66844b20097168b7f543446110 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,6 +11,7 @@
 				"@huggingface/hub": "^0.5.1",
 				"@huggingface/inference": "^2.6.3",
 				"@iconify-json/bi": "^1.1.21",
+				"@resvg/resvg-js": "^2.6.0",
 				"@xenova/transformers": "^2.6.0",
 				"autoprefixer": "^10.4.14",
 				"browser-image-resizer": "^2.4.1",
@@ -28,6 +29,8 @@
 				"parquetjs": "^0.11.2",
 				"postcss": "^8.4.31",
 				"saslprep": "^1.0.3",
+				"satori": "^0.10.11",
+				"satori-html": "^0.3.2",
 				"serpapi": "^1.1.1",
 				"tailwind-scrollbar": "^3.0.0",
 				"tailwindcss": "^3.4.0",
@@ -790,6 +793,208 @@
 			"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
 			"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="
 		},
+		"node_modules/@resvg/resvg-js": {
+			"version": "2.6.0",
+			"resolved": "https://registry.npmjs.org/@resvg/resvg-js/-/resvg-js-2.6.0.tgz",
+			"integrity": "sha512-Tf3YpbBKcQn991KKcw/vg7vZf98v01seSv6CVxZBbRkL/xyjnoYB6KgrFL6zskT1A4dWC/vg77KyNOW+ePaNlA==",
+			"engines": {
+				"node": ">= 10"
+			},
+			"optionalDependencies": {
+				"@resvg/resvg-js-android-arm-eabi": "2.6.0",
+				"@resvg/resvg-js-android-arm64": "2.6.0",
+				"@resvg/resvg-js-darwin-arm64": "2.6.0",
+				"@resvg/resvg-js-darwin-x64": "2.6.0",
+				"@resvg/resvg-js-linux-arm-gnueabihf": "2.6.0",
+				"@resvg/resvg-js-linux-arm64-gnu": "2.6.0",
+				"@resvg/resvg-js-linux-arm64-musl": "2.6.0",
+				"@resvg/resvg-js-linux-x64-gnu": "2.6.0",
+				"@resvg/resvg-js-linux-x64-musl": "2.6.0",
+				"@resvg/resvg-js-win32-arm64-msvc": "2.6.0",
+				"@resvg/resvg-js-win32-ia32-msvc": "2.6.0",
+				"@resvg/resvg-js-win32-x64-msvc": "2.6.0"
+			}
+		},
+		"node_modules/@resvg/resvg-js-android-arm-eabi": {
+			"version": "2.6.0",
+			"resolved": "https://registry.npmjs.org/@resvg/resvg-js-android-arm-eabi/-/resvg-js-android-arm-eabi-2.6.0.tgz",
+			"integrity": "sha512-lJnZ/2P5aMocrFMW7HWhVne5gH82I8xH6zsfH75MYr4+/JOaVcGCTEQ06XFohGMdYRP3v05SSPLPvTM/RHjxfA==",
+			"cpu": [
+				"arm"
+			],
+			"optional": true,
+			"os": [
+				"android"
+			],
+			"engines": {
+				"node": ">= 10"
+			}
+		},
+		"node_modules/@resvg/resvg-js-android-arm64": {
+			"version": "2.6.0",
+			"resolved": "https://registry.npmjs.org/@resvg/resvg-js-android-arm64/-/resvg-js-android-arm64-2.6.0.tgz",
+			"integrity": "sha512-N527f529bjMwYWShZYfBD60dXA4Fux+D695QsHQ93BDYZSHUoOh1CUGUyICevnTxs7VgEl98XpArmUWBZQVMfQ==",
+			"cpu": [
+				"arm64"
+			],
+			"optional": true,
+			"os": [
+				"android"
+			],
+			"engines": {
+				"node": ">= 10"
+			}
+		},
+		"node_modules/@resvg/resvg-js-darwin-arm64": {
+			"version": "2.6.0",
+			"resolved": "https://registry.npmjs.org/@resvg/resvg-js-darwin-arm64/-/resvg-js-darwin-arm64-2.6.0.tgz",
+			"integrity": "sha512-MabUKLVayEwlPo0mIqAmMt+qESN8LltCvv5+GLgVga1avpUrkxj/fkU1TKm8kQegutUjbP/B0QuMuUr0uhF8ew==",
+			"cpu": [
+				"arm64"
+			],
+			"optional": true,
+			"os": [
+				"darwin"
+			],
+			"engines": {
+				"node": ">= 10"
+			}
+		},
+		"node_modules/@resvg/resvg-js-darwin-x64": {
+			"version": "2.6.0",
+			"resolved": "https://registry.npmjs.org/@resvg/resvg-js-darwin-x64/-/resvg-js-darwin-x64-2.6.0.tgz",
+			"integrity": "sha512-zrFetdnSw/suXjmyxSjfDV7i61hahv6DDG6kM7BYN2yJ3Es5+BZtqYZTcIWogPJedYKmzN1YTMWGd/3f0ubFiA==",
+			"cpu": [
+				"x64"
+			],
+			"optional": true,
+			"os": [
+				"darwin"
+			],
+			"engines": {
+				"node": ">= 10"
+			}
+		},
+		"node_modules/@resvg/resvg-js-linux-arm-gnueabihf": {
+			"version": "2.6.0",
+			"resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-arm-gnueabihf/-/resvg-js-linux-arm-gnueabihf-2.6.0.tgz",
+			"integrity": "sha512-sH4gxXt7v7dGwjGyzLwn7SFGvwZG6DQqLaZ11MmzbCwd9Zosy1TnmrMJfn6TJ7RHezmQMgBPi18bl55FZ1AT4A==",
+			"cpu": [
+				"arm"
+			],
+			"optional": true,
+			"os": [
+				"linux"
+			],
+			"engines": {
+				"node": ">= 10"
+			}
+		},
+		"node_modules/@resvg/resvg-js-linux-arm64-gnu": {
+			"version": "2.6.0",
+			"resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-arm64-gnu/-/resvg-js-linux-arm64-gnu-2.6.0.tgz",
+			"integrity": "sha512-fCyMncqCJtrlANADIduYF4IfnWQ295UKib7DAxFXQhBsM9PLDTpizr0qemZcCNadcwSVHnAIzL4tliZhCM8P6A==",
+			"cpu": [
+				"arm64"
+			],
+			"optional": true,
+			"os": [
+				"linux"
+			],
+			"engines": {
+				"node": ">= 10"
+			}
+		},
+		"node_modules/@resvg/resvg-js-linux-arm64-musl": {
+			"version": "2.6.0",
+			"resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-arm64-musl/-/resvg-js-linux-arm64-musl-2.6.0.tgz",
+			"integrity": "sha512-ouLjTgBQHQyxLht4FdMPTvuY8xzJigM9EM2Tlu0llWkN1mKyTQrvYWi6TA6XnKdzDJHy7ZLpWpjZi7F5+Pg+Vg==",
+			"cpu": [
+				"arm64"
+			],
+			"optional": true,
+			"os": [
+				"linux"
+			],
+			"engines": {
+				"node": ">= 10"
+			}
+		},
+		"node_modules/@resvg/resvg-js-linux-x64-gnu": {
+			"version": "2.6.0",
+			"resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-x64-gnu/-/resvg-js-linux-x64-gnu-2.6.0.tgz",
+			"integrity": "sha512-n3zC8DWsvxC1AwxpKFclIPapDFibs5XdIRoV/mcIlxlh0vseW1F49b97F33BtJQRmlntsqqN6GMMqx8byB7B+Q==",
+			"cpu": [
+				"x64"
+			],
+			"optional": true,
+			"os": [
+				"linux"
+			],
+			"engines": {
+				"node": ">= 10"
+			}
+		},
+		"node_modules/@resvg/resvg-js-linux-x64-musl": {
+			"version": "2.6.0",
+			"resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-x64-musl/-/resvg-js-linux-x64-musl-2.6.0.tgz",
+			"integrity": "sha512-n4tasK1HOlAxdTEROgYA1aCfsEKk0UOFDNd/AQTTZlTmCbHKXPq+O8npaaKlwXquxlVK8vrkcWbksbiGqbCAcw==",
+			"cpu": [
+				"x64"
+			],
+			"optional": true,
+			"os": [
+				"linux"
+			],
+			"engines": {
+				"node": ">= 10"
+			}
+		},
+		"node_modules/@resvg/resvg-js-win32-arm64-msvc": {
+			"version": "2.6.0",
+			"resolved": "https://registry.npmjs.org/@resvg/resvg-js-win32-arm64-msvc/-/resvg-js-win32-arm64-msvc-2.6.0.tgz",
+			"integrity": "sha512-X2+EoBJFwDI5LDVb51Sk7ldnVLitMGr9WwU/i21i3fAeAXZb3hM16k67DeTy16OYkT2dk/RfU1tP1wG+rWbz2Q==",
+			"cpu": [
+				"arm64"
+			],
+			"optional": true,
+			"os": [
+				"win32"
+			],
+			"engines": {
+				"node": ">= 10"
+			}
+		},
+		"node_modules/@resvg/resvg-js-win32-ia32-msvc": {
+			"version": "2.6.0",
+			"resolved": "https://registry.npmjs.org/@resvg/resvg-js-win32-ia32-msvc/-/resvg-js-win32-ia32-msvc-2.6.0.tgz",
+			"integrity": "sha512-L7oevWjQoUgK5W1fCKn0euSVemhDXVhrjtwqpc7MwBKKimYeiOshO1Li1pa8bBt5PESahenhWgdB6lav9O0fEg==",
+			"cpu": [
+				"ia32"
+			],
+			"optional": true,
+			"os": [
+				"win32"
+			],
+			"engines": {
+				"node": ">= 10"
+			}
+		},
+		"node_modules/@resvg/resvg-js-win32-x64-msvc": {
+			"version": "2.6.0",
+			"resolved": "https://registry.npmjs.org/@resvg/resvg-js-win32-x64-msvc/-/resvg-js-win32-x64-msvc-2.6.0.tgz",
+			"integrity": "sha512-8lJlghb+Unki5AyKgsnFbRJwkEj9r1NpwyuBG8yEJiG1W9eEGl03R3I7bsVa3haof/3J1NlWf0rzSa1G++A2iw==",
+			"cpu": [
+				"x64"
+			],
+			"optional": true,
+			"os": [
+				"win32"
+			],
+			"engines": {
+				"node": ">= 10"
+			}
+		},
 		"node_modules/@rollup/plugin-commonjs": {
 			"version": "25.0.7",
 			"resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-25.0.7.tgz",
@@ -922,6 +1127,21 @@
 				}
 			}
 		},
+		"node_modules/@shuding/opentype.js": {
+			"version": "1.4.0-beta.0",
+			"resolved": "https://registry.npmjs.org/@shuding/opentype.js/-/opentype.js-1.4.0-beta.0.tgz",
+			"integrity": "sha512-3NgmNyH3l/Hv6EvsWJbsvpcpUba6R8IREQ83nH83cyakCw7uM1arZKNfHwv1Wz6jgqrF/j4x5ELvR6PnK9nTcA==",
+			"dependencies": {
+				"fflate": "^0.7.3",
+				"string.prototype.codepointat": "^0.2.1"
+			},
+			"bin": {
+				"ot": "bin/ot"
+			},
+			"engines": {
+				"node": ">= 8.0.0"
+			}
+		},
 		"node_modules/@sveltejs/adapter-node": {
 			"version": "1.3.1",
 			"resolved": "https://registry.npmjs.org/@sveltejs/adapter-node/-/adapter-node-1.3.1.tgz",
@@ -1931,6 +2151,14 @@
 				"node": ">= 6"
 			}
 		},
+		"node_modules/camelize": {
+			"version": "1.0.1",
+			"resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz",
+			"integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==",
+			"funding": {
+				"url": "https://github.com/sponsors/ljharb"
+			}
+		},
 		"node_modules/caniuse-lite": {
 			"version": "1.0.30001542",
 			"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001542.tgz",
@@ -2190,6 +2418,34 @@
 				"node": "*"
 			}
 		},
+		"node_modules/css-background-parser": {
+			"version": "0.1.0",
+			"resolved": "https://registry.npmjs.org/css-background-parser/-/css-background-parser-0.1.0.tgz",
+			"integrity": "sha512-2EZLisiZQ+7m4wwur/qiYJRniHX4K5Tc9w93MT3AS0WS1u5kaZ4FKXlOTBhOjc+CgEgPiGY+fX1yWD8UwpEqUA=="
+		},
+		"node_modules/css-box-shadow": {
+			"version": "1.0.0-3",
+			"resolved": "https://registry.npmjs.org/css-box-shadow/-/css-box-shadow-1.0.0-3.tgz",
+			"integrity": "sha512-9jaqR6e7Ohds+aWwmhe6wILJ99xYQbfmK9QQB9CcMjDbTxPZjwEmUQpU91OG05Xgm8BahT5fW+svbsQGjS/zPg=="
+		},
+		"node_modules/css-color-keywords": {
+			"version": "1.0.0",
+			"resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz",
+			"integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==",
+			"engines": {
+				"node": ">=4"
+			}
+		},
+		"node_modules/css-to-react-native": {
+			"version": "3.2.0",
+			"resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz",
+			"integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==",
+			"dependencies": {
+				"camelize": "^1.0.0",
+				"css-color-keywords": "^1.0.0",
+				"postcss-value-parser": "^4.0.2"
+			}
+		},
 		"node_modules/css-tree": {
 			"version": "2.3.1",
 			"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz",
@@ -2472,6 +2728,11 @@
 			"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.359.tgz",
 			"integrity": "sha512-OoVcngKCIuNXtZnsYoqlCvr0Cf3NIPzDIgwUfI9bdTFjXCrr79lI0kwQstLPZ7WhCezLlGksZk/BFAzoXC7GDw=="
 		},
+		"node_modules/emoji-regex": {
+			"version": "10.3.0",
+			"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz",
+			"integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw=="
+		},
 		"node_modules/end-of-stream": {
 			"version": "1.4.4",
 			"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
@@ -2542,6 +2803,11 @@
 				"node": ">=6"
 			}
 		},
+		"node_modules/escape-html": {
+			"version": "1.0.3",
+			"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+			"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
+		},
 		"node_modules/escape-string-regexp": {
 			"version": "4.0.0",
 			"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
@@ -2874,6 +3140,11 @@
 				"reusify": "^1.0.4"
 			}
 		},
+		"node_modules/fflate": {
+			"version": "0.7.4",
+			"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.7.4.tgz",
+			"integrity": "sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw=="
+		},
 		"node_modules/file-entry-cache": {
 			"version": "6.0.1",
 			"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
@@ -3184,6 +3455,17 @@
 				"node": ">= 0.4"
 			}
 		},
+		"node_modules/hex-rgb": {
+			"version": "4.3.0",
+			"resolved": "https://registry.npmjs.org/hex-rgb/-/hex-rgb-4.3.0.tgz",
+			"integrity": "sha512-Ox1pJVrDCyGHMG9CFg1tmrRUMRPRsAWYc/PinY0XzJU4K7y7vjNoLKIQ7BR5UJMCxNN8EM1MNDmHWA/B3aZUuw==",
+			"engines": {
+				"node": ">=6"
+			},
+			"funding": {
+				"url": "https://github.com/sponsors/sindresorhus"
+			}
+		},
 		"node_modules/highlight.js": {
 			"version": "11.7.0",
 			"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.7.0.tgz",
@@ -3682,6 +3964,23 @@
 				"node": ">=10"
 			}
 		},
+		"node_modules/linebreak": {
+			"version": "1.1.0",
+			"resolved": "https://registry.npmjs.org/linebreak/-/linebreak-1.1.0.tgz",
+			"integrity": "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==",
+			"dependencies": {
+				"base64-js": "0.0.8",
+				"unicode-trie": "^2.0.0"
+			}
+		},
+		"node_modules/linebreak/node_modules/base64-js": {
+			"version": "0.0.8",
+			"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz",
+			"integrity": "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==",
+			"engines": {
+				"node": ">= 0.4"
+			}
+		},
 		"node_modules/lines-and-columns": {
 			"version": "1.2.4",
 			"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
@@ -4417,6 +4716,11 @@
 				"url": "https://github.com/sponsors/sindresorhus"
 			}
 		},
+		"node_modules/pako": {
+			"version": "0.2.9",
+			"resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz",
+			"integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA=="
+		},
 		"node_modules/parent-module": {
 			"version": "1.0.1",
 			"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@@ -4457,6 +4761,15 @@
 				"node": ">=0.6.19"
 			}
 		},
+		"node_modules/parse-css-color": {
+			"version": "0.2.1",
+			"resolved": "https://registry.npmjs.org/parse-css-color/-/parse-css-color-0.2.1.tgz",
+			"integrity": "sha512-bwS/GGIFV3b6KS4uwpzCFj4w297Yl3uqnSgIPsoQkx7GMLROXfMnWvxfNkL0oh8HVhZA4hvJoEoEIqonfJ3BWg==",
+			"dependencies": {
+				"color-name": "^1.1.4",
+				"hex-rgb": "^4.1.0"
+			}
+		},
 		"node_modules/parse5": {
 			"version": "7.1.2",
 			"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz",
@@ -5290,6 +5603,34 @@
 				"node": ">=6"
 			}
 		},
+		"node_modules/satori": {
+			"version": "0.10.11",
+			"resolved": "https://registry.npmjs.org/satori/-/satori-0.10.11.tgz",
+			"integrity": "sha512-yLm1xPRPZUaKcBZJ6nmezoJjHB4MqV8x7Mu0PyZUJodRWRDD27UbeMwzuY9LEGG57WYLO4CQsGPlbHWV1Ex9TQ==",
+			"dependencies": {
+				"@shuding/opentype.js": "1.4.0-beta.0",
+				"css-background-parser": "^0.1.0",
+				"css-box-shadow": "1.0.0-3",
+				"css-to-react-native": "^3.0.0",
+				"emoji-regex": "^10.2.1",
+				"escape-html": "^1.0.3",
+				"linebreak": "^1.1.0",
+				"parse-css-color": "^0.2.1",
+				"postcss-value-parser": "^4.2.0",
+				"yoga-wasm-web": "^0.3.3"
+			},
+			"engines": {
+				"node": ">=16"
+			}
+		},
+		"node_modules/satori-html": {
+			"version": "0.3.2",
+			"resolved": "https://registry.npmjs.org/satori-html/-/satori-html-0.3.2.tgz",
+			"integrity": "sha512-wjTh14iqADFKDK80e51/98MplTGfxz2RmIzh0GqShlf4a67+BooLywF17TvJPD6phO0Hxm7Mf1N5LtRYvdkYRA==",
+			"dependencies": {
+				"ultrahtml": "^1.2.0"
+			}
+		},
 		"node_modules/saxes": {
 			"version": "6.0.0",
 			"resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
@@ -5553,6 +5894,11 @@
 				"safe-buffer": "~5.2.0"
 			}
 		},
+		"node_modules/string.prototype.codepointat": {
+			"version": "0.2.1",
+			"resolved": "https://registry.npmjs.org/string.prototype.codepointat/-/string.prototype.codepointat-0.2.1.tgz",
+			"integrity": "sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg=="
+		},
 		"node_modules/strip-ansi": {
 			"version": "6.0.1",
 			"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
@@ -6054,6 +6400,11 @@
 				"globrex": "^0.1.2"
 			}
 		},
+		"node_modules/tiny-inflate": {
+			"version": "1.0.3",
+			"resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz",
+			"integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="
+		},
 		"node_modules/tinybench": {
 			"version": "2.5.0",
 			"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.5.0.tgz",
@@ -6270,6 +6621,11 @@
 				"node": ">=0.8.0"
 			}
 		},
+		"node_modules/ultrahtml": {
+			"version": "1.5.2",
+			"resolved": "https://registry.npmjs.org/ultrahtml/-/ultrahtml-1.5.2.tgz",
+			"integrity": "sha512-qh4mBffhlkiXwDAOxvSGxhL0QEQsTbnP9BozOK3OYPEGvPvdWzvAUaXNtUSMdNsKDtuyjEbyVUPFZ52SSLhLqw=="
+		},
 		"node_modules/undici": {
 			"version": "5.26.4",
 			"resolved": "https://registry.npmjs.org/undici/-/undici-5.26.4.tgz",
@@ -6281,6 +6637,15 @@
 				"node": ">=14.0"
 			}
 		},
+		"node_modules/unicode-trie": {
+			"version": "2.0.0",
+			"resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz",
+			"integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==",
+			"dependencies": {
+				"pako": "^0.2.5",
+				"tiny-inflate": "^1.0.0"
+			}
+		},
 		"node_modules/universalify": {
 			"version": "0.2.0",
 			"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz",
@@ -6769,6 +7134,11 @@
 				"url": "https://github.com/sponsors/sindresorhus"
 			}
 		},
+		"node_modules/yoga-wasm-web": {
+			"version": "0.3.3",
+			"resolved": "https://registry.npmjs.org/yoga-wasm-web/-/yoga-wasm-web-0.3.3.tgz",
+			"integrity": "sha512-N+d4UJSJbt/R3wqY7Coqs5pcV0aUj2j9IaQ3rNj9bVCLld8tTGKRa2USARjnvZJWVx1NDmQev8EknoczaOQDOA=="
+		},
 		"node_modules/zod": {
 			"version": "3.22.3",
 			"resolved": "https://registry.npmjs.org/zod/-/zod-3.22.3.tgz",
diff --git a/package.json b/package.json
index 8d56863dfcddd0c0c73fd570264cf824f9bf3a87..5f2c4e86d1612d7c019d450815c2565299146e1c 100644
--- a/package.json
+++ b/package.json
@@ -47,6 +47,7 @@
 		"@huggingface/hub": "^0.5.1",
 		"@huggingface/inference": "^2.6.3",
 		"@iconify-json/bi": "^1.1.21",
+		"@resvg/resvg-js": "^2.6.0",
 		"@xenova/transformers": "^2.6.0",
 		"autoprefixer": "^10.4.14",
 		"browser-image-resizer": "^2.4.1",
@@ -64,6 +65,8 @@
 		"parquetjs": "^0.11.2",
 		"postcss": "^8.4.31",
 		"saslprep": "^1.0.3",
+		"satori": "^0.10.11",
+		"satori-html": "^0.3.2",
 		"serpapi": "^1.1.1",
 		"tailwind-scrollbar": "^3.0.0",
 		"tailwindcss": "^3.4.0",
diff --git a/src/lib/components/AssistantSettings.svelte b/src/lib/components/AssistantSettings.svelte
new file mode 100644
index 0000000000000000000000000000000000000000..85394e8d6cf7d3550bbe6f540997d62da91fa244
--- /dev/null
+++ b/src/lib/components/AssistantSettings.svelte
@@ -0,0 +1,277 @@
+<script lang="ts">
+	import type { readAndCompressImage } from "browser-image-resizer";
+	import type { Model } from "$lib/types/Model";
+	import type { Assistant } from "$lib/types/Assistant";
+
+	import { onMount } from "svelte";
+	import { applyAction, enhance } from "$app/forms";
+	import { base } from "$app/paths";
+	import CarbonPen from "~icons/carbon/pen";
+	import CarbonUpload from "~icons/carbon/upload";
+	import { useSettingsStore } from "$lib/stores/settings";
+	import IconLoading from "./icons/IconLoading.svelte";
+
+	type ActionData = {
+		error: boolean;
+		errors: {
+			field: string | number;
+			message: string;
+		}[];
+	} | null;
+
+	type AssistantFront = Omit<Assistant, "_id" | "createdById"> & { _id: string };
+
+	export let form: ActionData;
+	export let assistant: AssistantFront | undefined = undefined;
+	export let models: Model[] = [];
+
+	let files: FileList | null = null;
+
+	const settings = useSettingsStore();
+
+	let compress: typeof readAndCompressImage | null = null;
+
+	onMount(async () => {
+		const module = await import("browser-image-resizer");
+		compress = module.readAndCompressImage;
+	});
+
+	let inputMessage1 = assistant?.exampleInputs[0] ?? "";
+	let inputMessage2 = assistant?.exampleInputs[1] ?? "";
+	let inputMessage3 = assistant?.exampleInputs[2] ?? "";
+	let inputMessage4 = assistant?.exampleInputs[3] ?? "";
+
+	function resetErrors() {
+		if (form) {
+			form.errors = [];
+			form.error = false;
+		}
+	}
+
+	function onFilesChange(e: Event) {
+		const inputEl = e.target as HTMLInputElement;
+		if (inputEl.files?.length) {
+			files = inputEl.files;
+			resetErrors();
+			deleteExistingAvatar = false;
+		}
+	}
+
+	function getError(field: string, returnForm: ActionData) {
+		return returnForm?.errors.find((error) => error.field === field)?.message ?? "";
+	}
+
+	let deleteExistingAvatar = false;
+
+	let loading = false;
+</script>
+
+<form
+	method="POST"
+	class="flex h-full flex-col"
+	enctype="multipart/form-data"
+	use:enhance={async ({ formData }) => {
+		loading = true;
+		if (files?.[0] && files[0].size > 0 && compress) {
+			await compress(files[0], {
+				maxWidth: 500,
+				maxHeight: 500,
+				quality: 1,
+			}).then((resizedImage) => {
+				formData.set("avatar", resizedImage);
+			});
+		}
+
+		if (deleteExistingAvatar === true) {
+			if (assistant?.avatar) {
+				// if there is an avatar we explicitly removei t
+				formData.set("avatar", "null");
+			} else {
+				// else we just remove it from the input
+				formData.delete("avatar");
+			}
+		}
+
+		return async ({ result }) => {
+			loading = false;
+			await applyAction(result);
+		};
+	}}
+>
+	{#if assistant}
+		<h2 class="text-xl font-semibold">Edit assistant ({assistant?.name ?? ""})</h2>
+		<p class="mb-6 text-sm text-gray-500">
+			Modifying an existing assistant will propagate those changes to all users.
+		</p>
+	{:else}
+		<h2 class="text-xl font-semibold">Create new assistant</h2>
+		<p class="mb-6 text-sm text-gray-500">
+			Assistants are public, and can be accessed by anyone with the link.
+		</p>
+	{/if}
+
+	<div class="mx-1 grid flex-1 grid-cols-2 gap-4 max-sm:grid-cols-1">
+		<div class="flex flex-col gap-4">
+			<div>
+				<span class="mb-1 block pb-2 text-sm font-semibold">Avatar</span>
+				<input
+					type="file"
+					accept="image/*"
+					name="avatar"
+					id="avatar"
+					class="hidden"
+					on:change={onFilesChange}
+				/>
+
+				{#if (files && files[0]) || (assistant?.avatar && !deleteExistingAvatar)}
+					<div class="group relative mx-auto h-12 w-12">
+						{#if files && files[0]}
+							<img
+								src={URL.createObjectURL(files[0])}
+								alt="avatar"
+								class="crop mx-auto h-12 w-12 cursor-pointer rounded-full object-cover"
+							/>
+						{:else if assistant?.avatar}
+							<img
+								src="{base}/settings/assistants/{assistant._id}/avatar?hash={assistant.avatar}"
+								alt="avatar"
+								class="crop mx-auto h-12 w-12 cursor-pointer rounded-full object-cover"
+							/>
+						{/if}
+
+						<label
+							for="avatar"
+							class="invisible absolute bottom-0 h-12 w-12 rounded-full bg-black bg-opacity-50 p-1 group-hover:visible hover:visible"
+						>
+							<CarbonPen class="mx-auto my-auto h-full cursor-pointer text-center text-white" />
+						</label>
+					</div>
+					<div class="mx-auto w-max pt-1">
+						<button
+							type="button"
+							on:click|stopPropagation|preventDefault={() => {
+								files = null;
+								deleteExistingAvatar = true;
+							}}
+							class="mx-auto w-max text-center text-xs text-gray-600 hover:underline"
+						>
+							Delete
+						</button>
+					</div>
+				{:else}
+					<div class="mb-1 flex w-max flex-row gap-4">
+						<label
+							for="avatar"
+							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"
+						>
+							<CarbonUpload class="mr-2 text-xs " /> Upload
+						</label>
+					</div>
+					<p class="text-xs text-red-500">{getError("avatar", form)}</p>
+				{/if}
+			</div>
+
+			<label>
+				<span class="mb-1 text-sm font-semibold">Name</span>
+				<input
+					name="name"
+					class="w-full rounded-lg border-2 border-gray-200 bg-gray-100 p-2"
+					placeholder="My awesome model"
+					value={assistant?.name ?? ""}
+				/>
+				<p class="text-xs text-red-500">{getError("name", form)}</p>
+			</label>
+
+			<label>
+				<span class="mb-1 text-sm font-semibold">Description</span>
+				<textarea
+					name="description"
+					class="w-full rounded-lg border-2 border-gray-200 bg-gray-100 p-2"
+					placeholder="He knows everything about python"
+					value={assistant?.description ?? ""}
+				/>
+				<p class="text-xs text-red-500">{getError("description", form)}</p>
+			</label>
+
+			<label>
+				<span class="mb-1 text-sm font-semibold">Model</span>
+				<select name="modelId" class="w-full rounded-lg border-2 border-gray-200 bg-gray-100 p-2">
+					{#each models as model}
+						<option
+							value={model.id}
+							selected={assistant
+								? assistant?.modelId === model.id
+								: $settings.activeModel === model.id}>{model.displayName}</option
+						>
+					{/each}
+					<p class="text-xs text-red-500">{getError("modelId", form)}</p>
+				</select>
+			</label>
+
+			<label>
+				<span class="mb-1 text-sm font-semibold">Start messages</span>
+				<div class="flex flex-col gap-2 md:max-h-32">
+					<input
+						name="exampleInput1"
+						bind:value={inputMessage1}
+						class="w-full rounded-lg border-2 border-gray-200 bg-gray-100 p-2"
+					/>
+					{#if !!inputMessage1 || !!inputMessage2}
+						<input
+							name="exampleInput2"
+							bind:value={inputMessage2}
+							class="w-full rounded-lg border-2 border-gray-200 bg-gray-100 p-2"
+						/>
+					{/if}
+					{#if !!inputMessage2 || !!inputMessage3}
+						<input
+							name="exampleInput3"
+							bind:value={inputMessage3}
+							class="w-full rounded-lg border-2 border-gray-200 bg-gray-100 p-2"
+						/>
+					{/if}
+					{#if !!inputMessage3 || !!inputMessage4}
+						<input
+							name="exampleInput4"
+							bind:value={inputMessage4}
+							class="w-full rounded-lg border-2 border-gray-200 bg-gray-100 p-2"
+						/>
+					{/if}
+				</div>
+				<p class="text-xs text-red-500">{getError("inputMessage1", form)}</p>
+			</label>
+		</div>
+
+		<label class="flex flex-col">
+			<span class="mb-1 text-sm font-semibold"> Instructions (system prompt) </span>
+			<textarea
+				name="preprompt"
+				class="min-h-[8lh] flex-1 rounded-lg border-2 border-gray-200 bg-gray-100 p-2 text-sm"
+				placeholder="You'll act as..."
+				value={assistant?.preprompt ?? ""}
+			/>
+			<p class="text-xs text-red-500">{getError("preprompt", form)}</p>
+		</label>
+	</div>
+
+	<div class="mt-5 flex justify-end gap-2">
+		<a
+			href={assistant ? `${base}/settings/assistants/${assistant?._id}` : `${base}/settings`}
+			class="rounded-full bg-gray-200 px-8 py-2 font-semibold text-gray-600">Cancel</a
+		>
+		<button
+			type="submit"
+			disabled={loading}
+			aria-disabled={loading}
+			class="rounded-full bg-black px-8 py-2 font-semibold md:px-20"
+			class:bg-gray-200={loading}
+			class:text-gray-600={loading}
+			class:text-white={!loading}
+		>
+			{assistant ? "Save" : "Create"}
+			{#if loading}
+				<IconLoading classNames="ml-2 h-min" />
+			{/if}
+		</button>
+	</div>
+</form>
diff --git a/src/lib/components/DisclaimerModal.svelte b/src/lib/components/DisclaimerModal.svelte
index 7837869d6f28c20df00c15224c401ac1f4c95b70..ecb5f22a895417308bef132a1c1fcc12a52debe5 100644
--- a/src/lib/components/DisclaimerModal.svelte
+++ b/src/lib/components/DisclaimerModal.svelte
@@ -36,9 +36,8 @@
 					class:bg-white={$page.data.loginEnabled}
 					class:text-gray-800={$page.data.loginEnabled}
 					class:hover:bg-slate-100={$page.data.loginEnabled}
-					on:click={(e) => {
+					on:click|preventDefault|stopPropagation={() => {
 						if (!cookiesAreEnabled()) {
-							e.preventDefault();
 							window.open(window.location.href, "_blank");
 						}
 
diff --git a/src/lib/components/LoginModal.svelte b/src/lib/components/LoginModal.svelte
index 1f239644dddb4f896f1acc275d3aa19f513bb410..c333fdc0eec6cd10e9112c6d19e01d81fe99f511 100644
--- a/src/lib/components/LoginModal.svelte
+++ b/src/lib/components/LoginModal.svelte
@@ -51,7 +51,6 @@
 							e.preventDefault();
 							window.open(window.location.href, "_blank");
 						}
-
 						$settings.ethicsModalAccepted = true;
 					}}
 				>
diff --git a/src/lib/components/NavConversationItem.svelte b/src/lib/components/NavConversationItem.svelte
index b786856c3041b9602671636073c66947c53270c4..ce5325f0d803afd60abc08b044109adb75c67cb6 100644
--- a/src/lib/components/NavConversationItem.svelte
+++ b/src/lib/components/NavConversationItem.svelte
@@ -7,8 +7,10 @@
 	import CarbonTrashCan from "~icons/carbon/trash-can";
 	import CarbonClose from "~icons/carbon/close";
 	import CarbonEdit from "~icons/carbon/edit";
+	import { useSettingsStore } from "$lib/stores/settings";
+	import type { ConvSidebar } from "$lib/types/ConvSidebar";
 
-	export let conv: { id: string; title: string };
+	export let conv: ConvSidebar;
 
 	let confirmDelete = false;
 
@@ -16,6 +18,8 @@
 		deleteConversation: string;
 		editConversationTitle: { id: string; title: string };
 	}>();
+
+	const settings = useSettingsStore();
 </script>
 
 <a
@@ -29,11 +33,25 @@
 		? 'bg-gray-100 dark:bg-gray-700'
 		: ''}"
 >
-	<div class="flex-1 truncate">
+	<div class="flex flex-1 items-center truncate">
 		{#if confirmDelete}
-			<span class="font-semibold"> Delete </span>
+			<span class="mr-1 font-semibold"> Delete </span>
+		{/if}
+		{#if conv.avatarHash && !$settings.hideEmojiOnSidebar}
+			<img
+				src="{base}/settings/assistants/{conv.assistantId}/avatar?hash={conv.avatarHash}"
+				alt="Assistant avatar"
+				class="mr-1.5 inline size-4 rounded-full object-cover"
+			/>
+			{conv.title.replace(/\p{Emoji}/gu, "")}
+		{:else if conv.assistantId}
+			<div
+				class="mr-1.5 flex size-4 items-center justify-center rounded-full bg-gray-300 text-xs font-bold uppercase text-gray-500"
+			/>
+			{conv.title.replace(/\p{Emoji}/gu, "")}
+		{:else}
+			{conv.title}
 		{/if}
-		{conv.title}
 	</div>
 
 	{#if confirmDelete}
diff --git a/src/lib/components/NavMenu.svelte b/src/lib/components/NavMenu.svelte
index b7754727efa7caf9a4199ece694a2834a93b401d..3d3975875054ee195bad75e25af303f130c766e1 100644
--- a/src/lib/components/NavMenu.svelte
+++ b/src/lib/components/NavMenu.svelte
@@ -7,14 +7,9 @@
 	import { PUBLIC_APP_NAME, PUBLIC_ORIGIN } from "$env/static/public";
 	import NavConversationItem from "./NavConversationItem.svelte";
 	import type { LayoutData } from "../../routes/$types";
+	import type { ConvSidebar } from "$lib/types/ConvSidebar";
 
-	interface Conv {
-		id: string;
-		title: string;
-		updatedAt: Date;
-	}
-
-	export let conversations: Array<Conv> = [];
+	export let conversations: ConvSidebar[] = [];
 	export let canLogin: boolean;
 	export let user: LayoutData["user"];
 
diff --git a/src/lib/components/chat/AssistantIntroduction.svelte b/src/lib/components/chat/AssistantIntroduction.svelte
new file mode 100644
index 0000000000000000000000000000000000000000..4845241f139b9705a3a6ac6cca0fde3e7336d4af
--- /dev/null
+++ b/src/lib/components/chat/AssistantIntroduction.svelte
@@ -0,0 +1,85 @@
+<script lang="ts">
+	import { createEventDispatcher } from "svelte";
+	import IconGear from "~icons/bi/gear-fill";
+	import { base } from "$app/paths";
+	import type { Assistant } from "$lib/types/Assistant";
+
+	export let assistant: Pick<
+		Assistant,
+		"avatar" | "name" | "modelId" | "createdByName" | "exampleInputs" | "_id" | "description"
+	>;
+
+	const dispatch = createEventDispatcher<{ message: string }>();
+</script>
+
+<div class="flex h-full w-full flex-col content-center items-center justify-center">
+	<div
+		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"
+	>
+		<div class="flex items-center gap-4 p-4 pr-10 md:p-8 md:pt-10">
+			{#if assistant.avatar}
+				<img
+					src={`${base}/settings/assistants/${assistant._id.toString()}/avatar?hash=${
+						assistant.avatar
+					}`}
+					alt="avatar"
+					class="size-16 rounded-full object-cover md:size-32"
+				/>
+			{:else}
+				<div
+					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"
+				>
+					{assistant?.name[0]}
+				</div>
+			{/if}
+
+			<div class="flex h-full flex-col">
+				<p
+					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"
+				>
+					Assistant
+				</p>
+				<p class="text-xl font-bold sm:text-2xl">{assistant.name}</p>
+				<p class="text-sm text-gray-500 dark:text-gray-400">
+					{assistant.description}
+				</p>
+
+				{#if assistant.createdByName}
+					<p class="pt-2 text-sm text-gray-400 dark:text-gray-500">
+						Created by <a
+							class="hover:underline"
+							href="https://hf.co/{assistant.createdByName}"
+							target="_blank"
+						>
+							{assistant.createdByName}
+						</a>
+					</p>
+				{/if}
+			</div>
+		</div>
+		<div class="absolute right-2 top-3 sm:top-2">
+			<a
+				href="{base}/settings/assistants/{assistant._id.toString()}"
+				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"
+				><IconGear /></a
+			>
+		</div>
+	</div>
+	{#if assistant.exampleInputs}
+		<div class="mx-auto mt-auto w-full gap-8 sm:-mb-8">
+			<div class="md:col-span-2 md:mt-6">
+				<div class="grid grid-cols-1 gap-3 md:grid-cols-2">
+					{#each assistant.exampleInputs as example}
+						<button
+							type="button"
+							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"
+							on:click={() => dispatch("message", example)}
+						>
+							{example}
+						</button>
+					{/each}
+				</div>
+			</div>
+		</div>
+	{/if}
+</div>
diff --git a/src/lib/components/chat/ChatMessages.svelte b/src/lib/components/chat/ChatMessages.svelte
index 5031d280efc1b0600ac4e2a9b19fb52838b6da41..f938fd441ec855187788e52a303a33ba47b5f1a9 100644
--- a/src/lib/components/chat/ChatMessages.svelte
+++ b/src/lib/components/chat/ChatMessages.svelte
@@ -10,12 +10,17 @@
 	import type { WebSearchUpdate } from "$lib/types/MessageUpdate";
 	import { browser } from "$app/environment";
 	import SystemPromptModal from "../SystemPromptModal.svelte";
+	import type { Assistant } from "$lib/types/Assistant";
+	import AssistantIntroduction from "./AssistantIntroduction.svelte";
+	import { page } from "$app/stores";
+	import { base } from "$app/paths";
 
 	export let messages: Message[];
 	export let loading: boolean;
 	export let pending: boolean;
 	export let isAuthor: boolean;
 	export let currentModel: Model;
+	export let assistant: Assistant | undefined;
 	export let models: Model[];
 	export let preprompt: string | undefined;
 	export let readOnly: boolean;
@@ -42,7 +47,29 @@
 >
 	<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">
 		{#each messages as message, i}
-			{#if i === 0 && preprompt && preprompt != currentModel.preprompt}
+			{#if i === 0 && $page.data?.assistant}
+				<a
+					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"
+					href="{base}/settings/assistants/{$page.data.assistant._id}"
+				>
+					{#if $page.data?.assistant.avatar}
+						<img
+							src="{base}/settings/assistants/{$page.data?.assistant._id.toString()}/avatar?hash=${$page
+								.data?.assistant.avatar}"
+							alt="Avatar"
+							class="size-5 rounded-full object-cover"
+						/>
+					{:else}
+						<div
+							class="flex size-6 items-center justify-center rounded-full bg-gray-300 font-bold uppercase text-gray-500"
+						>
+							{$page.data?.assistant.name[0]}
+						</div>
+					{/if}
+
+					{$page.data.assistant.name}
+				</a>
+			{:else if i === 0 && preprompt && preprompt != currentModel.preprompt}
 				<SystemPromptModal preprompt={preprompt ?? ""} />
 			{/if}
 			<ChatMessage
@@ -57,7 +84,11 @@
 				on:continue
 			/>
 		{:else}
-			<ChatIntroduction {models} {currentModel} on:message />
+			{#if !assistant}
+				<ChatIntroduction {models} {currentModel} on:message />
+			{:else}
+				<AssistantIntroduction {assistant} on:message />
+			{/if}
 		{/each}
 		{#if pending && messages[messages.length - 1]?.from === "user"}
 			<ChatMessage
diff --git a/src/lib/components/chat/ChatWindow.svelte b/src/lib/components/chat/ChatWindow.svelte
index 691b9c3391a79ac342f6e1691cd3995dfe4e48b3..e402aae5e754c40044dd12cf1e7e25e794025f86 100644
--- a/src/lib/components/chat/ChatWindow.svelte
+++ b/src/lib/components/chat/ChatWindow.svelte
@@ -18,12 +18,12 @@
 	import LoginModal from "../LoginModal.svelte";
 	import type { WebSearchUpdate } from "$lib/types/MessageUpdate";
 	import { page } from "$app/stores";
-	import DisclaimerModal from "../DisclaimerModal.svelte";
 	import FileDropzone from "./FileDropzone.svelte";
 	import RetryBtn from "../RetryBtn.svelte";
 	import UploadBtn from "../UploadBtn.svelte";
 	import file2base64 from "$lib/utils/file2base64";
-	import { useSettingsStore } from "$lib/stores/settings";
+	import type { Assistant } from "$lib/types/Assistant";
+	import { base } from "$app/paths";
 	import ContinueBtn from "../ContinueBtn.svelte";
 
 	export let messages: Message[] = [];
@@ -32,6 +32,7 @@
 	export let shared = false;
 	export let currentModel: Model;
 	export let models: Model[];
+	export let assistant: Assistant | undefined = undefined;
 	export let webSearchMessages: WebSearchUpdate[] = [];
 	export let preprompt: string | undefined = undefined;
 	export let files: File[] = [];
@@ -78,8 +79,6 @@
 
 	$: sources = files.map((file) => file2base64(file));
 
-	const settings = useSettingsStore();
-
 	function onShare() {
 		dispatch("share");
 		isSharedRecently = true;
@@ -99,9 +98,7 @@
 </script>
 
 <div class="relative min-h-0 min-w-0">
-	{#if !$settings.ethicsModalAccepted}
-		<DisclaimerModal />
-	{:else if loginModalOpen}
+	{#if loginModalOpen}
 		<LoginModal
 			on:close={() => {
 				loginModalOpen = false;
@@ -113,6 +110,7 @@
 		{pending}
 		{currentModel}
 		{models}
+		{assistant}
 		{messages}
 		readOnly={isReadOnly}
 		isAuthor={!shared}
@@ -162,7 +160,7 @@
 
 		<div class="w-full">
 			<div class="flex w-full pb-3">
-				{#if $page.data.settings?.searchEnabled}
+				{#if $page.data.settings?.searchEnabled && !assistant}
 					<WebSearchToggle />
 				{/if}
 				{#if loading}
@@ -252,13 +250,16 @@
 				class="mt-2 flex justify-between self-stretch px-1 text-xs text-gray-400/90 max-md:mb-2 max-sm:gap-2"
 			>
 				<p>
-					Model: <a
-						href={currentModel.modelUrl || "https://huggingface.co/" + currentModel.name}
-						target="_blank"
-						rel="noreferrer"
-						class="hover:underline">{currentModel.displayName}</a
-					> <span class="max-sm:hidden">·</span><br class="sm:hidden" /> Generated content may be inaccurate
-					or false.
+					Model:
+					{#if !assistant}
+						<a href="{base}/settings/{currentModel.id}" class="hover:underline"
+							>{currentModel.displayName}</a
+						>{:else}
+						{@const model = models.find((m) => m.id === assistant?.modelId)}
+						<a href="{base}/settings/assistants/{assistant._id}" class="hover:underline"
+							>{model?.displayName}</a
+						>{/if} <span class="max-sm:hidden">·</span><br class="sm:hidden" /> Generated content may
+					be inaccurate or false.
 				</p>
 				{#if messages.length}
 					<button
diff --git a/src/lib/server/database.ts b/src/lib/server/database.ts
index 39d0c6a52732c930b62c4c20a8ad11fb5613613c..7facc7ada38889bc13b00614cd652ee4d57371fb 100644
--- a/src/lib/server/database.ts
+++ b/src/lib/server/database.ts
@@ -7,6 +7,8 @@ import type { Settings } from "$lib/types/Settings";
 import type { User } from "$lib/types/User";
 import type { MessageEvent } from "$lib/types/MessageEvent";
 import type { Session } from "$lib/types/Session";
+import type { Assistant } from "$lib/types/Assistant";
+import type { Report } from "$lib/types/Report";
 
 if (!MONGODB_URL) {
 	throw new Error(
@@ -23,6 +25,8 @@ export const connectPromise = client.connect().catch(console.error);
 const db = client.db(MONGODB_DB_NAME + (import.meta.env.MODE === "test" ? "-test" : ""));
 
 const conversations = db.collection<Conversation>("conversations");
+const assistants = db.collection<Assistant>("assistants");
+const reports = db.collection<Report>("reports");
 const sharedConversations = db.collection<SharedConversation>("sharedConversations");
 const abortedGenerations = db.collection<AbortedGeneration>("abortedGenerations");
 const settings = db.collection<Settings>("settings");
@@ -34,6 +38,8 @@ const bucket = new GridFSBucket(db, { bucketName: "files" });
 export { client, db };
 export const collections = {
 	conversations,
+	assistants,
+	reports,
 	sharedConversations,
 	abortedGenerations,
 	settings,
@@ -66,4 +72,6 @@ client.on("open", () => {
 	messageEvents.createIndex({ createdAt: 1 }, { expireAfterSeconds: 60 }).catch(console.error);
 	sessions.createIndex({ expiresAt: 1 }, { expireAfterSeconds: 0 }).catch(console.error);
 	sessions.createIndex({ sessionId: 1 }, { unique: true }).catch(console.error);
+	assistants.createIndex({ createdBy: 1 }).catch(console.error);
+	reports.createIndex({ assistantId: 1 }).catch(console.error);
 });
diff --git a/src/lib/stores/settings.ts b/src/lib/stores/settings.ts
index 18c55ce6e54a65e23fa377cfb7daec97db747cf1..4839c4fa8e76753986eb3e432d0d1f7fa6d733c7 100644
--- a/src/lib/stores/settings.ts
+++ b/src/lib/stores/settings.ts
@@ -2,6 +2,7 @@ import { browser } from "$app/environment";
 import { invalidate } from "$app/navigation";
 import { base } from "$app/paths";
 import { UrlDependency } from "$lib/types/UrlDependency";
+import type { ObjectId } from "mongodb";
 import { getContext, setContext } from "svelte";
 import { type Writable, writable, get } from "svelte/store";
 
@@ -13,7 +14,9 @@ type SettingsStore = {
 	activeModel: string;
 	customPrompts: Record<string, string>;
 	recentlySaved: boolean;
+	assistants: Array<ObjectId | string>;
 };
+
 export function useSettingsStore() {
 	return getContext<Writable<SettingsStore>>("settings");
 }
@@ -44,6 +47,7 @@ export function createSettingsStore(initialValue: Omit<SettingsStore, "recentlyS
 					}),
 				});
 
+				invalidate(UrlDependency.ConversationList);
 				// set savedRecently to true for 3s
 				baseStore.update((s) => ({
 					...s,
diff --git a/src/lib/types/Assistant.ts b/src/lib/types/Assistant.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f9f64c79fd92988a4c729ce708464fc43eb0c8b7
--- /dev/null
+++ b/src/lib/types/Assistant.ts
@@ -0,0 +1,15 @@
+import type { ObjectId } from "mongodb";
+import type { User } from "./User";
+import type { Timestamps } from "./Timestamps";
+
+export interface Assistant extends Timestamps {
+	_id: ObjectId;
+	createdById: User["_id"] | string; // user id or session
+	createdByName?: User["username"];
+	avatar?: string;
+	name: string;
+	description?: string;
+	modelId: string;
+	exampleInputs: string[];
+	preprompt: string;
+}
diff --git a/src/lib/types/ConvSidebar.ts b/src/lib/types/ConvSidebar.ts
new file mode 100644
index 0000000000000000000000000000000000000000..ac0e38583fab047a9aaf0775ad2b4272d33e749a
--- /dev/null
+++ b/src/lib/types/ConvSidebar.ts
@@ -0,0 +1,8 @@
+export interface ConvSidebar {
+	id: string;
+	title: string;
+	updatedAt: Date;
+	model?: string;
+	assistantId?: string;
+	avatarHash?: string;
+}
diff --git a/src/lib/types/Conversation.ts b/src/lib/types/Conversation.ts
index 665a688f6b4bc3151d4701a4c4893b7bf4f80ee1..aa34426b4cc87fe44c9551542e56c431e5253c59 100644
--- a/src/lib/types/Conversation.ts
+++ b/src/lib/types/Conversation.ts
@@ -2,6 +2,7 @@ import type { ObjectId } from "mongodb";
 import type { Message } from "./Message";
 import type { Timestamps } from "./Timestamps";
 import type { User } from "./User";
+import type { Assistant } from "./Assistant";
 
 export interface Conversation extends Timestamps {
 	_id: ObjectId;
@@ -20,4 +21,5 @@ export interface Conversation extends Timestamps {
 	};
 
 	preprompt?: string;
+	assistantId?: Assistant["_id"];
 }
diff --git a/src/lib/types/Report.ts b/src/lib/types/Report.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f4f450f516714c8a81d65468fdfcb0caa47e2df5
--- /dev/null
+++ b/src/lib/types/Report.ts
@@ -0,0 +1,10 @@
+import type { ObjectId } from "mongodb";
+import type { User } from "./User";
+import type { Assistant } from "./Assistant";
+import type { Timestamps } from "./Timestamps";
+
+export interface Report extends Timestamps {
+	_id: ObjectId;
+	createdBy: User["_id"] | string;
+	assistantId: Assistant["_id"];
+}
diff --git a/src/lib/types/Settings.ts b/src/lib/types/Settings.ts
index 1dc5e764eb32b5ffefba551d8be106bbb06ea182..5a6804e05a891cf527b3592c498350aa890101f8 100644
--- a/src/lib/types/Settings.ts
+++ b/src/lib/types/Settings.ts
@@ -1,4 +1,5 @@
 import { defaultModel } from "$lib/server/models";
+import type { Assistant } from "./Assistant";
 import type { Timestamps } from "./Timestamps";
 import type { User } from "./User";
 
@@ -18,6 +19,8 @@ export interface Settings extends Timestamps {
 
 	// model name and system prompts
 	customPrompts?: Record<string, string>;
+
+	assistants?: Assistant["_id"][];
 }
 
 // TODO: move this to a constant file along with other constants
@@ -25,4 +28,6 @@ export const DEFAULT_SETTINGS = {
 	shareConversationsWithModelAuthors: true,
 	activeModel: defaultModel.id,
 	hideEmojiOnSidebar: false,
+	customPrompts: {},
+	assistants: [],
 };
diff --git a/src/lib/types/SharedConversation.ts b/src/lib/types/SharedConversation.ts
index 1996bcc6ff98cb65e02e6eda4e5157e494ba1e09..49f680da999e70045317350bb4840eaee0ffb055 100644
--- a/src/lib/types/SharedConversation.ts
+++ b/src/lib/types/SharedConversation.ts
@@ -1,3 +1,4 @@
+import type { Assistant } from "./Assistant";
 import type { Message } from "./Message";
 import type { Timestamps } from "./Timestamps";
 
@@ -12,4 +13,5 @@ export interface SharedConversation extends Timestamps {
 	title: string;
 	messages: Message[];
 	preprompt?: string;
+	assistantId?: Assistant["_id"];
 }
diff --git a/src/lib/utils/timeout.ts b/src/lib/utils/timeout.ts
new file mode 100644
index 0000000000000000000000000000000000000000..65d229f155eaa726f9427e7531b72b28d9f687bd
--- /dev/null
+++ b/src/lib/utils/timeout.ts
@@ -0,0 +1,6 @@
+export const timeout = <T>(prom: Promise<T>, time: number): Promise<T> => {
+	let timer: NodeJS.Timeout;
+	return Promise.race([prom, new Promise<T>((_r, rej) => (timer = setTimeout(rej, time)))]).finally(
+		() => clearTimeout(timer)
+	);
+};
diff --git a/src/routes/+layout.server.ts b/src/routes/+layout.server.ts
index 9ce2c126a4477303749cbc3442abd20b82a1ce6d..fcf4069d50acb55b62f4000d10ec203fc5c5fa6a 100644
--- a/src/routes/+layout.server.ts
+++ b/src/routes/+layout.server.ts
@@ -12,16 +12,22 @@ import {
 	MESSAGES_BEFORE_LOGIN,
 	YDC_API_KEY,
 	USE_LOCAL_WEBSEARCH,
+	ENABLE_ASSISTANTS,
 } from "$env/static/private";
+import { ObjectId } from "mongodb";
+import type { ConvSidebar } from "$lib/types/ConvSidebar";
 
 export const load: LayoutServerLoad = async ({ locals, depends }) => {
-	const { conversations } = collections;
 	depends(UrlDependency.ConversationList);
 
 	const settings = await collections.settings.findOne(authCondition(locals));
 
 	// If the active model in settings is not valid, set it to the default model. This can happen if model was disabled.
-	if (settings && !validateModel(models).safeParse(settings?.activeModel).success) {
+	if (
+		settings &&
+		!validateModel(models).safeParse(settings?.activeModel).success &&
+		!settings.assistants?.map((el) => el.toString())?.includes(settings?.activeModel)
+	) {
 		settings.activeModel = defaultModel.id;
 		await collections.settings.updateOne(authCondition(locals), {
 			$set: { activeModel: defaultModel.id },
@@ -42,7 +48,7 @@ export const load: LayoutServerLoad = async ({ locals, depends }) => {
 	// get the number of messages where `from === "assistant"` across all conversations.
 	const totalMessages =
 		(
-			await conversations
+			await collections.conversations
 				.aggregate([
 					{ $match: authCondition(locals) },
 					{ $project: { messages: 1 } },
@@ -59,33 +65,61 @@ export const load: LayoutServerLoad = async ({ locals, depends }) => {
 
 	const loginRequired = requiresUser && !locals.user && userHasExceededMessages;
 
+	const enableAssistants = ENABLE_ASSISTANTS === "true";
+
+	const assistantActive = !models.map(({ id }) => id).includes(settings?.activeModel ?? "");
+
+	const assistant = assistantActive
+		? JSON.parse(
+				JSON.stringify(
+					await collections.assistants.findOne({
+						_id: new ObjectId(settings?.activeModel),
+					})
+				)
+		  )
+		: null;
+
+	const conversations = await collections.conversations
+		.find(authCondition(locals))
+		.sort({ updatedAt: -1 })
+		.project<
+			Pick<Conversation, "title" | "model" | "_id" | "updatedAt" | "createdAt" | "assistantId">
+		>({
+			title: 1,
+			model: 1,
+			_id: 1,
+			updatedAt: 1,
+			createdAt: 1,
+			assistantId: 1,
+		})
+		.toArray();
+
+	const assistantIds = conversations
+		.map((conv) => conv.assistantId)
+		.filter((el) => !!el) as ObjectId[];
+
+	const assistants = await collections.assistants.find({ _id: { $in: assistantIds } }).toArray();
+
 	return {
-		conversations: await conversations
-			.find(authCondition(locals))
-			.sort({ updatedAt: -1 })
-			.project<Pick<Conversation, "title" | "model" | "_id" | "updatedAt" | "createdAt">>({
-				title: 1,
-				model: 1,
-				_id: 1,
-				updatedAt: 1,
-				createdAt: 1,
-			})
-			.map((conv) => {
-				// remove emojis if settings say so
-				if (settings?.hideEmojiOnSidebar) {
-					conv.title = conv.title.replace(/\p{Emoji}/gu, "");
-				}
-
-				// remove invalid unicode and trim whitespaces
-				conv.title = conv.title.replace(/\uFFFD/gu, "").trimStart();
-				return {
-					id: conv._id.toString(),
-					title: settings?.hideEmojiOnSidebar ? conv.title.replace(/\p{Emoji}/gu, "") : conv.title,
-					model: conv.model ?? defaultModel,
-					updatedAt: conv.updatedAt,
-				};
-			})
-			.toArray(),
+		conversations: conversations.map((conv) => {
+			if (settings?.hideEmojiOnSidebar) {
+				conv.title = conv.title.replace(/\p{Emoji}/gu, "");
+			}
+
+			// remove invalid unicode and trim whitespaces
+			conv.title = conv.title.replace(/\uFFFD/gu, "").trimStart();
+
+			return {
+				id: conv._id.toString(),
+				title: conv.title,
+				model: conv.model ?? defaultModel,
+				updatedAt: conv.updatedAt,
+				assistantId: conv.assistantId?.toString(),
+				avatarHash:
+					conv.assistantId &&
+					assistants.find((a) => a._id.toString() === conv.assistantId?.toString())?.avatar,
+			};
+		}) satisfies ConvSidebar[],
 		settings: {
 			searchEnabled: !!(
 				SERPAPI_KEY ||
@@ -102,6 +136,7 @@ export const load: LayoutServerLoad = async ({ locals, depends }) => {
 				settings?.shareConversationsWithModelAuthors ??
 				DEFAULT_SETTINGS.shareConversationsWithModelAuthors,
 			customPrompts: settings?.customPrompts ?? {},
+			assistants: settings?.assistants?.map((el) => el.toString()) ?? [],
 		},
 		models: models.map((model) => ({
 			id: model.id,
@@ -120,10 +155,13 @@ export const load: LayoutServerLoad = async ({ locals, depends }) => {
 		})),
 		oldModels,
 		user: locals.user && {
+			id: locals.user._id.toString(),
 			username: locals.user.username,
 			avatarUrl: locals.user.avatarUrl,
 			email: locals.user.email,
 		},
+		assistant,
+		enableAssistants,
 		loginRequired,
 		loginEnabled: requiresUser,
 		guestMode: requiresUser && messagesBeforeLogin > 0,
diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte
index 318c21cc3ff471bdb372104aed0e91903bf819d0..18d250df978b29351d39a4e14d60cfa8d25ad5e8 100644
--- a/src/routes/+layout.svelte
+++ b/src/routes/+layout.svelte
@@ -4,7 +4,7 @@
 	import { page } from "$app/stores";
 	import "../styles/main.css";
 	import { base } from "$app/paths";
-	import { PUBLIC_ORIGIN } from "$env/static/public";
+	import { PUBLIC_APP_DESCRIPTION, PUBLIC_ORIGIN } from "$env/static/public";
 
 	import { shareConversation } from "$lib/shareConversation";
 	import { UrlDependency } from "$lib/types/UrlDependency";
@@ -17,6 +17,7 @@
 	import titleUpdate from "$lib/stores/titleUpdate";
 	import { createSettingsStore } from "$lib/stores/settings";
 	import { browser } from "$app/environment";
+	import DisclaimerModal from "$lib/components/DisclaimerModal.svelte";
 
 	export let data;
 
@@ -120,13 +121,19 @@
 	<meta name="description" content="The first open source alternative to ChatGPT. 💪" />
 	<meta name="twitter:card" content="summary_large_image" />
 	<meta name="twitter:site" content="@huggingface" />
-	<meta property="og:title" content={PUBLIC_APP_NAME} />
-	<meta property="og:type" content="website" />
-	<meta property="og:url" content="{PUBLIC_ORIGIN || $page.url.origin}{base}" />
-	<meta
-		property="og:image"
-		content="{PUBLIC_ORIGIN || $page.url.origin}{base}/{PUBLIC_APP_ASSETS}/thumbnail.png"
-	/>
+
+	<!-- use those meta tags everywhere except on the share assistant page -->
+	<!-- feel free to refacto if there's a better way -->
+	{#if !$page.url.pathname.includes("/assistant/")}
+		<meta property="og:title" content={PUBLIC_APP_NAME} />
+		<meta property="og:type" content="website" />
+		<meta property="og:url" content="{PUBLIC_ORIGIN || $page.url.origin}{base}" />
+		<meta
+			property="og:image"
+			content="{PUBLIC_ORIGIN || $page.url.origin}{base}/{PUBLIC_APP_ASSETS}/thumbnail.png"
+		/>
+		<meta property="og:description" content={PUBLIC_APP_DESCRIPTION} />
+	{/if}
 	<link
 		rel="icon"
 		href="{PUBLIC_ORIGIN || $page.url.origin}{base}/{PUBLIC_APP_ASSETS}/favicon.ico"
@@ -147,6 +154,10 @@
 	/>
 </svelte:head>
 
+{#if !$settings.ethicsModalAccepted}
+	<DisclaimerModal />
+{/if}
+
 <div
 	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"
 >
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte
index fa458cfe80b0b2306384f50b856794a888c43ccd..78d93c4a3f362eab499ae983494f17b5a673cb29 100644
--- a/src/routes/+page.svelte
+++ b/src/routes/+page.svelte
@@ -17,14 +17,32 @@
 	async function createConversation(message: string) {
 		try {
 			loading = true;
+
+			// check if $settings.activeModel is a valid model
+			// else check if it's an assistant, and use that model
+			// else use the first model
+
+			const validModels = data.models.map((model) => model.id);
+
+			let model;
+			if (validModels.includes($settings.activeModel)) {
+				model = $settings.activeModel;
+			} else {
+				if (validModels.includes(data.assistant?.modelId)) {
+					model = data.assistant?.modelId;
+				} else {
+					model = data.models[0].id;
+				}
+			}
 			const res = await fetch(`${base}/conversation`, {
 				method: "POST",
 				headers: {
 					"Content-Type": "application/json",
 				},
 				body: JSON.stringify({
-					model: $settings.activeModel,
+					model,
 					preprompt: $settings.customPrompts[$settings.activeModel],
+					assistantId: data.assistant?._id,
 				}),
 			});
 
@@ -60,6 +78,7 @@
 <ChatWindow
 	on:message={(ev) => createConversation(ev.detail)}
 	{loading}
+	assistant={data.assistant}
 	currentModel={findCurrentModel([...data.models, ...data.oldModels], $settings.activeModel)}
 	models={data.models}
 	bind:files
diff --git a/src/routes/assistant/[assistantId]/+page.server.ts b/src/routes/assistant/[assistantId]/+page.server.ts
new file mode 100644
index 0000000000000000000000000000000000000000..ac14877dbc4367ef9498e6bdd4a6207b79039aab
--- /dev/null
+++ b/src/routes/assistant/[assistantId]/+page.server.ts
@@ -0,0 +1,20 @@
+import { base } from "$app/paths";
+import { collections } from "$lib/server/database.js";
+import { redirect } from "@sveltejs/kit";
+import { ObjectId } from "mongodb";
+
+export const load = async ({ params }) => {
+	try {
+		const assistant = await collections.assistants.findOne({
+			_id: new ObjectId(params.assistantId),
+		});
+
+		if (!assistant) {
+			throw redirect(302, `${base}`);
+		}
+
+		return { assistant: JSON.parse(JSON.stringify(assistant)) };
+	} catch {
+		throw redirect(302, `${base}`);
+	}
+};
diff --git a/src/routes/assistant/[assistantId]/+page.svelte b/src/routes/assistant/[assistantId]/+page.svelte
new file mode 100644
index 0000000000000000000000000000000000000000..52b7bbc0bbdd31a94d71cb6fff4fbc468b6b6970
--- /dev/null
+++ b/src/routes/assistant/[assistantId]/+page.svelte
@@ -0,0 +1,112 @@
+<script lang="ts">
+	import { base } from "$app/paths";
+	import { clickOutside } from "$lib/actions/clickOutside";
+	import { afterNavigate, goto } from "$app/navigation";
+
+	import { useSettingsStore } from "$lib/stores/settings";
+	import type { PageData } from "./$types";
+	import { applyAction, enhance } from "$app/forms";
+	import { PUBLIC_APP_NAME, PUBLIC_ORIGIN } from "$env/static/public";
+	import { page } from "$app/stores";
+
+	export let data: PageData;
+
+	let previousPage: string = base;
+
+	afterNavigate(({ from }) => {
+		if (!from?.url.pathname.includes("settings")) {
+			previousPage = from?.url.pathname || previousPage;
+		}
+	});
+
+	const settings = useSettingsStore();
+</script>
+
+<svelte:head>
+	<meta property="og:title" content={data.assistant.name + " - " + PUBLIC_APP_NAME} />
+	<meta property="og:type" content="link" />
+	<meta
+		property="og:description"
+		content={`Use the ${data.assistant.name} assistant inside of ${PUBLIC_APP_NAME}`}
+	/>
+	<meta
+		property="og:image"
+		content="{PUBLIC_ORIGIN || $page.url.origin}{base}/assistant/{data.assistant._id}/thumbnail.png"
+	/>
+	<meta property="og:url" content={$page.url.href} />
+	<meta name="twitter:card" content="summary_large_image" />
+</svelte:head>
+
+<div
+	class="fixed inset-0 flex items-center justify-center bg-black/80 backdrop-blur-sm dark:bg-black/50"
+>
+	<dialog
+		open
+		use:clickOutside={() => {
+			goto(previousPage);
+		}}
+		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"
+	>
+		{#if data.assistant.avatar}
+			<img
+				class="h-24 w-24 rounded-full object-cover"
+				src="{base}/settings/assistants/{data.assistant._id}/avatar?hash={data.assistant.avatar}"
+				alt="avatar"
+			/>
+		{:else}
+			<div
+				class="flex h-24 w-24 items-center justify-center rounded-full bg-gray-300 font-bold uppercase text-gray-500"
+			>
+				{data.assistant.name[0]}
+			</div>
+		{/if}
+		<h1 class="text-2xl font-bold">
+			{data.assistant.name}
+		</h1>
+		<h3 class="text-sm text-gray-700">
+			{data.assistant.description}
+		</h3>
+		{#if data.assistant.createdByName}
+			<p class="text-sm text-gray-500">
+				Created by <a
+					class="hover:underline"
+					href="https://hf.co/{data.assistant.createdByName}"
+					target="_blank"
+				>
+					{data.assistant.createdByName}
+				</a>
+			</p>
+		{/if}
+		<button
+			class="mt-4 w-full rounded-full bg-gray-200 px-4 py-2 font-semibold text-gray-700"
+			on:click={() => {
+				goto(previousPage);
+			}}
+		>
+			Cancel
+		</button>
+		<form
+			method="POST"
+			action="{base}/settings/assistants/{data.assistant._id}?/subscribe"
+			class="w-full"
+			use:enhance={() => {
+				return async ({ result }) => {
+					// `result` is an `ActionResult` object
+					if (result.type === "success") {
+						$settings.activeModel = data.assistant._id;
+						goto(`${base}`);
+					} else {
+						await applyAction(result);
+					}
+				};
+			}}
+		>
+			<button
+				type="submit"
+				class=" w-full rounded-full bg-black px-4 py-3 font-semibold text-white"
+			>
+				Start chatting
+			</button>
+		</form>
+	</dialog>
+</div>
diff --git a/src/routes/assistant/[assistantId]/thumbnail.png/+server.ts b/src/routes/assistant/[assistantId]/thumbnail.png/+server.ts
new file mode 100644
index 0000000000000000000000000000000000000000..c43d46c1fda9001d4f66643dac06f964cfe0c5fb
--- /dev/null
+++ b/src/routes/assistant/[assistantId]/thumbnail.png/+server.ts
@@ -0,0 +1,64 @@
+import { APP_BASE } from "$env/static/private";
+import ChatThumbnail from "./ChatThumbnail.svelte";
+import { collections } from "$lib/server/database";
+import { error, type RequestHandler } from "@sveltejs/kit";
+import { ObjectId } from "mongodb";
+import type { SvelteComponent } from "svelte";
+
+import { Resvg } from "@resvg/resvg-js";
+import satori from "satori";
+import { html } from "satori-html";
+import { base } from "$app/paths";
+
+export const GET: RequestHandler = (async ({ url, params, fetch }) => {
+	const assistant = await collections.assistants.findOne({
+		_id: new ObjectId(params.assistantId),
+	});
+
+	if (!assistant) {
+		throw error(404, "Assistant not found.");
+	}
+
+	const renderedComponent = (ChatThumbnail as unknown as SvelteComponent).render({
+		href: url.origin,
+		name: assistant.name,
+		description: assistant.description,
+		createdByName: assistant.createdByName,
+		avatarUrl: assistant.avatar
+			? url.origin + APP_BASE + "/settings/assistants/" + assistant._id + "/avatar"
+			: undefined,
+	});
+
+	const reactLike = html(
+		"<style>" + renderedComponent.css.code + "</style>" + renderedComponent.html
+	);
+
+	const svg = await satori(reactLike, {
+		width: 1200,
+		height: 648,
+		fonts: [
+			{
+				name: "Inter",
+				data: await fetch(base + "/fonts/Inter-Regular.ttf").then((r) => r.arrayBuffer()),
+				weight: 500,
+			},
+			{
+				name: "Inter",
+				data: await fetch(base + "/fonts/Inter-Bold.ttf").then((r) => r.arrayBuffer()),
+				weight: 700,
+			},
+		],
+	});
+
+	const png = new Resvg(svg, {
+		fitTo: { mode: "original" },
+	})
+		.render()
+		.asPng();
+
+	return new Response(png, {
+		headers: {
+			"Content-Type": "image/png",
+		},
+	});
+}) satisfies RequestHandler;
diff --git a/src/routes/assistant/[assistantId]/thumbnail.png/ChatThumbnail.svelte b/src/routes/assistant/[assistantId]/thumbnail.png/ChatThumbnail.svelte
new file mode 100644
index 0000000000000000000000000000000000000000..9c791fa1f2cefa77db768c832b07f2c4769de23d
--- /dev/null
+++ b/src/routes/assistant/[assistantId]/thumbnail.png/ChatThumbnail.svelte
@@ -0,0 +1,41 @@
+<script lang="ts">
+	import { base } from "$app/paths";
+	import { PUBLIC_APP_ASSETS } from "$env/static/public";
+
+	export let href: string = "";
+	export let name: string;
+	export let description: string = "";
+	export let createdByName: string | undefined;
+	export let avatarUrl: string | undefined;
+
+	const imgUrl = `${href}${base}/${PUBLIC_APP_ASSETS}/logo.svg`;
+</script>
+
+<div class="flex h-full w-full flex-col items-center justify-center bg-black p-2">
+	<div class="flex w-full max-w-[540px] items-start justify-center text-white">
+		{#if avatarUrl}
+			<img class="h-64 w-64 rounded-full" src={avatarUrl} alt="avatar" />
+		{/if}
+		<div class="ml-10 flex flex-col items-start">
+			<p class="mb-2 mt-0 text-3xl font-normal text-gray-400">
+				<img class="mr-1.5 h-8 w-8" src={imgUrl} alt="app logo" />
+				AI assistant
+			</p>
+			<h1 class="m-0 {name.length < 38 ? 'text-5xl' : 'text-4xl'} text-balance font-black">
+				{name}
+			</h1>
+			<p class="mb-8 text-pretty text-2xl">
+				{description.slice(0, 160)}
+				{#if description.length > 160}...{/if}
+			</p>
+			<div class="rounded-full bg-[#FFA800] px-8 py-3 text-3xl font-semibold text-black">
+				Start chatting
+			</div>
+		</div>
+	</div>
+	{#if createdByName}
+		<p class="absolute bottom-4 right-8 text-2xl text-gray-400">
+			An AI assistant created by {createdByName}
+		</p>
+	{/if}
+</div>
diff --git a/src/routes/conversation/+server.ts b/src/routes/conversation/+server.ts
index 94901c6e89534b445aaa49640e47a213d0c72dd1..2b6f6f0c9bccf47c8249914c18e144e5b1b56016 100644
--- a/src/routes/conversation/+server.ts
+++ b/src/routes/conversation/+server.ts
@@ -18,11 +18,11 @@ export const POST: RequestHandler = async ({ locals, request }) => {
 		.object({
 			fromShare: z.string().optional(),
 			model: validateModel(models),
+			assistantId: z.string().optional(),
 			preprompt: z.string().optional(),
 		})
 		.parse(JSON.parse(body));
 
-	let preprompt = values.preprompt;
 	let embeddingModel: string;
 
 	if (values.fromShare) {
@@ -37,8 +37,9 @@ export const POST: RequestHandler = async ({ locals, request }) => {
 		title = conversation.title;
 		messages = conversation.messages;
 		values.model = conversation.model;
+		values.preprompt = conversation.preprompt;
+		values.assistantId = conversation.assistantId?.toString();
 		embeddingModel = conversation.embeddingModel;
-		preprompt = conversation.preprompt;
 	}
 
 	const model = models.find((m) => m.name === values.model);
@@ -54,7 +55,16 @@ export const POST: RequestHandler = async ({ locals, request }) => {
 	}
 
 	// Use the model preprompt if there is no conversation/preprompt in the request body
-	preprompt = preprompt === undefined ? model?.preprompt : preprompt;
+	const preprompt = await (async () => {
+		if (values.assistantId) {
+			const assistant = await collections.assistants.findOne({
+				_id: new ObjectId(values.assistantId),
+			});
+			return assistant?.preprompt;
+		} else {
+			return values?.preprompt ?? model?.preprompt;
+		}
+	})();
 
 	const res = await collections.conversations.insertOne({
 		_id: new ObjectId(),
@@ -62,6 +72,7 @@ export const POST: RequestHandler = async ({ locals, request }) => {
 		messages,
 		model: values.model,
 		preprompt: preprompt === model?.preprompt ? model?.preprompt : preprompt,
+		assistantId: values.assistantId ? new ObjectId(values.assistantId) : undefined,
 		createdAt: new Date(),
 		updatedAt: new Date(),
 		embeddingModel,
diff --git a/src/routes/conversation/[id]/+page.server.ts b/src/routes/conversation/[id]/+page.server.ts
index ee25b61c05a9815b4ab7ce0f1db14f6594ab3c1a..cbc80ffea127fee00c4d4257d359122b69217213 100644
--- a/src/routes/conversation/[id]/+page.server.ts
+++ b/src/routes/conversation/[id]/+page.server.ts
@@ -44,11 +44,21 @@ export const load = async ({ params, depends, locals }) => {
 			throw error(404, "Conversation not found.");
 		}
 	}
+
 	return {
 		messages: conversation.messages,
 		title: conversation.title,
 		model: conversation.model,
 		preprompt: conversation.preprompt,
+		assistant: conversation.assistantId
+			? JSON.parse(
+					JSON.stringify(
+						await collections.assistants.findOne({
+							_id: new ObjectId(conversation.assistantId),
+						})
+					)
+			  )
+			: null,
 		shared,
 	};
 };
diff --git a/src/routes/conversation/[id]/share/+server.ts b/src/routes/conversation/[id]/share/+server.ts
index 4877de755ad4b18cd0fc2562d14d2027ab0273c3..c2a038eb7d1b3a2a37e59d154739f747672aee01 100644
--- a/src/routes/conversation/[id]/share/+server.ts
+++ b/src/routes/conversation/[id]/share/+server.ts
@@ -40,6 +40,7 @@ export async function POST({ params, url, locals }) {
 		model: conversation.model,
 		embeddingModel: conversation.embeddingModel,
 		preprompt: conversation.preprompt,
+		assistantId: conversation.assistantId,
 	};
 
 	await collections.sharedConversations.insertOne(shared);
diff --git a/src/routes/settings/+layout.server.ts b/src/routes/settings/+layout.server.ts
new file mode 100644
index 0000000000000000000000000000000000000000..ab46b8798db1a716b76ddec0616f1f61ed9e0fd9
--- /dev/null
+++ b/src/routes/settings/+layout.server.ts
@@ -0,0 +1,31 @@
+import { collections } from "$lib/server/database";
+import { ObjectId } from "mongodb";
+import type { LayoutServerLoad } from "./$types";
+
+export const load = (async ({ locals, parent }) => {
+	const { settings } = await parent();
+
+	// find assistants matching the settings assistants
+	const assistants = await collections.assistants
+		.find({
+			_id: { $in: settings.assistants.map((el) => new ObjectId(el)) },
+		})
+		.toArray();
+
+	return {
+		assistants: await Promise.all(
+			assistants.map(async (el) => ({
+				...el,
+				_id: el._id.toString(),
+				createdById: undefined,
+				createdByMe:
+					el.createdById.toString() === (locals.user?._id ?? locals.sessionId).toString(),
+				reported:
+					(await collections.reports.countDocuments({
+						assistantId: el._id,
+						createdBy: locals.user?._id ?? locals.sessionId,
+					})) > 0,
+			}))
+		),
+	};
+}) satisfies LayoutServerLoad;
diff --git a/src/routes/settings/+layout.svelte b/src/routes/settings/+layout.svelte
index 7c02043683cd9f0ebfd4feb7e97b384e2a945f0a..dba111364b28851c37f6022bdc6a710cecbf3b04 100644
--- a/src/routes/settings/+layout.svelte
+++ b/src/routes/settings/+layout.svelte
@@ -1,14 +1,16 @@
 <script lang="ts">
 	import { base } from "$app/paths";
 	import { clickOutside } from "$lib/actions/clickOutside";
-	import { browser } from "$app/environment";
 	import { afterNavigate, goto } from "$app/navigation";
 	import { page } from "$app/stores";
 	import { useSettingsStore } from "$lib/stores/settings";
 	import CarbonClose from "~icons/carbon/close";
 	import CarbonCheckmark from "~icons/carbon/checkmark";
+	import CarbonAdd from "~icons/carbon/add";
 
 	import UserIcon from "~icons/carbon/user";
+	import { fade, fly } from "svelte/transition";
+	import { PUBLIC_APP_ASSETS } from "$env/static/public";
 	export let data;
 
 	let previousPage: string = base;
@@ -20,25 +22,27 @@
 	});
 
 	const settings = useSettingsStore();
+
+	const isHuggingChat = PUBLIC_APP_ASSETS === "huggingchat";
 </script>
 
 <div
 	class="fixed inset-0 flex items-center justify-center bg-black/80 backdrop-blur-sm dark:bg-black/50"
+	in:fade
 >
 	<dialog
+		in:fly={{ y: 100 }}
 		open
 		use:clickOutside={() => {
-			if (browser) window;
 			goto(previousPage);
 		}}
-		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]"
+		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]"
 	>
-		<div class="col-span-1 flex items-center justify-between md:col-span-3">
+		<div class="col-span-1 mb-4 flex items-center justify-between md:col-span-3">
 			<h2 class="text-xl font-bold">Settings</h2>
 			<button
 				class="btn rounded-lg"
 				on:click={() => {
-					if (browser) window;
 					goto(previousPage);
 				}}
 			>
@@ -46,38 +50,81 @@
 			</button>
 		</div>
 		<div
-			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"
+			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"
 		>
+			<h3 class="pb-3 pl-3 pt-2 text-[.8rem] text-gray-800 sm:pl-1">Models</h3>
+
 			{#each data.models.filter((el) => !el.unlisted) as model}
 				<a
 					href="{base}/settings/{model.id}"
-					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 ===
-					$page.params.model
-						? '!bg-gray-100 !text-gray-800'
-						: ''}"
+					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
+					{model.id === $page.params.model ? '!bg-gray-100 !text-gray-800' : ''}"
 				>
 					<div class="truncate">{model.displayName}</div>
 					{#if model.id === $settings.activeModel}
 						<div
-							class="rounded-lg bg-black px-2 py-1.5 text-xs font-semibold leading-none text-white"
+							class="ml-auto rounded-lg bg-black px-2 py-1.5 text-xs font-semibold leading-none text-white"
 						>
 							Active
 						</div>
 					{/if}
 				</a>
 			{/each}
+			<!-- if its huggingchat, the number of assistants owned by the user must be non-zero to show the UI -->
+			{#if data.enableAssistants && (!isHuggingChat || data.assistants.length >= 1)}
+				<h3 class="pb-3 pl-3 pt-5 text-[.8rem] text-gray-800 sm:pl-1">Assistants</h3>
+				{#each data.assistants as assistant}
+					<a
+						href="{base}/settings/assistants/{assistant._id.toString()}"
+						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
+						{assistant._id.toString() === $page.params.assistantId ? '!bg-gray-100 !text-gray-800' : ''}"
+					>
+						{#if assistant.avatar}
+							<img
+								src="{base}/settings/assistants/{assistant._id.toString()}/avatar?hash={assistant.avatar}"
+								alt="Avatar"
+								class="h-6 w-6 rounded-full object-cover"
+							/>
+						{:else}
+							<div
+								class="flex size-6 items-center justify-center rounded-full bg-gray-300 font-bold uppercase text-gray-500"
+							>
+								{assistant.name[0]}
+							</div>
+						{/if}
+						<div class="truncate">{assistant.name}</div>
+						{#if assistant._id.toString() === $settings.activeModel}
+							<div
+								class="ml-auto rounded-lg bg-black px-2 py-1.5 text-xs font-semibold leading-none text-white"
+							>
+								Active
+							</div>
+						{/if}
+					</a>
+				{/each}
+
+				{#if !data.loginEnabled || (data.loginEnabled && !!data.user)}
+					<a
+						href="{base}/settings/assistants/new"
+						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
+					{$page.url.pathname === `${base}/settings/assistants/new` ? '!bg-gray-100 !text-gray-800' : ''}"
+					>
+						<CarbonAdd />
+						<div class="truncate">Create new assistant</div>
+					</a>
+				{/if}
+			{/if}
+
 			<a
 				href="{base}/settings"
-				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
-					.params.model === undefined
-					? '!bg-gray-100 !text-gray-800'
-					: ''}"
+				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
+				{$page.url.pathname === `${base}/settings` ? '!bg-gray-100 !text-gray-800' : ''}"
 			>
-				<UserIcon class="pr-1 text-lg" />
+				<UserIcon class="text-lg" />
 				Application Settings
 			</a>
 		</div>
-		<div class="col-span-1 overflow-y-auto md:col-span-2">
+		<div class="col-span-1 overflow-y-auto px-4 max-md:-mx-4 max-md:pt-6 md:col-span-2">
 			<slot />
 		</div>
 
@@ -85,7 +132,7 @@
 			<div
 				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"
 			>
-				<CarbonCheckmark />
+				<CarbonCheckmark class="text-green-500" />
 				Saved
 			</div>
 		{/if}
diff --git a/src/routes/settings/+server.ts b/src/routes/settings/+server.ts
index 5455edeb2ac2cbe58cb0a5f7760f5309b0073234..81289bacba2bf77bbbdd160f29b88fec2824c0cc 100644
--- a/src/routes/settings/+server.ts
+++ b/src/routes/settings/+server.ts
@@ -1,6 +1,5 @@
 import { collections } from "$lib/server/database";
 import { z } from "zod";
-import { models, validateModel } from "$lib/server/models";
 import { authCondition } from "$lib/server/auth";
 import { DEFAULT_SETTINGS } from "$lib/types/Settings";
 
@@ -14,7 +13,7 @@ export async function POST({ request, locals }) {
 				.default(DEFAULT_SETTINGS.shareConversationsWithModelAuthors),
 			hideEmojiOnSidebar: z.boolean().default(DEFAULT_SETTINGS.hideEmojiOnSidebar),
 			ethicsModalAccepted: z.boolean().optional(),
-			activeModel: validateModel(models).default(DEFAULT_SETTINGS.activeModel),
+			activeModel: z.string().default(DEFAULT_SETTINGS.activeModel),
 			customPrompts: z.record(z.string()).default({}),
 		})
 		.parse(body);
diff --git a/src/routes/settings/[...model]/+page.svelte b/src/routes/settings/[...model]/+page.svelte
index 0c7b915a229b880fa4a93cb9d449c8d1cc185762..1bb368ccfa980caf90b89bfb381680bba7a9341c 100644
--- a/src/routes/settings/[...model]/+page.svelte
+++ b/src/routes/settings/[...model]/+page.svelte
@@ -78,7 +78,7 @@
 			value="{PUBLIC_ORIGIN || $page.url.origin}{base}?model={model.id}"
 			classNames="!border-none !shadow-none !py-0 !px-1 !rounded-md"
 		>
-			<div class="flex items-center gap-1.5">
+			<div class="flex items-center gap-1.5 hover:underline">
 				<CarbonLink />Copy direct link to model
 			</div>
 		</CopyToClipBoardBtn>
diff --git a/src/routes/settings/assistants/[assistantId]/+page.server.ts b/src/routes/settings/assistants/[assistantId]/+page.server.ts
new file mode 100644
index 0000000000000000000000000000000000000000..a145fe35e53b3cf0aeb119b6261da4ea0f2eaed4
--- /dev/null
+++ b/src/routes/settings/assistants/[assistantId]/+page.server.ts
@@ -0,0 +1,115 @@
+import { collections } from "$lib/server/database";
+import { type Actions, fail, redirect } from "@sveltejs/kit";
+import { ObjectId } from "mongodb";
+import { authCondition } from "$lib/server/auth";
+import { base } from "$app/paths";
+
+async function assistantOnlyIfAuthor(locals: App.Locals, assistantId?: string) {
+	const assistant = await collections.assistants.findOne({ _id: new ObjectId(assistantId) });
+
+	if (!assistant) {
+		throw Error("Assistant not found");
+	}
+
+	if (assistant.createdById.toString() !== (locals.user?._id ?? locals.sessionId).toString()) {
+		throw Error("You are not the author of this assistant");
+	}
+
+	return assistant;
+}
+
+export const actions: Actions = {
+	delete: async ({ params, locals }) => {
+		let assistant;
+		try {
+			assistant = await assistantOnlyIfAuthor(locals, params.assistantId);
+		} catch (e) {
+			return fail(400, { error: true, message: (e as Error).message });
+		}
+
+		await collections.assistants.deleteOne({ _id: assistant._id });
+
+		// and remove it from all users settings
+		await collections.settings.updateMany(
+			{},
+			{
+				$pull: { assistants: assistant._id },
+			}
+		);
+
+		// and delete all avatars
+		const fileCursor = collections.bucket.find({ filename: assistant._id.toString() });
+
+		// Step 2: Delete the existing file if it exists
+		let fileId = await fileCursor.next();
+		while (fileId) {
+			await collections.bucket.delete(fileId._id);
+			fileId = await fileCursor.next();
+		}
+
+		throw redirect(302, `${base}/settings`);
+	},
+	report: async ({ params, locals }) => {
+		// is there already a report from this user for this model ?
+		const report = await collections.reports.findOne({
+			assistantId: new ObjectId(params.assistantId),
+			createdBy: locals.user?._id ?? locals.sessionId,
+		});
+
+		if (report) {
+			return fail(400, { error: true, message: "Already reported" });
+		}
+
+		const { acknowledged } = await collections.reports.insertOne({
+			_id: new ObjectId(),
+			assistantId: new ObjectId(params.assistantId),
+			createdBy: locals.user?._id ?? locals.sessionId,
+			createdAt: new Date(),
+			updatedAt: new Date(),
+		});
+
+		if (!acknowledged) {
+			return fail(500, { error: true, message: "Failed to report assistant" });
+		}
+		return { from: "report", ok: true, message: "Assistant reported" };
+	},
+
+	subscribe: async ({ params, locals }) => {
+		const assistant = await collections.assistants.findOne({
+			_id: new ObjectId(params.assistantId),
+		});
+
+		if (!assistant) {
+			return fail(404, { error: true, message: "Assistant not found" });
+		}
+
+		// don't push if it's already there
+		const settings = await collections.settings.findOne(authCondition(locals));
+
+		if (settings?.assistants?.includes(assistant._id)) {
+			return fail(400, { error: true, message: "Already subscribed" });
+		}
+
+		await collections.settings.updateOne(authCondition(locals), {
+			$push: { assistants: assistant._id },
+		});
+
+		return { from: "subscribe", ok: true, message: "Assistant added" };
+	},
+
+	unsubscribe: async ({ params, locals }) => {
+		const assistant = await collections.assistants.findOne({
+			_id: new ObjectId(params.assistantId),
+		});
+
+		if (!assistant) {
+			return fail(404, { error: true, message: "Assistant not found" });
+		}
+
+		await collections.settings.updateOne(authCondition(locals), {
+			$pull: { assistants: assistant._id },
+		});
+
+		throw redirect(302, `${base}/settings`);
+	},
+};
diff --git a/src/routes/settings/assistants/[assistantId]/+page.svelte b/src/routes/settings/assistants/[assistantId]/+page.svelte
new file mode 100644
index 0000000000000000000000000000000000000000..d1b043a895d9940bdeedd0a91031c5fcd996f2d6
--- /dev/null
+++ b/src/routes/settings/assistants/[assistantId]/+page.svelte
@@ -0,0 +1,156 @@
+<script lang="ts">
+	import { enhance } from "$app/forms";
+	import { base } from "$app/paths";
+	import { page } from "$app/stores";
+	import { PUBLIC_ORIGIN, PUBLIC_SHARE_PREFIX } from "$env/static/public";
+	import { useSettingsStore } from "$lib/stores/settings";
+	import type { PageData } from "./$types";
+
+	import CarbonPen from "~icons/carbon/pen";
+	import CarbonTrash from "~icons/carbon/trash-can";
+	import CarbonCopy from "~icons/carbon/copy-file";
+	import CarbonFlag from "~icons/carbon/flag";
+	import CarbonLink from "~icons/carbon/link";
+	import CopyToClipBoardBtn from "$lib/components/CopyToClipBoardBtn.svelte";
+
+	export let data: PageData;
+
+	$: assistant = data.assistants.find((el) => el._id.toString() === $page.params.assistantId);
+
+	const settings = useSettingsStore();
+
+	$: isActive = $settings.activeModel === $page.params.assistantId;
+
+	const prefix = PUBLIC_SHARE_PREFIX || `${PUBLIC_ORIGIN || $page.url.origin}${base}`;
+
+	$: shareUrl = `${prefix}/assistant/${assistant?._id}`;
+</script>
+
+<div class="flex h-full flex-col gap-2">
+	<div class="flex gap-6">
+		{#if assistant?.avatar}
+			<!-- crop image if not square  -->
+			<img
+				src={`${base}/settings/assistants/${assistant?._id}/avatar?hash=${assistant?.avatar}`}
+				alt="Avatar"
+				class="h-24 w-24 rounded-full object-cover"
+			/>
+		{:else}
+			<div
+				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"
+			>
+				{assistant?.name[0]}
+			</div>
+		{/if}
+
+		<div>
+			<h1 class="text-xl font-semibold">
+				{assistant?.name}
+			</h1>
+
+			{#if assistant?.description}
+				<p class="pb-2 text-sm text-gray-500">
+					{assistant.description}
+				</p>
+			{/if}
+
+			<p class="text-sm text-gray-500">
+				Model: <span class="font-semibold"> {assistant?.modelId} </span>
+			</p>
+			<button
+				class="{isActive
+					? 'bg-gray-100'
+					: 'bg-black text-white'} my-2 flex w-fit items-center rounded-full px-3 py-1"
+				disabled={isActive}
+				name="Activate model"
+				on:click|stopPropagation={() => {
+					$settings.activeModel = $page.params.assistantId;
+				}}
+			>
+				{isActive ? "Active" : "Activate"}
+			</button>
+		</div>
+	</div>
+
+	<div>
+		<h2 class="text-lg font-semibold">Direct URL</h2>
+
+		<p class="pb-2 text-sm text-gray-500">
+			People with this link will be able to use your assistant.
+			{#if !assistant?.createdByMe && assistant?.createdByName}
+				Created by <a
+					class="underline"
+					target="_blank"
+					href={"https://hf.co/" + assistant?.createdByName}
+				>
+					{assistant?.createdByName}
+				</a>
+			{/if}
+		</p>
+
+		<div
+			class="flex flex-row gap-2 rounded-lg border-2 border-gray-200 bg-gray-100 py-2 pl-3 pr-1.5"
+		>
+			<input disabled class="flex-1 truncate bg-inherit" value={shareUrl} />
+			<CopyToClipBoardBtn
+				value={shareUrl}
+				classNames="!border-none !shadow-none !py-0 !px-1 !rounded-md"
+			>
+				<div class="flex items-center gap-1.5 text-gray-500 hover:underline">
+					<CarbonLink />Copy
+				</div>
+			</CopyToClipBoardBtn>
+		</div>
+	</div>
+
+	<!-- <div>
+		<h2 class="mb-2 text-lg font-semibold">Model used</h2>
+
+		<div
+			class="flex flex-row gap-2 rounded-lg border-2 border-gray-200 bg-gray-100 py-2 pl-3 pr-1.5"
+		>
+			<input disabled class="flex-1" value="Model" />
+		</div>
+	</div> -->
+
+	<h2 class="mt-4 text-lg font-semibold">System Instructions</h2>
+
+	<textarea disabled class="h-[8lh] w-full rounded-lg border-2 border-gray-200 bg-gray-100 p-2"
+		>{assistant?.preprompt}</textarea
+	>
+
+	<div class="mt-5 flex gap-4">
+		{#if assistant?.createdByMe}
+			<a href="{base}/settings/assistants/{assistant?._id}/edit" class="underline"
+				><CarbonPen class="mr-1.5 inline" />Edit assistant</a
+			>
+			<form method="POST" action="?/delete" use:enhance>
+				<button type="submit" class="flex items-center underline">
+					<CarbonTrash class="mr-1.5 inline" />Delete assistant</button
+				>
+			</form>
+		{:else}
+			<form method="POST" action="?/unsubscribe" use:enhance>
+				<button type="submit" class="underline">
+					<CarbonTrash class="mr-1.5 inline" />Remove assistant</button
+				>
+			</form>
+			<form method="POST" action="?/edit" use:enhance class="hidden">
+				<button type="submit" class="underline">
+					<CarbonCopy class="mr-1.5 inline" />Duplicate assistant</button
+				>
+			</form>
+			{#if !assistant?.reported}
+				<form method="POST" action="?/report" use:enhance>
+					<button type="submit" class="underline">
+						<CarbonFlag class="mr-1.5 inline" />Report assistant</button
+					>
+				</form>
+			{:else}
+				<button type="button" disabled class="text-gray-700">
+					<CarbonFlag class="mr-1.5 inline" />Reported</button
+				>
+			{/if}
+		{/if}
+	</div>
+</div>
diff --git a/src/routes/settings/assistants/[assistantId]/+page.ts b/src/routes/settings/assistants/[assistantId]/+page.ts
new file mode 100644
index 0000000000000000000000000000000000000000..7d2ef393ee413ae0a534029dd25cfcf99cbd54cb
--- /dev/null
+++ b/src/routes/settings/assistants/[assistantId]/+page.ts
@@ -0,0 +1,14 @@
+import { base } from "$app/paths";
+import { redirect } from "@sveltejs/kit";
+
+export async function load({ parent, params }) {
+	const data = await parent();
+
+	const assistant = data.settings.assistants.find((id) => id === params.assistantId);
+
+	if (!assistant) {
+		throw redirect(302, `${base}/assistant/${params.assistantId}`);
+	}
+
+	return data;
+}
diff --git a/src/routes/settings/assistants/[assistantId]/avatar/+server.ts b/src/routes/settings/assistants/[assistantId]/avatar/+server.ts
new file mode 100644
index 0000000000000000000000000000000000000000..0b6be2c2cf63022a967d8435baa494d655ee4154
--- /dev/null
+++ b/src/routes/settings/assistants/[assistantId]/avatar/+server.ts
@@ -0,0 +1,46 @@
+import { collections } from "$lib/server/database";
+import { error, type RequestHandler } from "@sveltejs/kit";
+import { ObjectId } from "mongodb";
+
+export const GET: RequestHandler = async ({ params }) => {
+	const assistant = await collections.assistants.findOne({
+		_id: new ObjectId(params.assistantId),
+	});
+
+	if (!assistant) {
+		throw error(404, "No assistant found");
+	}
+
+	if (!assistant.avatar) {
+		throw error(404, "No avatar found");
+	}
+
+	const fileId = collections.bucket.find({ filename: assistant._id.toString() });
+
+	let mime = "";
+
+	const content = await fileId.next().then(async (file) => {
+		mime = file?.metadata?.mime;
+
+		if (!file?._id) {
+			throw error(404, "Avatar not found");
+		}
+
+		const fileStream = collections.bucket.openDownloadStream(file?._id);
+
+		const fileBuffer = await new Promise<Buffer>((resolve, reject) => {
+			const chunks: Uint8Array[] = [];
+			fileStream.on("data", (chunk) => chunks.push(chunk));
+			fileStream.on("error", reject);
+			fileStream.on("end", () => resolve(Buffer.concat(chunks)));
+		});
+
+		return fileBuffer;
+	});
+
+	return new Response(content, {
+		headers: {
+			"Content-Type": mime ?? "application/octet-stream",
+		},
+	});
+};
diff --git a/src/routes/settings/assistants/[assistantId]/edit/+page.server.ts b/src/routes/settings/assistants/[assistantId]/edit/+page.server.ts
new file mode 100644
index 0000000000000000000000000000000000000000..22716f4a7644793b2c94ad86580088895a664fa4
--- /dev/null
+++ b/src/routes/settings/assistants/[assistantId]/edit/+page.server.ts
@@ -0,0 +1,136 @@
+import { base } from "$app/paths";
+import { requiresUser } from "$lib/server/auth";
+import { collections } from "$lib/server/database";
+import { fail, type Actions, redirect } from "@sveltejs/kit";
+import { ObjectId } from "mongodb";
+
+import { z } from "zod";
+import sizeof from "image-size";
+import { sha256 } from "$lib/utils/sha256";
+
+const newAsssistantSchema = z.object({
+	name: z.string().min(1),
+	modelId: z.string().min(1),
+	preprompt: z.string().min(1),
+	description: z.string().optional(),
+	exampleInput1: z.string().optional(),
+	exampleInput2: z.string().optional(),
+	exampleInput3: z.string().optional(),
+	exampleInput4: z.string().optional(),
+	avatar: z.union([z.instanceof(File), z.literal("null")]).optional(),
+});
+
+const uploadAvatar = async (avatar: File, assistantId: ObjectId): Promise<string> => {
+	const hash = await sha256(await avatar.text());
+	const upload = collections.bucket.openUploadStream(`${assistantId.toString()}`, {
+		metadata: { type: avatar.type, hash },
+	});
+
+	upload.write((await avatar.arrayBuffer()) as unknown as Buffer);
+	upload.end();
+
+	// only return the filename when upload throws a finish event or a 10s time out occurs
+	return new Promise((resolve, reject) => {
+		upload.once("finish", () => resolve(hash));
+		upload.once("error", reject);
+		setTimeout(() => reject(new Error("Upload timed out")), 10000);
+	});
+};
+
+export const actions: Actions = {
+	default: async ({ request, locals, params }) => {
+		const assistant = await collections.assistants.findOne({
+			_id: new ObjectId(params.assistantId),
+		});
+
+		if (!assistant) {
+			throw Error("Assistant not found");
+		}
+
+		if (assistant.createdById.toString() !== (locals.user?._id ?? locals.sessionId).toString()) {
+			throw Error("You are not the author of this assistant");
+		}
+
+		const formData = Object.fromEntries(await request.formData());
+
+		const parse = newAsssistantSchema.safeParse(formData);
+
+		if (!parse.success) {
+			// Loop through the errors array and create a custom errors array
+			const errors = parse.error.errors.map((error) => {
+				return {
+					field: error.path[0],
+					message: error.message,
+				};
+			});
+
+			return fail(400, { error: true, errors });
+		}
+
+		// can only create assistants when logged in, IF login is setup
+		if (!locals.user && requiresUser) {
+			const errors = [{ field: "preprompt", message: "Must be logged in. Unauthorized" }];
+			return fail(400, { error: true, errors });
+		}
+
+		const exampleInputs: string[] = [
+			parse?.data?.exampleInput1 ?? "",
+			parse?.data?.exampleInput2 ?? "",
+			parse?.data?.exampleInput3 ?? "",
+			parse?.data?.exampleInput4 ?? "",
+		].filter((input) => !!input);
+
+		const deleteAvatar = parse.data.avatar === "null";
+
+		let hash;
+		if (parse.data.avatar && parse.data.avatar !== "null" && parse.data.avatar.size > 0) {
+			const dims = sizeof(Buffer.from(await parse.data.avatar.arrayBuffer()));
+
+			if ((dims.height ?? 1000) > 512 || (dims.width ?? 1000) > 512) {
+				const errors = [{ field: "avatar", message: "Avatar too big" }];
+				return fail(400, { error: true, errors });
+			}
+
+			const fileCursor = collections.bucket.find({ filename: assistant._id.toString() });
+
+			// Step 2: Delete the existing file if it exists
+			let fileId = await fileCursor.next();
+			while (fileId) {
+				await collections.bucket.delete(fileId._id);
+				fileId = await fileCursor.next();
+			}
+
+			hash = await uploadAvatar(parse.data.avatar, assistant._id);
+		} else if (deleteAvatar) {
+			// delete the avatar
+			const fileCursor = collections.bucket.find({ filename: assistant._id.toString() });
+
+			let fileId = await fileCursor.next();
+			while (fileId) {
+				await collections.bucket.delete(fileId._id);
+				fileId = await fileCursor.next();
+			}
+		}
+
+		const { acknowledged } = await collections.assistants.replaceOne(
+			{
+				_id: assistant._id,
+			},
+			{
+				createdById: assistant?.createdById,
+				createdByName: locals.user?.username ?? locals.user?.name,
+				...parse.data,
+				exampleInputs,
+				avatar: deleteAvatar ? undefined : hash ?? assistant.avatar,
+				createdAt: new Date(),
+				updatedAt: new Date(),
+			}
+		);
+
+		if (acknowledged) {
+			throw redirect(302, `${base}/settings/assistants/${assistant._id}`);
+		} else {
+			throw Error("Update failed");
+		}
+	},
+};
diff --git a/src/routes/settings/assistants/[assistantId]/edit/+page.svelte b/src/routes/settings/assistants/[assistantId]/edit/+page.svelte
new file mode 100644
index 0000000000000000000000000000000000000000..682c29dfe02b840a5e8aff8067709e98ea46e285
--- /dev/null
+++ b/src/routes/settings/assistants/[assistantId]/edit/+page.svelte
@@ -0,0 +1,12 @@
+<script lang="ts">
+	import type { PageData, ActionData } from "./$types";
+	import { page } from "$app/stores";
+	import AssistantSettings from "$lib/components/AssistantSettings.svelte";
+
+	export let data: PageData;
+	export let form: ActionData;
+
+	$: assistant = data.assistants.find((el) => el._id.toString() === $page.params.assistantId);
+</script>
+
+<AssistantSettings bind:form {assistant} models={data.models} />
diff --git a/src/routes/settings/assistants/new/+page.server.ts b/src/routes/settings/assistants/new/+page.server.ts
new file mode 100644
index 0000000000000000000000000000000000000000..bdb077c761a62f0de97e57a67c45e29a71bc4e55
--- /dev/null
+++ b/src/routes/settings/assistants/new/+page.server.ts
@@ -0,0 +1,112 @@
+import { base } from "$app/paths";
+import { authCondition, requiresUser } from "$lib/server/auth";
+import { collections } from "$lib/server/database";
+import { fail, type Actions, redirect } from "@sveltejs/kit";
+import { ObjectId } from "mongodb";
+
+import { z } from "zod";
+import sizeof from "image-size";
+import { sha256 } from "$lib/utils/sha256";
+
+const newAsssistantSchema = z.object({
+	name: z.string().min(1),
+	modelId: z.string().min(1),
+	preprompt: z.string().min(1),
+	description: z.string().optional(),
+	exampleInput1: z.string().optional(),
+	exampleInput2: z.string().optional(),
+	exampleInput3: z.string().optional(),
+	exampleInput4: z.string().optional(),
+	avatar: z.instanceof(File).optional(),
+});
+
+const uploadAvatar = async (avatar: File, assistantId: ObjectId): Promise<string> => {
+	const hash = await sha256(await avatar.text());
+	const upload = collections.bucket.openUploadStream(`${assistantId.toString()}`, {
+		metadata: { type: avatar.type, hash },
+	});
+
+	upload.write((await avatar.arrayBuffer()) as unknown as Buffer);
+	upload.end();
+
+	// only return the filename when upload throws a finish event or a 10s time out occurs
+	return new Promise((resolve, reject) => {
+		upload.once("finish", () => resolve(hash));
+		upload.once("error", reject);
+		setTimeout(() => reject(new Error("Upload timed out")), 10000);
+	});
+};
+
+export const actions: Actions = {
+	default: async ({ request, locals }) => {
+		const formData = Object.fromEntries(await request.formData());
+
+		const parse = newAsssistantSchema.safeParse(formData);
+
+		if (!parse.success) {
+			// Loop through the errors array and create a custom errors array
+			const errors = parse.error.errors.map((error) => {
+				return {
+					field: error.path[0],
+					message: error.message,
+				};
+			});
+
+			return fail(400, { error: true, errors });
+		}
+
+		// can only create assistants when logged in, IF login is setup
+		if (!locals.user && requiresUser) {
+			const errors = [{ field: "preprompt", message: "Must be logged in. Unauthorized" }];
+			return fail(400, { error: true, errors });
+		}
+
+		const createdById = locals.user?._id ?? locals.sessionId;
+
+		const newAssistantId = new ObjectId();
+
+		const exampleInputs: string[] = [
+			parse?.data?.exampleInput1 ?? "",
+			parse?.data?.exampleInput2 ?? "",
+			parse?.data?.exampleInput3 ?? "",
+			parse?.data?.exampleInput4 ?? "",
+		].filter((input) => !!input);
+
+		let hash;
+		if (parse.data.avatar && parse.data.avatar.size > 0) {
+			const dims = sizeof(Buffer.from(await parse.data.avatar.arrayBuffer()));
+
+			if ((dims.height ?? 1000) > 512 || (dims.width ?? 1000) > 512) {
+				const errors = [
+					{
+						field: "avatar",
+						message:
+							"Avatar is too big. Please make sure the size of your avatar is no bigger than 512px by 512px.",
+					},
+				];
+				return fail(400, { error: true, errors });
+			}
+
+			hash = await uploadAvatar(parse.data.avatar, newAssistantId);
+		}
+
+		const { insertedId } = await collections.assistants.insertOne({
+			_id: newAssistantId,
+			createdById,
+			createdByName: locals.user?.username ?? locals.user?.name,
+			...parse.data,
+			exampleInputs,
+			avatar: hash,
+			createdAt: new Date(),
+			updatedAt: new Date(),
+		});
+
+		// add insertedId to user settings
+
+		await collections.settings.updateOne(authCondition(locals), {
+			$push: { assistants: insertedId },
+		});
+
+		throw redirect(302, `${base}/settings/assistants/${insertedId}`);
+	},
+};
diff --git a/src/routes/settings/assistants/new/+page.svelte b/src/routes/settings/assistants/new/+page.svelte
new file mode 100644
index 0000000000000000000000000000000000000000..e77cbfd26919d846978df0eaf68c2c7d86df9bcd
--- /dev/null
+++ b/src/routes/settings/assistants/new/+page.svelte
@@ -0,0 +1,9 @@
+<script lang="ts">
+	import type { ActionData, PageData } from "./$types";
+	import AssistantSettings from "$lib/components/AssistantSettings.svelte";
+
+	export let data: PageData;
+	export let form: ActionData;
+</script>
+
+<AssistantSettings bind:form models={data.models} />
diff --git a/static/fonts/Inter-Black.ttf b/static/fonts/Inter-Black.ttf
new file mode 100644
index 0000000000000000000000000000000000000000..b27822baea48062bf11617ce763e8d623c3a9769
Binary files /dev/null and b/static/fonts/Inter-Black.ttf differ
diff --git a/static/fonts/Inter-Bold.ttf b/static/fonts/Inter-Bold.ttf
new file mode 100644
index 0000000000000000000000000000000000000000..fe23eeb9c93a377d0f4ab003f1f77b555d19b1d1
Binary files /dev/null and b/static/fonts/Inter-Bold.ttf differ
diff --git a/static/fonts/Inter-ExtraBold.ttf b/static/fonts/Inter-ExtraBold.ttf
new file mode 100644
index 0000000000000000000000000000000000000000..874b1b0dd7c63f46240530a710ccd503d58d866d
Binary files /dev/null and b/static/fonts/Inter-ExtraBold.ttf differ
diff --git a/static/fonts/Inter-ExtraLight.ttf b/static/fonts/Inter-ExtraLight.ttf
new file mode 100644
index 0000000000000000000000000000000000000000..c993e82216cd9b62f025c441fb65b7b7ccf2f96e
Binary files /dev/null and b/static/fonts/Inter-ExtraLight.ttf differ
diff --git a/static/fonts/Inter-Light.ttf b/static/fonts/Inter-Light.ttf
new file mode 100644
index 0000000000000000000000000000000000000000..71188f5cb2815547a5fb785f220910324ad8aa65
Binary files /dev/null and b/static/fonts/Inter-Light.ttf differ
diff --git a/static/fonts/Inter-Medium.ttf b/static/fonts/Inter-Medium.ttf
new file mode 100644
index 0000000000000000000000000000000000000000..a01f3777a6fc284b7a720c0f8248a27066389ef9
Binary files /dev/null and b/static/fonts/Inter-Medium.ttf differ
diff --git a/static/fonts/Inter-Regular.ttf b/static/fonts/Inter-Regular.ttf
new file mode 100644
index 0000000000000000000000000000000000000000..5e4851f0ab7e0268da6ce903306e2f871ee19821
Binary files /dev/null and b/static/fonts/Inter-Regular.ttf differ
diff --git a/static/fonts/Inter-SemiBold.ttf b/static/fonts/Inter-SemiBold.ttf
new file mode 100644
index 0000000000000000000000000000000000000000..ecc7041e23ed1a61574efe48e8bc42441a401d6e
Binary files /dev/null and b/static/fonts/Inter-SemiBold.ttf differ
diff --git a/static/fonts/Inter-Thin.ttf b/static/fonts/Inter-Thin.ttf
new file mode 100644
index 0000000000000000000000000000000000000000..fe77243fc7ae5f860243c768fc7bc659839988a5
Binary files /dev/null and b/static/fonts/Inter-Thin.ttf differ